From 8ee6bc7ecbafff4c926e1f6ced7bb4cda6b02282 Mon Sep 17 00:00:00 2001 From: webb <822028533@qq.com> Date: Thu, 15 Feb 2024 14:53:33 +0800 Subject: [PATCH] v1.0.0 --- .dockerignore | 34 ++ .gitignore | 34 ++ Dockerfile | 38 ++ LICENSE-2.0.txt | 202 +++++++++ README.md | 3 + app/controller/message_contoller.go | 309 ++++++++++++++ app/middleware/auth_middleware.go | 95 +++++ app/model/base_model.go | 31 ++ app/model/message_model.go | 35 ++ app/repository/auto_repository.go | 25 ++ app/repository/message_repository.go | 276 +++++++++++++ app/request/base_requests.go | 104 +++++ app/request/message_requests.go | 179 ++++++++ app/response/base_response.go | 28 ++ app/response/message_response.go | 44 ++ config/config.go | 63 +++ config/config.yaml | 38 ++ database/migrations.go | 29 ++ database/mysql.go | 106 +++++ docker-compose.yml | 31 ++ docs/docs.go | 586 +++++++++++++++++++++++++++ docs/swagger.json | 562 +++++++++++++++++++++++++ docs/swagger.yaml | 375 +++++++++++++++++ go.mod | 70 ++++ go.sum | 209 ++++++++++ logs/log.go | 63 +++ main.go | 64 +++ resources/lang/en.yaml | 2 + resources/lang/zh.yaml | 5 + router/index_rotuer.go | 28 ++ router/message_router.go | 39 ++ utils/log.go | 32 ++ utils/message.go | 21 + 33 files changed, 3760 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE-2.0.txt create mode 100644 README.md create mode 100644 app/controller/message_contoller.go create mode 100755 app/middleware/auth_middleware.go create mode 100644 app/model/base_model.go create mode 100644 app/model/message_model.go create mode 100644 app/repository/auto_repository.go create mode 100644 app/repository/message_repository.go create mode 100755 app/request/base_requests.go create mode 100755 app/request/message_requests.go create mode 100644 app/response/base_response.go create mode 100644 app/response/message_response.go create mode 100644 config/config.go create mode 100644 config/config.yaml create mode 100755 database/migrations.go create mode 100644 database/mysql.go create mode 100644 docker-compose.yml create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logs/log.go create mode 100644 main.go create mode 100644 resources/lang/en.yaml create mode 100644 resources/lang/zh.yaml create mode 100644 router/index_rotuer.go create mode 100644 router/message_router.go create mode 100644 utils/log.go create mode 100644 utils/message.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d99a2b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# logs +logs/*.log + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d99a2b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# logs +logs/*.log + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c6a94a4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# 第一阶段:构建应用程序 +FROM golang:1.21.7-alpine AS builder + +# 设置作者信息 +LABEL authors="webb" + +# 设置工作目录 +WORKDIR /app + +# 复制所有文件到工作目录 +COPY . . + +# 设置环境变量 +ENV GOPROXY=https://mirrors.cloud.tencent.com/go/ +ENV GIN_MODE=release + +# 构建应用程序 +RUN go build -o message . + +# 第二阶段:构建最终镜像 +FROM alpine:latest + +# 设置工作目录 +WORKDIR /app + +# 创建日志目录 +RUN mkdir logs + +# 从第一阶段复制配置文件、资源文件和应用程序到最终镜像 +COPY --from=builder /app/config/config.yaml config/config.yaml +COPY --from=builder /app/resources resources +COPY --from=builder /app/message . + +# 定义容器启动命令 +CMD ["./message"] + +# 暴露端口 +EXPOSE 1204 diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5899176 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 消息服务 + +让您的项目快速拥有消息功能。 diff --git a/app/controller/message_contoller.go b/app/controller/message_contoller.go new file mode 100644 index 0000000..2c2b8c9 --- /dev/null +++ b/app/controller/message_contoller.go @@ -0,0 +1,309 @@ +package controller + +import ( + lang "github.com/gin-contrib/i18n" + "github.com/gin-gonic/gin" + "message/app/model" + "message/app/repository" + "message/app/request" + "message/app/response" + "message/logs" + "net/http" +) + +// MessageIndex 查询消息 +// +// @Summary 查询消息 +// @Description 根据用户凭证查询消息 +// @Tags message +// @Accept json +// @Produce json +// @Security BasicAuth +// @Param filter query string false "过滤语句(title = 标题,status = 0|1|2,...)" +// @Param sortColumn query string false "排序列(created_at|updated_at|sender_ids|title|content|category|big_content|introducer_ids|status)" +// @Param sortType query string false "排序类型(asc/desc)" +// @Param page query int false "查询第几页数据" +// @Success 200 {array} []response.Message "消息信息" +// @Failure 400 {object} request.ValidationError "请求参数错误" +// @Failure 401 {object} response.HTTPError "凭证错误" +// @Failure 502 {object} response.HTTPError "系统异常" +// @Router /message [get] +func MessageIndex(ctx *gin.Context) { + // 从上下文中获取 token + token, tokenExists := ctx.Get("token") + // 从上下文中获取 message + message, messageExists := ctx.Get("message") + // 从上下文中获取 messageFilters + messageFilter, messageFilterExists := ctx.Get("messageFilters") + + // 检查 token 和 message 是否存在 + if !tokenExists || !messageExists { + response.NewError( + ctx, + http.StatusBadGateway, + lang.MustGetMessage(ctx, "badGateway"), + ) + return + } + + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + // 将 message 转换为 MessageRequest 类型 + messageRequest := message.(*request.MessageRequest) + + var messageFilters []request.MessageFilterRequest + if messageFilterExists { + // 将 messageFilter 转换为 []MessageFilterRequest 类型 + messageFilterRequest := messageFilter.(*[]request.MessageFilterRequest) + for _, filter := range *messageFilterRequest { + messageFilters = append( + messageFilters, + filter, + ) + } + } + + logs.LogInfo.Infof("MessageIndex %v %s", messageRequest, messageToken.AuthId) + + // 返回查询结果 + ctx.JSON( + http.StatusOK, + repository.QueryMessagesByMessageTokenMessageRequest( + messageToken, + messageRequest, + messageFilters, + ), + ) +} + +// MessageCreate 创建消息 +// +// @Summary 创建消息 +// @Description 创建消息 +// @Tags message +// @Accept json +// @Produce json +// @Security BasicAuth +// @Param _ body request.MessageCreateUpdateRequest true "创建的数据" +// @Success 200 {object} response.Message "创建成功" +// @Success 202 {object} response.HTTPError "创建失败" +// @Failure 400 {object} request.ValidationError "请求参数错误" +// @Failure 401 {object} response.HTTPError "凭证错误" +// @Failure 502 {object} response.HTTPError "系统异常" +// @Router /message [post] +func MessageCreate(ctx *gin.Context) { + // 从上下文中获取 token + token, tokenExists := ctx.Get("token") + // 从上下文中获取 messageCreateUpdate + messageCreate, messageCreateExists := ctx.Get("messageCreateUpdate") + + // 检查 token 和 messageCreateUpdate 是否存在 + if !tokenExists || !messageCreateExists { + response.NewError( + ctx, + http.StatusBadGateway, + lang.MustGetMessage(ctx, "badGateway"), + ) + return + } + + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + // 将 messageCreateUpdate 转换为 MessageCreateUpdateRequest 类型 + messageCreateRequest := messageCreate.(*request.MessageCreateUpdateRequest) + + // 创建消息 + message, err := repository.CreateMessage( + messageToken, + messageCreateRequest, + ) + + if err != nil { + // 如果创建失败,返回状态码 Accepted + response.NewError( + ctx, + http.StatusAccepted, + lang.MustGetMessage(ctx, "createMessageFail"), + ) + + logs.LogInfo.Infof("MessageCreate-失败 %s %s", err, messageToken.AuthId) + return + } + + logs.LogInfo.Infof("MessageCreate-成功 %s", messageToken.AuthId) + + // 返回创建成功的消息 + ctx.JSON(http.StatusOK, message) +} + +// MessageUpdate 更新消息 +// +// @Summary 更新消息 +// @Description 根据消息id更新消息 +// @Tags message +// @Accept json +// @Produce json +// @Security BasicAuth +// @Param id path string true "消息id" +// @Param _ body request.MessageCreateUpdateRequest true "更新消息" +// @Success 200 {object} response.Message "更新成功" +// @Success 202 {object} response.HTTPError "更新失败" +// @Failure 400 {object} request.ValidationError "请求参数错误" +// @Failure 401 {object} response.HTTPError "凭证错误" +// @Failure 404 {object} response.HTTPError "找不到数据" +// @Failure 502 {object} response.HTTPError "系统异常" +// @Router /message/{id} [put] +func MessageUpdate(ctx *gin.Context) { + // 从上下文中获取 token + token, tokenExists := ctx.Get("token") + // 从上下文中获取 messageCreateUpdate + messageUpdate, messageUpdateExists := ctx.Get("messageCreateUpdate") + + // 检查 token 和 messageCreateUpdate 是否存在 + if !tokenExists || !messageUpdateExists { + response.NewError( + ctx, + http.StatusBadGateway, + lang.MustGetMessage(ctx, "badGateway"), + ) + return + } + + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + // 将 messageCreateUpdate 转换为 MessageCreateUpdateRequest 类型 + messageUpdateRequest := messageUpdate.(*request.MessageCreateUpdateRequest) + + // 根据id查询消息 + oldMessage := repository.QueryMessageById( + messageToken.AuthId, + ctx.Param("id"), + ) + + if oldMessage == nil { + // 如果找不到对应的消息,返回状态码 NotFound + response.NewError( + ctx, + http.StatusNotFound, + lang.MustGetMessage(ctx, "notFound"), + ) + return + } + + // 更新消息 + messageNew, err := repository.UpdateMessage( + oldMessage, + messageUpdateRequest, + ) + if err != nil { + // 如果更新失败,返回状态码 Accepted + response.NewError( + ctx, + http.StatusAccepted, + lang.MustGetMessage(ctx, "updateMessageFail"), + ) + + logs.LogInfo.Infof("MessageUpdate-失败 %s %s", err, messageToken.AuthId) + return + } + + logs.LogInfo.Infof("MessageUpdate-成功 %s", messageToken.AuthId) + + // 返回更新成功后的消息 + ctx.JSON(http.StatusOK, messageNew) +} + +// MessageUpdateStatus 更新消息状态 +// +// @Summary 更新状态 +// @Description 根据数组的数据更新消息状态 +// @Tags message +// @Accept json +// @Produce json +// @Security BasicAuth +// @Param _ body []request.MessageStatusRequest true "消息状态" +// @Success 200 {object} []response.MessageStatusResponse "更新后返回的数据" +// @Failure 400 {object} request.ValidationError "请求参数错误" +// @Failure 401 {object} response.HTTPError "凭证错误" +// @Failure 502 {object} response.HTTPError "系统异常" +// @Router /message/status [put] +func MessageUpdateStatus(ctx *gin.Context) { + // 从上下文中获取 token + token, tokenExists := ctx.Get("token") + // 从上下文中获取 messageStatus + messageStatus, messageExists := ctx.Get("messageStatus") + + // 检查 token 和 messageStatus 是否存在 + if !tokenExists || !messageExists { + response.NewError( + ctx, + http.StatusBadGateway, + lang.MustGetMessage(ctx, "badGateway"), + ) + return + } + + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + // 将 messageStatus 转换为 []MessageStatusRequest 类型 + messageStatusRequest := messageStatus.(*[]request.MessageStatusRequest) + + logs.LogInfo.Infof("MessageUpdateStatus %v %s", messageStatusRequest, messageToken.AuthId) + + // 更新消息状态,并返回更新后的结果 + ctx.JSON( + http.StatusOK, + repository.UpdateMessageStatus( + messageToken, + messageStatusRequest, + ), + ) +} + +// MessageDelete 删除消息 +// +// @Summary 删除消息 +// @Description 根据数组的数据删除消息 +// @Tags message +// @Accept json +// @Produce json +// @Security BasicAuth +// @Param _ body []request.MessageDeleteRequest true "删除的消息" +// @Success 200 {object} []response.MessageDeleteResponse "更新后返回的数据" +// @Failure 400 {object} request.ValidationError "请求参数错误" +// @Failure 401 {object} response.HTTPError "凭证错误" +// @Failure 404 {string} string "找不到兑换码" +// @Failure 502 {string} string "系统异常" +// @Router /message [delete] +func MessageDelete(ctx *gin.Context) { + // 从上下文中获取 token + token, tokenExists := ctx.Get("token") + // 从上下文中获取 messageDelete + messageDelete, messageDeleteExists := ctx.Get("messageDelete") + + // 检查 token 和 messageDelete 是否存在 + if !tokenExists || !messageDeleteExists { + response.NewError( + ctx, + http.StatusBadGateway, + lang.MustGetMessage(ctx, "badGateway"), + ) + return + } + + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + // 将 messageDelete 转换为 []MessageDeleteRequest 类型 + messageDeleteRequests := messageDelete.(*[]request.MessageDeleteRequest) + + logs.LogInfo.Infof("MessageDelete %v %s", messageDeleteRequests, messageToken.AuthId) + + // 删除消息,并返回删除结果 + ctx.JSON( + http.StatusOK, + repository.DeleteMessagesById( + messageToken, + messageDeleteRequests, + ), + ) +} diff --git a/app/middleware/auth_middleware.go b/app/middleware/auth_middleware.go new file mode 100755 index 0000000..3f97f45 --- /dev/null +++ b/app/middleware/auth_middleware.go @@ -0,0 +1,95 @@ +package middleware + +import ( + "encoding/base64" + "fmt" + lang "github.com/gin-contrib/i18n" + "github.com/gin-gonic/gin" + "message/app/repository" + "message/app/response" + "message/logs" + "net/http" + "strings" +) + +// parseAuthorization 函数用于解析 Authorization 头中的 Basic token。 +// +// 它接受一个字符串参数 basic,该字符串应包含完整的 Authorization 请求头的值。 +// +// 函数返回一个包含两个字符串的切片和一个错误对象。 +// +// 如果解析成功,切片中将包含从基于 Base64 编码的 token 中解码得到的信息; +// +// 如果解析失败,则返回一个错误。 +func parseAuthorization(authorization string) (token []string, err error) { + // 使用空格将传入的 basic 字符串分割成两部分。 + parts := strings.Split(authorization, " ") + // 检查分割后的结果是否正好两部分,并且第一部分(不区分大小写)是否为"basic"。 + if len(parts) != 2 || strings.ToLower(parts[0]) != "basic" { + // 如果不满足条件,返回一个空的字符串切片和一个格式错误的错误信息。 + return []string{}, fmt.Errorf("格式错误") + } + + // 尝试对第二部分(即 Base64 编码的 token)进行解码。 + decodeByte, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + // 如果解码过程中发生错误,同样返回一个空的字符串切片和一个格式错误的错误信息。 + return []string{}, fmt.Errorf("格式错误") + } + + // 将解码后的字节序列转换为字符串,并以冒号为分隔符进行分割。 + info := strings.Split(string(decodeByte), ":") + // 检查分割后的结果是否正好两部分,这通常对应于用户名和密码。 + if len(info) != 2 { + // 如果不满足条件,再次返回一个空的字符串切片和一个格式错误的错误信息。 + return []string{}, fmt.Errorf("格式错误") + } + // 如果所有检查都通过,则返回解析得到的信息和 nil 错误。 + return info, nil +} + +// AuthMiddleware 是一个 Gin 中间件函数,用于验证请求的授权信息。 +// +// 该中间件从请求头中获取 Authorization,并解析为授权信息。 +// +// 授权信息通过调用 repository.GetMessageToken() 方法获取消息令牌。 +// +// 如果授权信息无效或获取消息令牌失败,则返回相应的错误响应。 +// +// 否则,将消息令牌设置到上下文中,并继续处理后续请求。 +// +// 返回一个 gin.HandlerFunc 处理程序函数。 +func AuthMiddleware() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 从请求头中获取 Authorization 并解析 Authorization + authorization, err := parseAuthorization(ctx.GetHeader("Authorization")) + if err != nil { + logs.LogInfo.Infof("AuthMiddleware-失败 %s %s", err, ctx.ClientIP()) + response.NewError( + ctx, + http.StatusUnauthorized, + lang.MustGetMessage(ctx, "unauthorized"), + ) + ctx.Abort() + return + } + + // 获取消息令牌 + token, err := repository.GetMessageToken(authorization[0], authorization[1]) + if err != nil { + logs.LogInfo.Infof("AuthMiddleware-失败-找不到凭证 %s", ctx.ClientIP()) + response.NewError( + ctx, + http.StatusUnauthorized, + lang.MustGetMessage(ctx, "unauthorized"), + ) + ctx.Abort() + return + } + + logs.LogInfo.Infof("AuthMiddleware-成功 %s %s", ctx.ClientIP(), token.AuthId) + // 将消息令牌设置到上下文中 + ctx.Set("token", token) + ctx.Next() + } +} diff --git a/app/model/base_model.go b/app/model/base_model.go new file mode 100644 index 0000000..a714aab --- /dev/null +++ b/app/model/base_model.go @@ -0,0 +1,31 @@ +package model + +import ( + "database/sql/driver" + "errors" + "strings" +) + +// StringArray 是一个自定义类型,表示字符串数组。用于MYSQL不支持字符串数组。 +type StringArray []string + +// Scan 实现了 sql.Scanner 接口,用于将数据库中的原始数据转换为 StringArray 类型。 +func (sa *StringArray) Scan(src interface{}) error { + var source string + switch src := src.(type) { + case []byte: + source = string(src) + case string: + source = src + default: + return errors.New("incompatible type for SenderIDs") + } + + *sa = strings.Split(source, ",") + return nil +} + +// Value 实现了 driver.Valuer 接口,用于将 StringArray 类型转换为数据库中的原始数据。 +func (sa StringArray) Value() (driver.Value, error) { + return strings.Join(sa, ","), nil +} diff --git a/app/model/message_model.go b/app/model/message_model.go new file mode 100644 index 0000000..5b7743a --- /dev/null +++ b/app/model/message_model.go @@ -0,0 +1,35 @@ +package model + +import ( + "gorm.io/gorm" +) + +// 定义消息状态的常量 +const ( + Unread = iota // 未读状态,默认为 0 + Read // 已读状态 + Archived // 归档状态 +) + +// Message 消息 +type Message struct { + gorm.Model `json:"-"` + MessageId string `gorm:"type:varchar(32);index;unique;not null;comment:消息id"` + SenderIds StringArray `gorm:"type:text;comment:发送者的ID集合"` + Title string `gorm:"type:varchar(25);not null;comment:消息标题"` + Content string `gorm:"type:varchar(50);not null;comment:消息内容"` + Category string `gorm:"type:varchar(50);index;not null;comment:消息类别"` + BigContent string `gorm:"type:longtext;not null;comment:消息的详细内容"` + IntroducerIds StringArray `gorm:"type:text;comment:接收者的ID集合"` + Status uint8 `gorm:"type:tinyint;default:0;comment:消息阅读状态"` +} + +// MessageToken 查询消息的凭证 +type MessageToken struct { + gorm.Model `json:"_"` + AuthId string `json:"auth_id,omitempty" gorm:"type:varchar(32);index;unique;comment:"` // User字段长度为32 + Token string `json:"token,omitempty" gorm:"type:varchar(255)"` // Token字段长度为255 +} + +type MessageCategory struct { +} diff --git a/app/repository/auto_repository.go b/app/repository/auto_repository.go new file mode 100644 index 0000000..1329e14 --- /dev/null +++ b/app/repository/auto_repository.go @@ -0,0 +1,25 @@ +package repository + +import ( + "message/app/model" + "message/database" +) + +// GetMessageToken 尝试获取一个符合指定条件的MessageToken。 +// +// 它返回找到的MessageToken和可能出现的错误。如果记录不存在,将返回nil和gorm.ErrRecordNotFound。 +func GetMessageToken(authId string, authToken string) (model.MessageToken, error) { + var token model.MessageToken + result := database.DB.Model(model.MessageToken{}). + Where("auth_id = ?", authId). + Where("token = ?", authToken). + First(&token) + + if result.Error != nil { + // 直接返回错误,包括未找到记录的情况 + return token, result.Error + } + + // 记录被成功找到,返回token的指针和nil作为错误 + return token, nil +} diff --git a/app/repository/message_repository.go b/app/repository/message_repository.go new file mode 100644 index 0000000..52ff8f2 --- /dev/null +++ b/app/repository/message_repository.go @@ -0,0 +1,276 @@ +package repository + +import ( + "fmt" + "message/app/model" + "message/app/request" + "message/app/response" + "message/config" + "message/database" + "message/utils" + "slices" + "strings" +) + +// QueryMessagesByMessageTokenMessageRequest 根据消息凭证和消息请求查询消息 +func QueryMessagesByMessageTokenMessageRequest( + // 消息凭证 + token model.MessageToken, + // 消息请求参数 + messageRequest *request.MessageRequest, + // 消息过滤器 + filters []request.MessageFilterRequest, +) []response.Message { + var messages []response.Message + // 创建消息查询对象 + query := database.DB.Model(&model.Message{}) + + // 根据消息凭证中的 AuthId 进行筛选或introducer_ids等于空字符串 + query.Where( + query.Where( + "introducer_ids LIKE ?", + fmt.Sprintf("%%%s%%", token.AuthId), + ).Or("introducer_ids = ?", ""), + ) + + if len(filters) > 0 { + // 根据传入的过滤器条件进行进一步筛选 + for _, filter := range filters { + if filter.Comparison == "in" { + query.Where( + fmt.Sprintf( + "%s %s ?", + filter.Column, + filter.Comparison, + ), + strings.Split(filter.Value, "|"), + ) + } else { + query.Where( + fmt.Sprintf( + "%s %s ?", + filter.Column, + filter.Comparison, + ), + filter.Value, + ) + } + } + } + + // 根据排序字段和排序类型进行排序 + if messageRequest.SortColumn == "" { + messageRequest.SortColumn = "created_at" + } + if messageRequest.SortType == "" { + messageRequest.SortType = "desc" + } + query.Order(fmt.Sprintf( + "%s %s", + messageRequest.SortColumn, + messageRequest.SortType, + )) + + // 根据分页信息查询消息并存储在 messages 中 + maxLimit := config.AppConfig.API.MaxLimit + if messageRequest.Page == 0 { + messageRequest.Page = 1 + } + query.Limit(maxLimit).Offset((messageRequest.Page - 1) * maxLimit).Find(&messages) + + // 返回查询到的消息数组 + return messages +} + +// CreateMessage 创建一条新消息 +func CreateMessage( + token model.MessageToken, + createMessage *request.MessageCreateUpdateRequest, +) (*response.Message, error) { + messageId := utils.BuildMessageId() + // 将消息插入到数据库中 + result := database.DB.Model(&model.Message{}).Create(&model.Message{ + // 生成消息 ID + MessageId: messageId, + // 设置消息的发送者 ID + SenderIds: []string{token.AuthId}, + // 设置消息标题 + Title: createMessage.Title, + // 设置消息内容 + Content: createMessage.Content, + // 设置消息类别 + Category: createMessage.Category, + // 设置消息大文本内容 + BigContent: createMessage.BigContent, + // 设置消息介绍者 ID + IntroducerIds: createMessage.IntroducerIds, + }) + // 如果发生错误或者影响的行数为 0,则返回 nil + if result.Error != nil { + return nil, result.Error + } + + newMessage := &response.Message{} + database.DB.Model(&model.Message{}).Where("message_id = ?", messageId).First(newMessage) + // 返回创建的消息对象 + return newMessage, nil +} + +// UpdateMessage 更新消息内容 +func UpdateMessage( + // 待更新的消息对象 + message *model.Message, + // 消息更新的内容 + messageUpdate *request.MessageCreateUpdateRequest, +) (*response.Message, error) { + // 更新消息标题 + message.Title = messageUpdate.Title + // 更新消息内容 + message.Content = messageUpdate.Content + // 更新消息类别 + message.Category = messageUpdate.Category + // 更新消息大文本内容 + message.BigContent = messageUpdate.BigContent + // 更新消息介绍者 ID + message.IntroducerIds = messageUpdate.IntroducerIds + + // 保存更新后的消息到数据库中 + result := database.DB.Model(&model.Message{}).Where("id = ?", message.ID).Updates(message) + // 如果发生错误或者影响的行数为 0,则返回 nil + if result.Error != nil { + return nil, result.Error + } + + newMessage := &response.Message{} + database.DB.Model(&model.Message{}).Where("id = ?", message.ID).First(newMessage) + // 返回更新后的消息对象 + return newMessage, nil +} + +// UpdateMessageStatus 更新消息状态 +func UpdateMessageStatus( + // 消息凭证 + token model.MessageToken, + // 要更新的状态请求切片 + status *[]request.MessageStatusRequest, +) []response.MessageStatusResponse { + // 初始化一个空的MessageStatusResponse切片用于存放每个状态更新的结果 + results := make([]response.MessageStatusResponse, 0) + + // 遍历状态请求切片,对每个请求进行处理 + for _, statusRequest := range *status { + // 对model.Message模型执行更新操作,设置新的状态 + // 使用LIKE查询匹配introducer_ids,并确保message_id与AuthId相符 + result := database.DB.Model(&model.Message{}). + Where("introducer_ids LIKE ?", fmt.Sprintf("%%%s%%", token.AuthId)). + Where("message_id = ?", statusRequest.Id). + Update("status", statusRequest.Status) + + // 将每次更新操作的结果封装到MessageStatusResponse中,并追加到结果切片中 + results = append(results, response.MessageStatusResponse{ + Id: statusRequest.Id, + Status: statusRequest.Status, + Result: result.Error == nil && result.RowsAffected != 0, + }) + } + + // 返回所有更新操作的结果 + return results +} + +// QueryMessageById 通过消息 ID 查询消息 +func QueryMessageById( + // 用户认证 ID + authId string, + // 消息 ID + id string, +) *model.Message { + // 初始化一个空的消息对象 + message := &model.Message{} + + // 在数据库中查询匹配条件的消息 + result := database.DB.Model(model.Message{}). + Where("sender_ids LIKE ?", fmt.Sprintf("%%%s%%", authId)). + Where("message_id = ?", id). + First(message) + + // 如果查询出错或者没有匹配到数据,则返回 nil + if result.Error != nil || result.RowsAffected == 0 { + return nil + } + + // 返回查询到的消息对象 + return message +} + +// DeleteMessagesById 根据消息 ID 批量删除消息 +func DeleteMessagesById( + // 消息凭证 + token model.MessageToken, + // 要删除的消息请求切片 + deleteRequests *[]request.MessageDeleteRequest, +) []response.MessageDeleteResponse { + // 存储要物理删除的消息 ID + var deletes []string + // 存储要软删除的消息 ID + var softDeletes []string + + // 遍历消息删除请求切片,将要删除和要软删除的消息 ID 分别添加到对应的切片中 + for _, messageDelete := range *deleteRequests { + if messageDelete.Delete { + deletes = append(deletes, messageDelete.MessageId) + } else { + softDeletes = append(softDeletes, messageDelete.MessageId) + } + } + + // 物理删除要删除的消息 + database.DB.Unscoped(). + Where("sender_ids LIKE ?", fmt.Sprintf("%%%s%%", token.AuthId)). + Where("message_id in ?", deletes). + Delete(&model.Message{}) + + // 软删除要软删除的消息 + database.DB. + Where("sender_ids LIKE ?", fmt.Sprintf("%%%s%%", token.AuthId)). + Where("message_id in ?", softDeletes). + Delete(&model.Message{}) + + // 存储物理删除失败的消息 ID + var failDeletes []string + database.DB. + Select("message_id"). + Where("sender_ids LIKE ?", fmt.Sprintf("%%%s%%", token.AuthId)). + Where("message_id in ?", softDeletes).Find(&failDeletes) + + // 存储软删除失败的消息 ID + var failSoftDeletes []string + database.DB. + Select("message_id"). + Where("sender_ids LIKE ?", fmt.Sprintf("%%%s%%", token.AuthId)). + Where("message_id in ?", softDeletes).Find(&failSoftDeletes) + + // 存储删除操作的结果切片 + var results []response.MessageDeleteResponse + + // 遍历物理删除的消息 ID,将每个 ID 和删除状态封装到 MessageDeleteResponse 中,并根据删除是否成功设置相应的状态 + for _, id := range deletes { + results = append(results, response.MessageDeleteResponse{ + Id: id, + Delete: true, + Status: slices.Contains(failDeletes, id), + }) + } + + // 遍历软删除失败的消息 ID,将每个 ID 和删除状态封装到 MessageDeleteResponse 中,并根据删除是否成功设置相应的状态 + for _, id := range failSoftDeletes { + results = append(results, response.MessageDeleteResponse{ + Id: id, + Delete: false, + Status: slices.Contains(failSoftDeletes, id), + }) + } + + // 返回删除操作的结果切片 + return results +} diff --git a/app/request/base_requests.go b/app/request/base_requests.go new file mode 100755 index 0000000..9543b9b --- /dev/null +++ b/app/request/base_requests.go @@ -0,0 +1,104 @@ +package request + +import ( + "errors" + lang "github.com/gin-contrib/i18n" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "message/app/response" + "message/logs" + "net/http" +) + +// Validate 是一个用于参数校验的 validator 实例 +var Validate = validator.New(validator.WithRequiredStructEnabled()) + +// ValidationError 用于存储参数校验失败时的相关信息 +type ValidationError struct { + Field string `json:"field"` // 字段名 + Type string `json:"type"` // 字段类型 + Value interface{} `json:"value"` // 字段数值 + Param string `json:"param"` // 参数 + Message string `json:"message"` // 错误消息 +} + +// validateSliceAndSetContext 对传入的切片类型数据进行校验,并将校验通过的数据存储到 Gin 上下文中 +func validateSliceAndSetContext(ctx *gin.Context, object interface{}, saveKey string) bool { + // 检查并绑定 JSON 数据到对象 + if err := ctx.ShouldBindJSON(object); err != nil { + logs.LogError.Errorf("validateSliceAndSetContext %s %s %s", saveKey, object, err) + response.NewError( + ctx, + http.StatusBadGateway, + lang.MustGetMessage(ctx, "badGateway"), + ) + return false + } + + // 使用 Validate 对象进行参数校验 + if err := Validate.Var(object, "required,gt=0,dive,required"); err != nil { + handlingErrors(ctx, err) + return false + } + + // 将校验通过的数据存储到 Gin 上下文中 + ctx.Set(saveKey, object) + ctx.Next() + return true +} + +// validateStructAndSetContext 对传入的结构体数据进行校验,并将校验通过的数据存储到 Gin 上下文中 +func validateStructAndSetContext(ctx *gin.Context, object interface{}, saveKey string) bool { + // 检查并绑定数据到对象 + if err := ctx.ShouldBind(object); err != nil { + logs.LogError.Errorf("validateStructAndSetContext %s %s %s", saveKey, object, err) + response.NewError( + ctx, + http.StatusBadGateway, + lang.MustGetMessage(ctx, "badGateway"), + ) + return false + } + + // 使用 Validate 对象进行参数校验 + if err := Validate.Struct(object); err != nil { + handlingErrors(ctx, err) + return false + } + + // 将校验通过的数据存储到 Gin 上下文中 + ctx.Set(saveKey, object) + ctx.Next() + return true +} + +// handlingErrors 处理错误 +func handlingErrors(ctx *gin.Context, err error) { + // 处理校验错误 + var invalidValidationError *validator.InvalidValidationError + if errors.As(err, &invalidValidationError) { + logs.LogError.Errorf("handlingErrors %s", err) + response.NewError( + ctx, + http.StatusBadGateway, + lang.MustGetMessage(ctx, "badGateway"), + ) + ctx.Abort() + return + } + + var errorValidations []ValidationError + for _, err := range err.(validator.ValidationErrors) { + errorValidations = append(errorValidations, ValidationError{ + Field: err.Field(), + Type: err.Type().String(), + Value: err.Value(), + Param: err.Param(), + Message: err.Tag(), + }) + } + + // 返回校验错误信息给客户端 + ctx.JSON(http.StatusBadRequest, errorValidations) + ctx.Abort() +} diff --git a/app/request/message_requests.go b/app/request/message_requests.go new file mode 100755 index 0000000..61917a8 --- /dev/null +++ b/app/request/message_requests.go @@ -0,0 +1,179 @@ +package request + +import ( + "github.com/gin-gonic/gin" + "message/app/model" + "message/logs" + "strings" +) + +type MessageFilterRequest struct { + Column string `description:"过滤的列" validate:"required,oneof=created_at updated_at sender_ids title content category big_content introducer_ids status"` + Comparison string `description:"比较" validate:"required,oneof=> = < >= <= != like in"` + Value string `description:"过滤的值" validate:"required"` +} + +type MessageRequest struct { + Filter string `description:"过滤的语句" form:"filter" example:"status = 1"` + SortColumn string `description:"排序列" form:"sortColumn" validate:"omitempty,oneof=created_at updated_at sender_ids title content category big_content introducer_ids status" example:"title"` + SortType string `description:"排序类型" form:"sortType" validate:"omitempty,oneof=desc asc" example:"asc"` + Page int `description:"查询第几页" form:"page" validate:"omitempty,min=1,max=99999999" example:"1"` +} + +// ValidateMessageRequestMiddleware 用于验证消息请求参数的中间件 +func ValidateMessageRequestMiddleware() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 从上下文中获取 token + token, _ := ctx.Get("token") + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + + message := &MessageRequest{} + if !validateStructAndSetContext( + ctx, + message, + "message", + ) { + logs.LogInfo.Infof("ValidateMessageRequestMiddleware-参数错误 %s", messageToken.AuthId) + return + } + + if message.Filter != "" { + var filters []MessageFilterRequest + for _, filterStr := range strings.Split(message.Filter, ",") { + filter := strings.Split(filterStr, " ") + switch len(filter) { + case 1: + filters = append(filters, MessageFilterRequest{ + Column: filter[0], + }) + break + case 2: + filters = append(filters, MessageFilterRequest{ + Column: filter[0], + Comparison: filter[1], + }) + break + case 3: + filters = append(filters, MessageFilterRequest{ + Column: filter[0], + Comparison: filter[1], + Value: filter[2], + }) + break + default: + filters = append(filters, MessageFilterRequest{}) + break + } + } + + if err := Validate.Var(&filters, "required,gt=0,dive,required"); err != nil { + handlingErrors(ctx, err) + logs.LogInfo.Infof("ValidateMessageRequestMiddleware-失败-查询过滤语法 %s", messageToken.AuthId) + return + } + ctx.Set("messageFilters", &filters) + ctx.Next() + } + + logs.LogInfo.Infof("ValidateMessageRequestMiddleware-成功 %s", messageToken.AuthId) + } +} + +type MessageCreateUpdateRequest struct { + Title string `description:"标题" json:"title" validate:"required" example:"标题"` + Content string `description:"简单的内容" json:"content" validate:"required" example:"简单的内容"` + Category string `description:"消息类型" json:"category" validate:"required" example:"important"` + BigContent string `description:"复杂消息" json:"bigContent" validate:"required" example:"复杂的内容"` + IntroducerIds []string `description:"发给谁" json:"introducerIds" validate:"required,gt=0,dive,required" example:"发给谁"` +} + +// ValidateMessageCreateUpdateRequestMiddleware 用于验证创建或更新消息请求参数的中间件 +func ValidateMessageCreateUpdateRequestMiddleware() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 从上下文中获取 token + token, _ := ctx.Get("token") + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + + if !validateStructAndSetContext( + ctx, + &MessageCreateUpdateRequest{}, + "messageCreateUpdate", + ) { + logs.LogInfo.Infof("ValidateMessageCreateUpdateRequestMiddleware-失败-参数错误 %s", messageToken.AuthId) + return + } + logs.LogInfo.Infof("ValidateMessageCreateUpdateRequestMiddleware-成功 %s", messageToken.AuthId) + } +} + +type MessageStatusRequest struct { + Id string `json:"id,omitempty" validate:"required,len=32" example:"1"` + Status uint8 `json:"status,omitempty" validate:"required,max=2,min=0" example:"1"` +} + +// ValidateMessageStatusRequestMiddleware 用于验证消息状态请求参数的中间件 +func ValidateMessageStatusRequestMiddleware() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 从上下文中获取 token + token, _ := ctx.Get("token") + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + + if !validateSliceAndSetContext( + ctx, + &[]MessageStatusRequest{}, + "messageStatus", + ) { + logs.LogInfo.Infof("ValidateMessageStatusRequestMiddleware-失败-参数错误 %s", messageToken.AuthId) + return + } + logs.LogInfo.Infof("ValidateMessageStatusRequestMiddleware-成功 %s", messageToken.AuthId) + } +} + +type MessageDeleteRequest struct { + MessageId string `description:"消息id" json:"messageId" validate:"required,len=32" example:"id"` + Delete bool `description:"如果为true表示删除数据否则软删除" json:"delete" example:"false"` +} + +// ValidateMessageDeleteRequestMiddleware 用于验证删除消息请求参数的中间件 +func ValidateMessageDeleteRequestMiddleware() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 从上下文中获取 token + token, _ := ctx.Get("token") + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + + if !validateSliceAndSetContext( + ctx, + &[]MessageDeleteRequest{}, + "messageDelete", + ) { + logs.LogInfo.Infof("ValidateMessageDeleteRequestMiddleware-失败-参数错误 %s", messageToken.AuthId) + return + } + logs.LogInfo.Infof("ValidateMessageDeleteRequestMiddleware-成功 %s", messageToken.AuthId) + } +} + +// ValidateMessageIdRequestMiddleware 用于验证消息ID请求参数的中间件 +func ValidateMessageIdRequestMiddleware() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 从上下文中获取 token + token, _ := ctx.Get("token") + // 将 token 转换为 MessageToken 类型 + messageToken := token.(model.MessageToken) + + err := Validate.Var(ctx.Param("id"), "required,len=32") + if err != nil { + handlingErrors(ctx, err) + logs.LogInfo.Infof("ValidateMessageIdRequestMiddleware-失败-参数错误 %s", messageToken.AuthId) + return + } + + ctx.Next() + logs.LogInfo.Infof("ValidateMessageIdRequestMiddleware-成功 %s", messageToken.AuthId) + } +} diff --git a/app/response/base_response.go b/app/response/base_response.go new file mode 100644 index 0000000..61ee200 --- /dev/null +++ b/app/response/base_response.go @@ -0,0 +1,28 @@ +package response + +import "github.com/gin-gonic/gin" + +// NewError 是一个示例函数,用于在 Gin 上下文中返回错误响应。 +// +// 它接收一个 Gin 上下文对象、状态码和错误信息作为参数。 +// +// 创建一个 HTTPError 对象,将状态码和错误信息填充到该对象中。 +// +// 最后,使用 JSON 方法将 HTTPError 对象以指定的状态码作为响应返回给客户端。 +func NewError(ctx *gin.Context, status int, message string) { + er := HTTPError{ + Code: status, + Message: message, + } + ctx.JSON(status, er) +} + +// HTTPError 是一个示例结构体,表示 HTTP 错误的信息。 +// +// 它有两个字段:Code(状态码)和 Message(错误消息)。 +// +// 这些字段将在响应中以 JSON 格式返回给客户端。 +type HTTPError struct { + Code int `json:"code" example:"400"` + Message string `json:"message" example:"status bad request"` +} diff --git a/app/response/message_response.go b/app/response/message_response.go new file mode 100644 index 0000000..faef869 --- /dev/null +++ b/app/response/message_response.go @@ -0,0 +1,44 @@ +package response + +import ( + "message/app/model" + "time" +) + +type Message struct { + MessageId string `json:"message_id" example:"7e55cb38290f49ee2b0e9cfd2adf13e4"` + SenderIds model.StringArray `json:"sender_ids" example:"2f14ec370621a8be08c8f0ece459e7e0,22798c5dcd6e5b66c8660c447010d49d,..."` + Title string `json:"title" example:"标题"` + Content string `json:"content" example:"简单的内容"` + Category string `json:"category" example:"important"` + BigContent string `json:"big_content" example:"复杂的内容"` + IntroducerIds model.StringArray `json:"introducer_ids" example:"fc64c1a807c2e69655f68d31e5caa35d,70c021d35ce60436c115b20b5cf583d0,..."` + Status uint8 `json:"status" example:"0"` + CreatedAt time.Time `json:"created_at" example:"2024-02-15T05:49:57Z"` + UpdatedAt time.Time `json:"updated_at" example:"2024-02-15T05:49:57Z"` +} + +// MessageStatusResponse 用于封装消息状态更新操作的响应数据 +type MessageStatusResponse struct { + // Id 表示消息的唯一标识符。 + Id string `json:"id"` + + // Status 表示消息的当前状态。 + Status uint8 `json:"status"` + + // Result 表示消息状态更新操作的结果。 + // true 表示更新成功,false 表示更新失败。 + Result bool `json:"result"` +} + +// MessageDeleteResponse 表示消息删除操作的响应数据结构 +type MessageDeleteResponse struct { + // Id 表示消息的唯一标识符。 + Id string `json:"id"` + + // Delete 表示消息是否永久删除。 + Delete bool `json:"delete"` + + // Status 表示消息删除操作的状态,用于指示操作是否成功 + Status bool `json:"status"` +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..57774e3 --- /dev/null +++ b/config/config.go @@ -0,0 +1,63 @@ +package config + +import ( + "fmt" + "github.com/spf13/viper" + "os" +) + +type ServiceConfig struct { + App struct { + Title string `yaml:"title"` + Debug bool `yaml:"debug"` + Language string `yaml:"language"` + Store struct { + Path string `yaml:"path"` + Prefix string `yaml:"prefix"` + } `yaml:"store"` + Env string `yaml:"env"` + Log struct { + Info string `yaml:"info"` + Error string `yaml:"error"` + Access string `yaml:"access"` + } `yaml:"log"` + } `yaml:"app"` + Database struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + User string `yaml:"user"` + Pwd string `yaml:"pwd"` + Name string `yaml:"name"` + MaxIdleCon int `yaml:"max_idle_con"` + MaxOpenCon int `yaml:"max_open_con"` + Params struct { + Character string `yaml:"character"` + } `yaml:"params"` + } `yaml:"database"` + API struct { + Test bool `yaml:"test"` + MaxLimit int `yaml:"maxLimit"` + } `yaml:"api"` +} + +var AppConfig ServiceConfig + +func InitConfig() { + workDir, _ := os.Getwd() + // 设置配置文件的名字 + viper.SetConfigName("config") + // 设置配置文件的类型 + viper.SetConfigType("yaml") + // 添加配置文件的路径,指定 config 目录下寻找 + viper.AddConfigPath(workDir + "/config") + // 寻找配置文件并读取 + err := viper.ReadInConfig() + if err != nil { + panic(fmt.Errorf("fatal error config file: %w", err)) + } + + err = viper.Unmarshal(&AppConfig) + if err != nil { + panic(fmt.Errorf("fatal error parset file: %w", err)) + } +} diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..a48e3cc --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,38 @@ +app: + # 网站标题 + title: Message + # 是否为调试模式 + debug: true + # 网站语言 + language: zh + # 文件存储设置,设置上传文件的存储路径以及路由前缀 + store: + path: ./uploads + prefix: uploads + + # 开发环境:本地 EnvLocal / 测试 EnvTest / 生产 EnvProd + env: local + + # 日志本地存储路径 + log: + info: logs/info.log + error: logs/error.log + access: logs/access.log + +database: + host: 127.0.0.1 + port: 3306 + user: message + pwd: message + name: message + max_idle_con: 5 + max_open_con: 10 + # params为驱动需要的额外的传参 + params: + character: utf8mb4 + +api: + # 是否开启SwaggerApi + test: true + # 返回最多数量 + maxLimit: 15 \ No newline at end of file diff --git a/database/migrations.go b/database/migrations.go new file mode 100755 index 0000000..df0941e --- /dev/null +++ b/database/migrations.go @@ -0,0 +1,29 @@ +package database + +import ( + "fmt" + "message/app/model" + "message/config" + "message/logs" +) + +// InitMigration 用于初始化数据库迁移 +func InitMigration() { + // 从配置中获取字符集设置 + charset := config.AppConfig.Database.Params.Character + + // 构建数据库配置字符串 + dbConfig := fmt.Sprintf("charset=%s", charset) + + // 设置表选项为指定的数据库配置,并自动迁移指定的数据模型 + err := DB.Set("gorm:table_options", dbConfig).AutoMigrate( + // 迁移消息模型 + &model.Message{}, + // 迁移消息令牌模型 + &model.MessageToken{}, + ) + if err != nil { + // 输出迁移错误信息 + logs.LogError.Errorf("InitMigration %s", err) + } +} diff --git a/database/mysql.go b/database/mysql.go new file mode 100644 index 0000000..6c46b97 --- /dev/null +++ b/database/mysql.go @@ -0,0 +1,106 @@ +package database + +import ( + "github.com/gin-gonic/gin" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + "message/config" + "message/logs" + "strconv" + "strings" + "time" +) + +// DB 全局变量 DB 用于存储数据库连接实例 +var DB *gorm.DB + +// 进行连接重试 +var maxRetries = 5 +var curRetries = 1 + +// InitMySQL 用于初始化 MySQL 数据库连接 +func InitMySQL() { + // 从配置中获取数据库连接所需的信息 + username := config.AppConfig.Database.User + password := config.AppConfig.Database.Pwd + host := config.AppConfig.Database.Host + port := config.AppConfig.Database.Port + database := config.AppConfig.Database.Name + charset := config.AppConfig.Database.Params.Character + + // 构建 DSN 字符串 + dsn := strings.Join([]string{ + username, + ":", + password, + "@tcp(", + host, + ":", + strconv.Itoa(port), + ")/", + database, + "?charset" + charset + "&parseTime=True", + }, "") + + // 进行数据库连接 + err := databaseConnect(dsn) + if err != nil { + logs.LogError.Errorf( + "InitMySQL-数据库连接失败! %s 3秒后尝试连接。正在尝试%d次 剩余%d次。", + err, + curRetries, + maxRetries-curRetries, + ) + curRetries = maxRetries - curRetries + if maxRetries-curRetries > 0 { + time.Sleep(time.Second * 3) + InitMySQL() + } + } +} + +// databaseConnect 用于实际连接数据库 +func databaseConnect(dsn string) error { + // 根据 Gin 的模式设置 ORM 日志级别 + var ormLogger logger.Interface + if gin.Mode() == "debug" { + ormLogger = logger.Default.LogMode(logger.Info) + } else { + ormLogger = logger.Default + } + + // 使用 GORM 进行数据库连接 + db, err := gorm.Open(mysql.New(mysql.Config{ + DSN: dsn, + DefaultStringSize: 256, + DisableDatetimePrecision: true, + DontSupportRenameIndex: true, + DontSupportRenameColumn: true, + SkipInitializeWithVersion: false, + }), &gorm.Config{ + Logger: ormLogger, + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, + }, + }) + if err != nil { + return err + } + + // 设置数据库连接池参数 + sqlDB, _ := db.DB() + sqlDB.SetMaxOpenConns(20) + sqlDB.SetMaxIdleConns(100) + sqlDB.SetConnMaxLifetime(time.Second * 10) + sqlDB.SetConnMaxIdleTime(time.Second * 15) + + // 将数据库连接赋值给全局变量 DB + DB = db + + // 初始化数据库迁移 + InitMigration() + + return err +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fe5f7d8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3' +services: + app: + build: + context: . + networks: + - default + links: + - mysql + ports: + - "1204:1204" + depends_on: + - mysql + mysql: + image: mysql:8.0 + command: --default-authentication-plugin=mysql_native_password + networks: + - default + environment: + MYSQL_RANDOM_ROOT_PASSWORD: 'yes' + MYSQL_DATABASE: 'message' + MYSQL_USER: 'message' + MYSQL_PASSWORD: 'message' + volumes: + - mysql-data:/var/lib/mysql + ports: + - "3306:3306" +volumes: + mysql-data: +networks: + default: \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..04cdbb2 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,586 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "https://github.com/webb-l", + "contact": { + "name": "Webb", + "url": "https://github.com/webb-l", + "email": "822028533@qq.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/message": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "根据用户凭证查询消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "查询消息", + "parameters": [ + { + "type": "string", + "description": "过滤语句(title = 标题,status = 0|1|2,...)", + "name": "filter", + "in": "query" + }, + { + "type": "string", + "description": "排序列(created_at|updated_at|sender_ids|title|content|category|big_content|introducer_ids|status)", + "name": "sortColumn", + "in": "query" + }, + { + "type": "string", + "description": "排序类型(asc/desc)", + "name": "sortType", + "in": "query" + }, + { + "type": "integer", + "description": "查询第几页数据", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "消息信息", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/response.Message" + } + } + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "502": { + "description": "系统异常", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + } + } + }, + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "创建消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "创建消息", + "parameters": [ + { + "description": "创建的数据", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.MessageCreateUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "$ref": "#/definitions/response.Message" + } + }, + "202": { + "description": "创建失败", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "502": { + "description": "系统异常", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + } + } + }, + "delete": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "根据数组的数据删除消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "删除消息", + "parameters": [ + { + "description": "删除的消息", + "name": "_", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/request.MessageDeleteRequest" + } + } + } + ], + "responses": { + "200": { + "description": "更新后返回的数据", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.MessageDeleteResponse" + } + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "404": { + "description": "找不到兑换码", + "schema": { + "type": "string" + } + }, + "502": { + "description": "系统异常", + "schema": { + "type": "string" + } + } + } + } + }, + "/message/status": { + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "根据数组的数据更新消息状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "更新状态", + "parameters": [ + { + "description": "消息状态", + "name": "_", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/request.MessageStatusRequest" + } + } + } + ], + "responses": { + "200": { + "description": "更新后返回的数据", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.MessageStatusResponse" + } + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "502": { + "description": "系统异常", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + } + } + } + }, + "/message/{id}": { + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "根据消息id更新消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "更新消息", + "parameters": [ + { + "type": "string", + "description": "消息id", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新消息", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.MessageCreateUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/response.Message" + } + }, + "202": { + "description": "更新失败", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "404": { + "description": "找不到数据", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "502": { + "description": "系统异常", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + } + } + } + } + }, + "definitions": { + "request.MessageCreateUpdateRequest": { + "type": "object", + "required": [ + "bigContent", + "category", + "content", + "introducerIds", + "title" + ], + "properties": { + "bigContent": { + "type": "string", + "example": "复杂的内容" + }, + "category": { + "type": "string", + "example": "important" + }, + "content": { + "type": "string", + "example": "简单的内容" + }, + "introducerIds": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "发给谁" + ] + }, + "title": { + "type": "string", + "example": "标题" + } + } + }, + "request.MessageDeleteRequest": { + "type": "object", + "required": [ + "messageId" + ], + "properties": { + "delete": { + "type": "boolean", + "example": false + }, + "messageId": { + "type": "string", + "example": "id" + } + } + }, + "request.MessageStatusRequest": { + "type": "object", + "required": [ + "id", + "status" + ], + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "status": { + "type": "integer", + "maximum": 2, + "minimum": 0, + "example": 1 + } + } + }, + "request.ValidationError": { + "type": "object", + "properties": { + "field": { + "description": "字段名", + "type": "string" + }, + "message": { + "description": "错误消息", + "type": "string" + }, + "param": { + "description": "参数", + "type": "string" + }, + "type": { + "description": "字段类型", + "type": "string" + }, + "value": { + "description": "字段数值" + } + } + }, + "response.HTTPError": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 400 + }, + "message": { + "type": "string", + "example": "status bad request" + } + } + }, + "response.Message": { + "type": "object", + "properties": { + "big_content": { + "type": "string" + }, + "category": { + "type": "string" + }, + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "introducer_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "message_id": { + "type": "string" + }, + "sender_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "response.MessageDeleteResponse": { + "type": "object", + "properties": { + "delete": { + "description": "Delete 表示消息是否永久删除。", + "type": "boolean" + }, + "id": { + "description": "Id 表示消息的唯一标识符。", + "type": "string" + }, + "status": { + "description": "Status 表示消息删除操作的状态,用于指示操作是否成功", + "type": "boolean" + } + } + }, + "response.MessageStatusResponse": { + "type": "object", + "properties": { + "id": { + "description": "Id 表示消息的唯一标识符。", + "type": "string" + }, + "result": { + "description": "Result 表示消息状态更新操作的结果。\ntrue 表示更新成功,false 表示更新失败。", + "type": "boolean" + }, + "status": { + "description": "Status 表示消息的当前状态。", + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:1204", + BasePath: "/", + Schemes: []string{}, + Title: "消息系统 API", + Description: "简单又好用的消息服务。快来给你\"项目\"添加消息功能。", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..7b1c9a1 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,562 @@ +{ + "swagger": "2.0", + "info": { + "description": "简单又好用的消息服务。快来给你\"项目\"添加消息功能。", + "title": "消息系统 API", + "termsOfService": "https://github.com/webb-l", + "contact": { + "name": "Webb", + "url": "https://github.com/webb-l", + "email": "822028533@qq.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:1204", + "basePath": "/", + "paths": { + "/message": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "根据用户凭证查询消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "查询消息", + "parameters": [ + { + "type": "string", + "description": "过滤语句(title = 标题,status = 0|1|2,...)", + "name": "filter", + "in": "query" + }, + { + "type": "string", + "description": "排序列(created_at|updated_at|sender_ids|title|content|category|big_content|introducer_ids|status)", + "name": "sortColumn", + "in": "query" + }, + { + "type": "string", + "description": "排序类型(asc/desc)", + "name": "sortType", + "in": "query" + }, + { + "type": "integer", + "description": "查询第几页数据", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "消息信息", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/response.Message" + } + } + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "502": { + "description": "系统异常", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + } + } + }, + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "创建消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "创建消息", + "parameters": [ + { + "description": "创建的数据", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.MessageCreateUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "$ref": "#/definitions/response.Message" + } + }, + "202": { + "description": "创建失败", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "502": { + "description": "系统异常", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + } + } + }, + "delete": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "根据数组的数据删除消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "删除消息", + "parameters": [ + { + "description": "删除的消息", + "name": "_", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/request.MessageDeleteRequest" + } + } + } + ], + "responses": { + "200": { + "description": "更新后返回的数据", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.MessageDeleteResponse" + } + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "404": { + "description": "找不到兑换码", + "schema": { + "type": "string" + } + }, + "502": { + "description": "系统异常", + "schema": { + "type": "string" + } + } + } + } + }, + "/message/status": { + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "根据数组的数据更新消息状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "更新状态", + "parameters": [ + { + "description": "消息状态", + "name": "_", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/request.MessageStatusRequest" + } + } + } + ], + "responses": { + "200": { + "description": "更新后返回的数据", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/response.MessageStatusResponse" + } + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "502": { + "description": "系统异常", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + } + } + } + }, + "/message/{id}": { + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "根据消息id更新消息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "message" + ], + "summary": "更新消息", + "parameters": [ + { + "type": "string", + "description": "消息id", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新消息", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.MessageCreateUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/response.Message" + } + }, + "202": { + "description": "更新失败", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/request.ValidationError" + } + }, + "401": { + "description": "凭证错误", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "404": { + "description": "找不到数据", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + }, + "502": { + "description": "系统异常", + "schema": { + "$ref": "#/definitions/response.HTTPError" + } + } + } + } + } + }, + "definitions": { + "request.MessageCreateUpdateRequest": { + "type": "object", + "required": [ + "bigContent", + "category", + "content", + "introducerIds", + "title" + ], + "properties": { + "bigContent": { + "type": "string", + "example": "复杂的内容" + }, + "category": { + "type": "string", + "example": "important" + }, + "content": { + "type": "string", + "example": "简单的内容" + }, + "introducerIds": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "发给谁" + ] + }, + "title": { + "type": "string", + "example": "标题" + } + } + }, + "request.MessageDeleteRequest": { + "type": "object", + "required": [ + "messageId" + ], + "properties": { + "delete": { + "type": "boolean", + "example": false + }, + "messageId": { + "type": "string", + "example": "id" + } + } + }, + "request.MessageStatusRequest": { + "type": "object", + "required": [ + "id", + "status" + ], + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "status": { + "type": "integer", + "maximum": 2, + "minimum": 0, + "example": 1 + } + } + }, + "request.ValidationError": { + "type": "object", + "properties": { + "field": { + "description": "字段名", + "type": "string" + }, + "message": { + "description": "错误消息", + "type": "string" + }, + "param": { + "description": "参数", + "type": "string" + }, + "type": { + "description": "字段类型", + "type": "string" + }, + "value": { + "description": "字段数值" + } + } + }, + "response.HTTPError": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 400 + }, + "message": { + "type": "string", + "example": "status bad request" + } + } + }, + "response.Message": { + "type": "object", + "properties": { + "big_content": { + "type": "string" + }, + "category": { + "type": "string" + }, + "content": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "introducer_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "message_id": { + "type": "string" + }, + "sender_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "response.MessageDeleteResponse": { + "type": "object", + "properties": { + "delete": { + "description": "Delete 表示消息是否永久删除。", + "type": "boolean" + }, + "id": { + "description": "Id 表示消息的唯一标识符。", + "type": "string" + }, + "status": { + "description": "Status 表示消息删除操作的状态,用于指示操作是否成功", + "type": "boolean" + } + } + }, + "response.MessageStatusResponse": { + "type": "object", + "properties": { + "id": { + "description": "Id 表示消息的唯一标识符。", + "type": "string" + }, + "result": { + "description": "Result 表示消息状态更新操作的结果。\ntrue 表示更新成功,false 表示更新失败。", + "type": "boolean" + }, + "status": { + "description": "Status 表示消息的当前状态。", + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..8022f91 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,375 @@ +basePath: / +definitions: + request.MessageCreateUpdateRequest: + properties: + bigContent: + example: 复杂的内容 + type: string + category: + example: important + type: string + content: + example: 简单的内容 + type: string + introducerIds: + example: + - 发给谁 + items: + type: string + type: array + title: + example: 标题 + type: string + required: + - bigContent + - category + - content + - introducerIds + - title + type: object + request.MessageDeleteRequest: + properties: + delete: + example: false + type: boolean + messageId: + example: id + type: string + required: + - messageId + type: object + request.MessageStatusRequest: + properties: + id: + example: "1" + type: string + status: + example: 1 + maximum: 2 + minimum: 0 + type: integer + required: + - id + - status + type: object + request.ValidationError: + properties: + field: + description: 字段名 + type: string + message: + description: 错误消息 + type: string + param: + description: 参数 + type: string + type: + description: 字段类型 + type: string + value: + description: 字段数值 + type: object + response.HTTPError: + properties: + code: + example: 400 + type: integer + message: + example: status bad request + type: string + type: object + response.Message: + properties: + big_content: + type: string + category: + type: string + content: + type: string + created_at: + type: string + introducer_ids: + items: + type: string + type: array + message_id: + type: string + sender_ids: + items: + type: string + type: array + status: + type: integer + title: + type: string + updated_at: + type: string + type: object + response.MessageDeleteResponse: + properties: + delete: + description: Delete 表示消息是否永久删除。 + type: boolean + id: + description: Id 表示消息的唯一标识符。 + type: string + status: + description: Status 表示消息删除操作的状态,用于指示操作是否成功 + type: boolean + type: object + response.MessageStatusResponse: + properties: + id: + description: Id 表示消息的唯一标识符。 + type: string + result: + description: |- + Result 表示消息状态更新操作的结果。 + true 表示更新成功,false 表示更新失败。 + type: boolean + status: + description: Status 表示消息的当前状态。 + type: integer + type: object +externalDocs: + description: OpenAPI + url: https://swagger.io/resources/open-api/ +host: localhost:1204 +info: + contact: + email: 822028533@qq.com + name: Webb + url: https://github.com/webb-l + description: 简单又好用的消息服务。快来给你"项目"添加消息功能。 + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: https://github.com/webb-l + title: 消息系统 API + version: "1.0" +paths: + /message: + delete: + consumes: + - application/json + description: 根据数组的数据删除消息 + parameters: + - description: 删除的消息 + in: body + name: _ + required: true + schema: + items: + $ref: '#/definitions/request.MessageDeleteRequest' + type: array + produces: + - application/json + responses: + "200": + description: 更新后返回的数据 + schema: + items: + $ref: '#/definitions/response.MessageDeleteResponse' + type: array + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/request.ValidationError' + "401": + description: 凭证错误 + schema: + $ref: '#/definitions/response.HTTPError' + "404": + description: 找不到兑换码 + schema: + type: string + "502": + description: 系统异常 + schema: + type: string + security: + - BasicAuth: [] + summary: 删除消息 + tags: + - message + get: + consumes: + - application/json + description: 根据用户凭证查询消息 + parameters: + - description: 过滤语句(title = 标题,status = 0|1|2,...) + in: query + name: filter + type: string + - description: 排序列(created_at|updated_at|sender_ids|title|content|category|big_content|introducer_ids|status) + in: query + name: sortColumn + type: string + - description: 排序类型(asc/desc) + in: query + name: sortType + type: string + - description: 查询第几页数据 + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: 消息信息 + schema: + items: + items: + $ref: '#/definitions/response.Message' + type: array + type: array + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/request.ValidationError' + "401": + description: 凭证错误 + schema: + $ref: '#/definitions/response.HTTPError' + "502": + description: 系统异常 + schema: + $ref: '#/definitions/response.HTTPError' + security: + - BasicAuth: [] + summary: 查询消息 + tags: + - message + post: + consumes: + - application/json + description: 创建消息 + parameters: + - description: 创建的数据 + in: body + name: _ + required: true + schema: + $ref: '#/definitions/request.MessageCreateUpdateRequest' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + $ref: '#/definitions/response.Message' + "202": + description: 创建失败 + schema: + $ref: '#/definitions/response.HTTPError' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/request.ValidationError' + "401": + description: 凭证错误 + schema: + $ref: '#/definitions/response.HTTPError' + "502": + description: 系统异常 + schema: + $ref: '#/definitions/response.HTTPError' + security: + - BasicAuth: [] + summary: 创建消息 + tags: + - message + /message/{id}: + put: + consumes: + - application/json + description: 根据消息id更新消息 + parameters: + - description: 消息id + in: path + name: id + required: true + type: string + - description: 更新消息 + in: body + name: _ + required: true + schema: + $ref: '#/definitions/request.MessageCreateUpdateRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + $ref: '#/definitions/response.Message' + "202": + description: 更新失败 + schema: + $ref: '#/definitions/response.HTTPError' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/request.ValidationError' + "401": + description: 凭证错误 + schema: + $ref: '#/definitions/response.HTTPError' + "404": + description: 找不到数据 + schema: + $ref: '#/definitions/response.HTTPError' + "502": + description: 系统异常 + schema: + $ref: '#/definitions/response.HTTPError' + security: + - BasicAuth: [] + summary: 更新消息 + tags: + - message + /message/status: + put: + consumes: + - application/json + description: 根据数组的数据更新消息状态 + parameters: + - description: 消息状态 + in: body + name: _ + required: true + schema: + items: + $ref: '#/definitions/request.MessageStatusRequest' + type: array + produces: + - application/json + responses: + "200": + description: 更新后返回的数据 + schema: + items: + $ref: '#/definitions/response.MessageStatusResponse' + type: array + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/request.ValidationError' + "401": + description: 凭证错误 + schema: + $ref: '#/definitions/response.HTTPError' + "502": + description: 系统异常 + schema: + $ref: '#/definitions/response.HTTPError' + security: + - BasicAuth: [] + summary: 更新状态 + tags: + - message +securityDefinitions: + BasicAuth: + type: basic +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..84afe99 --- /dev/null +++ b/go.mod @@ -0,0 +1,70 @@ +module message + +go 1.21 + +require ( + github.com/gin-contrib/i18n v1.1.0 + github.com/gin-gonic/gin v1.9.1 + github.com/spf13/viper v1.18.2 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.2 + golang.org/x/text v0.14.0 + gorm.io/driver/mysql v1.5.2 + gorm.io/gorm v1.25.6 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/bytedance/sonic v1.10.2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/spec v0.20.14 // indirect + github.com/go-openapi/swag v0.22.9 // 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.17.0 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.6.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/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/leodido/go-urn v1.3.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // 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/nicksnyder/go-i18n/v2 v2.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // 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/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/tools v0.17.0 // indirect + google.golang.org/protobuf v1.32.0 // 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 new file mode 100644 index 0000000..3f4ad0f --- /dev/null +++ b/go.sum @@ -0,0 +1,209 @@ +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= +github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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.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/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/i18n v1.1.0 h1:kmNinScsMKAsgGg0lvxCm6ecifzmwvxZIu0ImLHVfEA= +github.com/gin-contrib/i18n v1.1.0/go.mod h1:n0OOIxqHLXT445Vv8CIybqwKVGUekjHP/vMl1rI62GI= +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.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= +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.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= +github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +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= +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.3.0 h1:jX8FDLfW4ThVXctBNZ+3cIWnCSnrACDV73r76dy0aQQ= +github.com/leodido/go-urn v1.3.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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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/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= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA= +github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +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/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/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.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.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.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= +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= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= +golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A= +gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +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/logs/log.go b/logs/log.go new file mode 100644 index 0000000..0293d6a --- /dev/null +++ b/logs/log.go @@ -0,0 +1,63 @@ +package logs + +import ( + "fmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "message/config" +) + +var LogInfo, LogError, LogAccess *zap.SugaredLogger +var logInfo, logError, logAccess *zap.Logger + +func InitLog() { + cfg := zap.Config{ + Level: zap.NewAtomicLevelAt(zap.DebugLevel), + Development: false, + Encoding: "console", + EncoderConfig: zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + MessageKey: "message", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.RFC3339TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stdout"}, + InitialFields: map[string]interface{}{ + "version": "v1.0.0", + }, + } + + // 信息日志 + logInfo = createLog(cfg, config.AppConfig.App.Log.Info) + LogInfo = logInfo.Sugar() + + // 错误日志 + logError = createLog(cfg, config.AppConfig.App.Log.Error) + LogError = logError.Sugar() + + // 访问日志 + logAccess = createLog(cfg, config.AppConfig.App.Log.Access) + LogAccess = logAccess.Sugar() +} + +func createLog(cfg zap.Config, outputPath string) *zap.Logger { + cfg.OutputPaths = append(cfg.OutputPaths, outputPath) + logger, err := cfg.Build() + if err != nil { + panic(fmt.Sprintln("日志功能出现错误!", "\n", err)) + } + return logger +} + +func Sync() { + logInfo.Sync() + logError.Sync() + logAccess.Sync() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e7373b5 --- /dev/null +++ b/main.go @@ -0,0 +1,64 @@ +package main + +import ( + lang "github.com/gin-contrib/i18n" + "github.com/gin-gonic/gin" + "golang.org/x/text/language" + "gopkg.in/yaml.v3" + "message/config" + "message/database" + "message/logs" + "message/router" + "message/utils" +) + +// @title 消息系统 API +// @version 1.0 +// @description 简单又好用的消息服务。快来给你"项目"添加消息功能。 +// @termsOfService https://github.com/webb-l + +// @contact.name Webb +// @contact.url https://github.com/webb-l +// @contact.email 822028533@qq.com + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:1204 +// @BasePath / + +// @securityDefinitions.basic BasicAuth + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ + +func main() { + // 初始化Gin引擎 + r := gin.Default() + + // 初始化配置 + config.InitConfig() + + // 初始日志 + logs.InitLog() + defer logs.Sync() + r.Use(utils.AccessLogger()) + + // 国际化中间件 + r.Use(lang.Localize(lang.WithBundle(&lang.BundleCfg{ + DefaultLanguage: language.Chinese, + FormatBundleFile: "yaml", + AcceptLanguage: []language.Tag{language.Chinese, language.English}, + RootPath: "resources/lang", + UnmarshalFunc: yaml.Unmarshal, + }))) + + // 初始化路由 + router.InitRouter(r) + + // 连接MySQL数据库 + database.InitMySQL() + + // 启动Gin引擎 + r.Run(":1204") +} diff --git a/resources/lang/en.yaml b/resources/lang/en.yaml new file mode 100644 index 0000000..cd72db0 --- /dev/null +++ b/resources/lang/en.yaml @@ -0,0 +1,2 @@ +welcome: hello +welcomeWithName: hello {{ .name }} \ No newline at end of file diff --git a/resources/lang/zh.yaml b/resources/lang/zh.yaml new file mode 100644 index 0000000..5be0be4 --- /dev/null +++ b/resources/lang/zh.yaml @@ -0,0 +1,5 @@ +unauthorized: 凭证错误 +notFound: 找不到数据 +createMessageFail: 创建消息失败 +updateMessageFail: 更新消息失败 +badGateway: 服务器出现错误。\n请联系管理员查看错误日期。 \ No newline at end of file diff --git a/router/index_rotuer.go b/router/index_rotuer.go new file mode 100644 index 0000000..a5ce806 --- /dev/null +++ b/router/index_rotuer.go @@ -0,0 +1,28 @@ +package router + +import ( + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + "message/app/middleware" + "message/config" + _ "message/docs" + "net/http" +) + +// InitRouter 用于初始化路由配置 +func InitRouter(router *gin.Engine) { + // 添加一个简单的路由示例 + router.GET("/ping", func(c *gin.Context) { + c.String(http.StatusOK, "OK") + }) + + // 创建一个名为 message 的路由组,并应用 AuthMiddleware 中间件 + messageGroup := router.Group("message", middleware.AuthMiddleware()) + InitMessageRouter(messageGroup) + + // 根据配置文件中的设置决定是否允许访问 SwaggerApi + if config.AppConfig.API.Test { + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + } +} diff --git a/router/message_router.go b/router/message_router.go new file mode 100644 index 0000000..be13e85 --- /dev/null +++ b/router/message_router.go @@ -0,0 +1,39 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "message/app/controller" + "message/app/request" +) + +// InitMessageRouter 用于初始化消息相关的路由 +func InitMessageRouter(router *gin.RouterGroup) { + // 查询消息 + router.GET( + "", + request.ValidateMessageRequestMiddleware(), + controller.MessageIndex, + ) + // 新增消息 + router.POST("", + request.ValidateMessageCreateUpdateRequestMiddleware(), + controller.MessageCreate, + ) + // 更新消息 + router.PUT(":id", + request.ValidateMessageIdRequestMiddleware(), + request.ValidateMessageCreateUpdateRequestMiddleware(), + controller.MessageUpdate, + ) + // 更新消息状态 + router.PUT( + "status", + request.ValidateMessageStatusRequestMiddleware(), + controller.MessageUpdateStatus, + ) + // 删除通知 + router.DELETE("", + request.ValidateMessageDeleteRequestMiddleware(), + controller.MessageDelete, + ) +} diff --git a/utils/log.go b/utils/log.go new file mode 100644 index 0000000..ca4129d --- /dev/null +++ b/utils/log.go @@ -0,0 +1,32 @@ +package utils + +import ( + "github.com/gin-gonic/gin" + "message/logs" + "time" +) + +// AccessLogger 是自定义的日志记录器中间件 +func AccessLogger() gin.HandlerFunc { + return func(ctx *gin.Context) { + // 记录请求开始时间 + startTime := time.Now() + + // 调用后续的中间件和处理函数 + ctx.Next() + + // 记录请求结束时间 + endTime := time.Now() + // 计算请求耗时 + latency := endTime.Sub(startTime) + + // 将日志信息输出到标准输出流 + logs.LogAccess.Infof( + "%s %s %s %s", + ctx.Request.Method, + ctx.Request.URL.Path, + latency.String(), + ctx.ClientIP(), + ) + } +} diff --git a/utils/message.go b/utils/message.go new file mode 100644 index 0000000..703f99e --- /dev/null +++ b/utils/message.go @@ -0,0 +1,21 @@ +package utils + +import ( + "crypto/md5" + "fmt" + "github.com/google/uuid" +) + +// BuildMessageId 用于生成消息ID +func BuildMessageId() string { + // 生成一个新的UUID + u := uuid.New() + // 将UUID转换为字节切片 + uuidBytes := []byte(u.String()) + // 使用MD5算法加密UUID + md5Bytes := md5.Sum(uuidBytes) + // 将加密后的字节切片转换为16进制字符串 + md5String := fmt.Sprintf("%x", md5Bytes) + + return md5String +}