├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cmd └── jank_blog_server.go ├── configs ├── .air.toml ├── config.go ├── config.yml └── nginx.conf ├── docker-compose.yml ├── docs ├── Jank_blog.postman_collection.json ├── README.md ├── docs.go ├── jank_blog_architecture.drawio ├── standards.md ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── internal ├── banner │ ├── README.md │ └── banner.go ├── db │ ├── README.md │ ├── auto_migrate.go │ └── conn.go ├── error │ ├── README.md │ ├── biz_error.go │ └── err_code.go ├── global │ ├── README.md │ └── global.go ├── logger │ ├── README.md │ └── logger.go ├── middleware │ ├── README.md │ ├── auth │ │ ├── README.md │ │ └── jwt_authentication.go │ ├── cors │ │ ├── README.md │ │ └── cors.go │ ├── error │ │ ├── README.md │ │ └── error_handler.go │ ├── logger │ │ └── logger.go │ ├── middleware.go │ ├── recover │ │ ├── README.md │ │ └── recover.go │ ├── secure │ │ ├── README.md │ │ ├── csrf.go │ │ └── xss.go │ └── swagger │ │ ├── README.md │ │ └── swagger.go ├── model │ ├── README.md │ ├── account │ │ ├── README.md │ │ └── account.go │ ├── association │ │ ├── README.md │ │ └── post_category.go │ ├── base │ │ ├── READEME.md │ │ └── base.go │ ├── category │ │ ├── README.md │ │ └── category.go │ ├── comment │ │ ├── README.md │ │ └── comment.go │ ├── get_all_models.go │ └── post │ │ ├── README.md │ │ └── post.go ├── redis │ ├── README.md │ └── redis.go └── utils │ ├── README.md │ ├── db_transaction_utils.go │ ├── email_utils.go │ ├── img_verification_utils.go │ ├── jwt_utils.go │ ├── logger_utils.go │ ├── map_model_to_vo_utils.go │ ├── markdown_utils.go │ ├── snowflake_utils.go │ └── validator_utils.go ├── main.go └── pkg ├── router ├── README.md ├── router.go └── routes │ ├── account.go │ ├── category.go │ ├── comment.go │ ├── post.go │ ├── test.go │ └── verification.go ├── serve ├── controller │ ├── account │ │ ├── account.go │ │ └── dto │ │ │ ├── README.md │ │ │ ├── get_acc_request.go │ │ │ ├── login_acc_request.go │ │ │ ├── register_acc_request.go │ │ │ └── reset_acc_pwd_request.go │ ├── category │ │ ├── category.go │ │ └── dto │ │ │ ├── create_category_request.go │ │ │ ├── delete_category_request.go │ │ │ ├── get_category_request.go │ │ │ └── update_category_request.go │ ├── comment │ │ ├── comment.go │ │ └── dto │ │ │ ├── create_comment_request.go │ │ │ ├── delete_comment_request.go │ │ │ ├── get_comment_graph_request.go │ │ │ └── get_one_comment.go │ ├── post │ │ ├── dto │ │ │ ├── README.md │ │ │ ├── create_post_request.go │ │ │ ├── delete_post_request.go │ │ │ ├── get_post_request.go │ │ │ └── update_post_request.go │ │ └── post.go │ ├── test │ │ ├── README.md │ │ └── test.go │ └── verification │ │ └── verification.go ├── mapper │ ├── account.go │ ├── category.go │ ├── comment.go │ ├── post.go │ └── post_category.go └── service │ ├── account │ └── account.go │ ├── category │ └── category.go │ ├── comment │ └── comment.go │ └── post │ └── post.go └── vo ├── README.md ├── account ├── get_acc_vo.go ├── login_vo.go └── register_acc_vo.go ├── category └── categories_vo.go ├── comment └── comments_vo.go ├── post └── posts_vo.go ├── result.go └── verification └── img_verification_vo.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | docker/ 4 | *.md 5 | *.log 6 | tmp/ 7 | 8 | node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 忽略 .logs 目录 2 | .logs/ 3 | 4 | # 忽略 tmp 目录 5 | tmp/ 6 | 7 | # 忽略 .history 文件 8 | .history 9 | 10 | # 忽略 .vscode 和 .idea 11 | .vscode/ 12 | .idea/ 13 | 14 | # 忽略 ci.yml 文件 15 | ci.yml 16 | 17 | # 忽略配置文件 18 | .configs/conf.yaml 19 | 20 | # 忽略 docs 目录 21 | .docs/ 22 | 23 | # 忽略数据库文件 24 | /database/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 第一阶段:构建阶段 2 | FROM golang:1.23.0 AS builder 3 | 4 | # 设置环境变量 5 | ENV CGO_ENABLED=0 GOOS=linux GOPROXY=https://goproxy.cn,direct 6 | 7 | # 设置工作目录 8 | WORKDIR /jank 9 | 10 | # 复制 go.mod 和 go.sum,并下载依赖 11 | COPY go.mod go.sum ./ 12 | RUN go mod download && go mod tidy 13 | 14 | # 安装 swag 工具 15 | RUN go install github.com/swaggo/swag/cmd/swag@latest 16 | 17 | # 复制项目源码到容器中 18 | COPY . . 19 | 20 | # 构建 Go 应用 21 | RUN go build -o main . 22 | 23 | # 第二阶段:生产镜像 24 | FROM alpine:3.18 25 | 26 | # 安装基础依赖并设置时区 27 | RUN apk --no-cache add ca-certificates tzdata && \ 28 | cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 29 | echo "Asia/Shanghai" > /etc/timezone 30 | 31 | # 设置工作目录 32 | WORKDIR /app 33 | 34 | # 从构建阶段复制必要文件 35 | COPY --from=builder /jank/main . 36 | COPY --from=builder /go/bin/swag /usr/local/bin/swag 37 | COPY --from=builder /jank/pkg /app/pkg 38 | 39 | # 开放端口 9010 40 | EXPOSE 9010 41 | 42 | # 启动命令 43 | CMD ["./main"] 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Fender 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Jank 3 |

4 | 5 |

6 | Jank,一个轻量级的博客系统,基于 Go 语言和 Echo 框架开发,强调极简、低耦合和高扩展 7 |

8 | 9 |

10 | 11 | Stars 12 |   13 | 14 | Forks 15 |   16 | 17 | Contributors 18 |   19 | 20 | Issues 21 |   22 | 23 | Pull Requests 24 |   25 | 26 | License 27 | 28 |

29 | 30 | --- 31 | 32 | Jank 是一个轻量级的博客系统,基于 Go 语言和 Echo 框架开发,设计理念强调简约、低耦合和高扩展,旨在为用户提供优雅的博客体验。 33 | 34 | ## 快速链接 35 | 36 | 👉 [演示站](https://www.jank.org.cn) | [开发社区](https://github.com/Jank-Community) 37 | 38 | ## 界面预览 39 | 40 | ![首页](https://s2.loli.net/2025/04/07/l1tGYV4WkmoiIHv.png) 41 | ![文章列表](https://s2.loli.net/2025/04/07/xR62vhWKsmgw3Ht.png) 42 | ![文章详情1](https://s2.loli.net/2025/04/07/DbcJzryKmBNR7vQ.png) 43 | ![文章详情2](https://s2.loli.net/2025/04/07/iNpXyMdkjaDbn92.png) 44 | 45 | ## 技术栈 46 | 47 | - **Go 语言**:热门后端开发语言,适合构建高并发应用。 48 | - **Echo 框架**:高性能的 Web 框架,支持快速开发和灵活的路由管理。 49 | - **数据库**:开源的关系型数据库,支持 Postgres、MySQL 和 SQLite。 50 | - **Redis**:热门缓存解决方案,提供快速数据存取和持久化选项。 51 | - **JWT**:安全的用户身份验证机制,确保数据传输的完整性和安全性。 52 | - **Docker**:容器化部署工具,简化应用的打包和分发流程。 53 | - **前端**:react + ts + vite + shadcn/ui + tailwindcss。 54 | 55 | ## 功能模块 56 | 57 | - **账户模块**:实现 JWT 身份验证,支持用户登录、注册、注销、密码修改和个人信息更新。 58 | - **权限模块**:实现 RBAC(Role-Based Access Control)角色权限管理,支持用户-角色-权限的增删改查。 59 | - 基本功能已实现,考虑到用户使用的不友好性和复杂性,因此暂不推出此功能。 60 | - **文章模块**:提供文章的创建、查看、更新和删除功能。 61 | - **分类模块**:支持类目树及子类目树递归查询,单一类目查询,以及类目的创建、更新和删除。 62 | - **评论模块**:提供评论的创建、查看、删除和回复功能,支持评论树结构的展示。 63 | - **插件系统**:正在火热开发中,即将推出... 64 | - **其他功能**: 65 | - 提供 OpenAPI 接口文档 66 | - 集成 Air 实现热重载 67 | - 提供 Logrus 实现日志记录 68 | - 支持 CORS 跨域请求 69 | - 提供 CSRF 和 XSS 防护 70 | - 支持 Markdown 的服务端渲染 71 | - 集成图形验证码功能 72 | - 支持 QQ/Gmail/Outlook 等主流邮箱服务端发送能力 73 | - 支持 oss 对象存储(MinIO) 74 | - **其他模块正在开发中**,欢迎提供宝贵意见和建议! 75 | 76 | ## 开发指南 77 | 78 | ### 本地开发 79 | 80 | 1. **安装依赖** 81 | 82 | ```bash 83 | # 安装 swagger 工具 84 | go install github.com/swaggo/swag/cmd/swag@latest 85 | # 安装 air,需要 go 1.22 或更高版本 86 | go install github.com/air-verse/air@latest 87 | # 安装依赖包 88 | go mod tidy 89 | ``` 90 | 91 | 2. **配置数据库和邮箱** 92 | 93 | ```yaml 94 | APP: 95 | APP_NAME: "JANK_BLOG" 96 | APP_HOST: "127.0.0.1" # 如果使用 docker,则改为"0.0.0.0" 97 | APP_PORT: "9010" 98 | EMAIL: 99 | EMAIL_TYPE: "qq" # 支持的邮箱类型: qq, gmail, outlook 100 | FROM_EMAIL: "" # 发件人邮箱 101 | EMAIL_SMTP: "" # SMTP 授权码 102 | SWAGGER: 103 | SWAGGER_HOST: "127.0.0.1:9010" 104 | SWAGGER_ENABLED: true 105 | 106 | DATABASE: 107 | DB_DIALECT: "postgres" # 数据库类型: postgres, mysql, sqlite 108 | DB_NAME: "jank_db" 109 | DB_HOST: "127.0.0.1" # 如果使用 docker,则改为"postgres_db" 110 | DB_PORT: "5432" 111 | DB_USER: "fender" 112 | DB_PSW: "Lh20230623" 113 | DB_PATH: "./database" # SQLite 数据库文件路径 114 | ``` 115 | 116 | 3. **启动服务** 117 | 118 | ```bash 119 | # 方式一:直接运行 120 | go run main.go 121 | 122 | # 方式二:使用 Air 热重载(推荐) 123 | air -c ./configs/.air.toml 124 | ``` 125 | 126 | ### Docker 部署 127 | 128 | 1. **修改配置** 129 | 修改 `configs/config.yaml` 和 `docker-compose.yaml` 中的相关配置 130 | 131 | 2. **启动容器** 132 | 133 | ```bash 134 | docker-compose up -d 135 | ``` 136 | 137 | ## 开发路线图 138 | 139 | ![开发路线图](https://s2.loli.net/2025/03/09/qJrtOeFvD95PV4Y.png) 140 | 141 | ## 社区支持 142 | 143 | ### 官方社区 144 | 145 | 官方社区 146 | 147 | > 注:因社群成员较多,请自觉遵守规范。严禁讨论涉黄、赌、毒及政治敏感内容,禁止发布任何形式的不良广告。 148 | 149 | ### 贡献者 150 | 151 | 152 | 贡献者名单 153 | 154 | 155 | ### 特别鸣谢 156 | 157 |

158 | 159 | 160 | 161 | 162 | 163 | 164 |

165 | 166 | ## 联系方式 167 | 168 | - **QQ**: 927171598 169 | - **微信**: l927171598 170 | - **邮箱**: fenderisfine@outlook.com 171 | 172 | > 合作、推广和赞助可联系作者 173 | 174 | ## 许可证 175 | 176 | 本项目遵循 [MIT 协议](https://opensource.org/licenses/MIT) 177 | 178 | ## 项目趋势 179 | 180 | GitHub Stats 181 | -------------------------------------------------------------------------------- /cmd/jank_blog_server.go: -------------------------------------------------------------------------------- 1 | // Package cmd 提供应用程序的启动和运行入口 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/labstack/echo/v4" 11 | 12 | "jank.com/jank_blog/configs" 13 | "jank.com/jank_blog/internal/banner" 14 | "jank.com/jank_blog/internal/db" 15 | "jank.com/jank_blog/internal/logger" 16 | "jank.com/jank_blog/internal/middleware" 17 | "jank.com/jank_blog/internal/redis" 18 | "jank.com/jank_blog/pkg/router" 19 | ) 20 | 21 | // Start 启动服务 22 | func Start() { 23 | if err := configs.Init(configs.DefaultConfigPath); err != nil { 24 | log.Fatalf("配置初始化失败: %v", err) 25 | return 26 | } 27 | 28 | config, err := configs.LoadConfig() 29 | if err != nil { 30 | log.Fatalf("获取配置失败: %v", err) 31 | return 32 | } 33 | 34 | // 初始化 Logger 35 | logger.New() 36 | 37 | // 初始化 echo 实例 38 | app := echo.New() 39 | 40 | // 初始化 Banner 41 | banner.New(app) 42 | 43 | // 初始化中间件 44 | middleware.New(app) 45 | 46 | // 初始化数据库连接并自动迁移模型 47 | db.New(config) 48 | 49 | // 初始化 Redis 连接 50 | redis.New(config) 51 | 52 | // 注册路由 53 | router.New(app) 54 | 55 | // 启动服务 56 | app.Logger.Fatal(app.Start(fmt.Sprintf("%s:%s", config.AppConfig.AppHost, config.AppConfig.AppPort))) 57 | } 58 | -------------------------------------------------------------------------------- /configs/.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | bin = "./tmp/main.exe" 7 | cmd = "go build -o ./tmp/main.exe ." 8 | delay = 500 9 | exclude_dir = ["assets", "tmp", "vendor", "testdata", "docs", ".git", ".idea"] 10 | exclude_file = [] 11 | exclude_regex = ["_test.go"] 12 | exclude_unchanged = false 13 | follow_symlink = false 14 | full_bin = "" 15 | include_ext = ["go", "tpl", "tmpl", "html"] 16 | kill_delay = "0s" 17 | log = "build-errors.log" 18 | send_interrupt = false 19 | stop_on_error = true 20 | 21 | [color] 22 | app = "" 23 | build = "yellow" 24 | main = "magenta" 25 | runner = "green" 26 | watcher = "cyan" 27 | 28 | [log] 29 | main_only = false 30 | time = false 31 | 32 | [misc] 33 | clean_on_exit = false 34 | 35 | [proxy] 36 | app_port = 0 37 | enabled = false 38 | proxy_port = 0 39 | 40 | [screen] 41 | clear_on_rebuild = false 42 | keep_scroll = true 43 | -------------------------------------------------------------------------------- /configs/config.go: -------------------------------------------------------------------------------- 1 | // Package configs 提供应用程序配置加载和更新功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package configs 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "reflect" 10 | "sync" 11 | 12 | "github.com/fsnotify/fsnotify" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | // AppConfig 应用配置 17 | type AppConfig struct { 18 | AppName string `mapstructure:"APP_NAME"` 19 | AppHost string `mapstructure:"APP_HOST"` 20 | AppPort string `mapstructure:"APP_PORT"` 21 | EmailType string `mapstructure:"EMAIL_TYPE"` 22 | FromEmail string `mapstructure:"FROM_EMAIL"` 23 | EmailSmtp string `mapstructure:"EMAIL_SMTP"` 24 | } 25 | 26 | // DatabaseConfig 数据库配置 27 | type DatabaseConfig struct { 28 | DBDialect string `mapstructure:"DB_DIALECT"` 29 | DBName string `mapstructure:"DB_NAME"` 30 | DBHost string `mapstructure:"DB_HOST"` 31 | DBPort string `mapstructure:"DB_PORT"` 32 | DBUser string `mapstructure:"DB_USER"` 33 | DBPassword string `mapstructure:"DB_PSW"` 34 | DBPath string `mapstructure:"DB_PATH"` 35 | } 36 | 37 | // RedisConfig Redis配置 38 | type RedisConfig struct { 39 | RedisHost string `mapstructure:"REDIS_HOST"` 40 | RedisPort string `mapstructure:"REDIS_PORT"` 41 | RedisDB string `mapstructure:"REDIS_DB"` 42 | RedisPassword string `mapstructure:"REDIS_PSW"` 43 | } 44 | 45 | // LogConfig 日志配置 46 | type LogConfig struct { 47 | LogFilePath string `mapstructure:"LOG_FILE_PATH"` 48 | LogFileName string `mapstructure:"LOG_FILE_NAME"` 49 | LogTimestampFmt string `mapstructure:"LOG_TIMESTAMP_FMT"` 50 | LogMaxAge int64 `mapstructure:"LOG_MAX_AGE"` 51 | LogRotationTime int64 `mapstructure:"LOG_ROTATION_TIME"` 52 | LogLevel string `mapstructure:"LOG_LEVEL"` 53 | } 54 | 55 | // SwaggerConfig Swagger配置 56 | type SwaggerConfig struct { 57 | SwaggerHost string `mapstructure:"SWAGGER_HOST"` 58 | SwaggerEnabled string `mapstructure:"SWAGGER_ENABLED"` 59 | } 60 | 61 | // Config 总配置结构 62 | type Config struct { 63 | AppConfig AppConfig `mapstructure:"app"` 64 | DBConfig DatabaseConfig `mapstructure:"database"` 65 | RedisConfig RedisConfig `mapstructure:"redis"` 66 | LogConfig LogConfig `mapstructure:"log"` 67 | SwaggerConfig SwaggerConfig `mapstructure:"swagger"` 68 | } 69 | 70 | // DefaultConfigPath 默认配置文件路径 71 | const DefaultConfigPath = "./configs/config.yml" 72 | 73 | var ( 74 | globalConfig *Config // 全局配置实例 75 | configLock sync.RWMutex // 配置读写锁 76 | viperInstance *viper.Viper // viper实例 77 | ) 78 | 79 | // Init 初始化配置 80 | // 参数: 81 | // - configPath: 配置文件路径 82 | // 83 | // 返回值: 84 | // - error: 初始化过程中的错误 85 | func Init(configPath string) error { 86 | viperInstance = viper.New() 87 | viperInstance.SetConfigFile(configPath) 88 | 89 | if err := viperInstance.ReadInConfig(); err != nil { 90 | return fmt.Errorf("配置文件读取失败: %w", err) 91 | } 92 | 93 | var config Config 94 | if err := viperInstance.Unmarshal(&config); err != nil { 95 | return fmt.Errorf("配置解析失败: %w", err) 96 | } 97 | 98 | globalConfig = &config 99 | go monitorConfigChanges() 100 | return nil 101 | } 102 | 103 | // LoadConfig 获取配置 104 | // 返回值: 105 | // - *Config: 配置副本 106 | // - error: 获取过程中的错误 107 | func LoadConfig() (*Config, error) { 108 | configLock.RLock() 109 | defer configLock.RUnlock() 110 | 111 | if globalConfig == nil { 112 | return nil, fmt.Errorf("配置未初始化") 113 | } 114 | 115 | configCopy := *globalConfig 116 | return &configCopy, nil 117 | } 118 | 119 | // monitorConfigChanges 监听配置变更 120 | func monitorConfigChanges() { 121 | viperInstance.WatchConfig() 122 | viperInstance.OnConfigChange(func(e fsnotify.Event) { 123 | var newConfig Config 124 | if err := viperInstance.Unmarshal(&newConfig); err != nil { 125 | log.Printf("新配置解析失败: %v", err) 126 | return 127 | } 128 | 129 | configLock.Lock() 130 | defer configLock.Unlock() 131 | 132 | oldConfig := *globalConfig 133 | changes := make(map[string][2]interface{}) 134 | 135 | if !compareStructs(oldConfig, newConfig, "", changes) { 136 | log.Printf("配置类型不一致,变更被阻止") 137 | return 138 | } 139 | 140 | globalConfig = &newConfig 141 | 142 | for path, values := range changes { 143 | log.Printf("配置项 [%s] 发生变化: %v -> %v", path, values[0], values[1]) 144 | } 145 | }) 146 | } 147 | 148 | // compareStructs 比较结构体并收集变更 149 | // 参数: 150 | // - oldObj: 旧结构体 151 | // - newObj: 新结构体 152 | // - prefix: 字段路径前缀 153 | // - changes: 记录变更的映射 154 | // 155 | // 返回值: 156 | // - bool: 结构体类型是否一致 157 | func compareStructs(oldObj, newObj interface{}, prefix string, changes map[string][2]interface{}) bool { 158 | oldVal := reflect.ValueOf(oldObj) 159 | newVal := reflect.ValueOf(newObj) 160 | 161 | if oldVal.Type() != newVal.Type() { 162 | return false 163 | } 164 | 165 | if oldVal.Kind() != reflect.Struct { 166 | return true 167 | } 168 | 169 | for i := 0; i < oldVal.NumField(); i++ { 170 | oldField := oldVal.Field(i) 171 | newField := newVal.Field(i) 172 | fieldName := oldVal.Type().Field(i).Name 173 | fullName := prefix + fieldName 174 | 175 | if oldField.Kind() == reflect.Struct { 176 | if !compareStructs(oldField.Interface(), newField.Interface(), fullName+".", changes) { 177 | return false 178 | } 179 | continue 180 | } 181 | 182 | if oldField.Kind() != newField.Kind() { 183 | return false 184 | } 185 | 186 | if !reflect.DeepEqual(oldField.Interface(), newField.Interface()) { 187 | changes[fullName] = [2]interface{}{oldField.Interface(), newField.Interface()} 188 | } 189 | } 190 | 191 | return true 192 | } 193 | -------------------------------------------------------------------------------- /configs/config.yml: -------------------------------------------------------------------------------- 1 | # 应用相关 2 | app: 3 | APP_NAME: "JANK_BLOG" 4 | APP_HOST: "127.0.0.1" # 如果使用docker,则改为"0.0.0.0" 5 | APP_PORT: "9010" 6 | EMAIL_TYPE: "qq" # 支持的邮箱类型: qq, gmail, outlook 7 | FROM_EMAIL: "" # 发件人邮箱 8 | EMAIL_SMTP: "" # SMTP 授权码 9 | 10 | database: 11 | DB_DIALECT: "postgres" # 数据库类型, 可选值: postgres, mysql, sqlite 12 | DB_NAME: "jank_db" 13 | DB_HOST: "127.0.0.1" # 如果使用docker,则改为"postgres_db" 14 | DB_PORT: "5432" 15 | DB_USER: "" 16 | DB_PSW: "" 17 | DB_PATH: "./database" # SQLite 数据库文件路径 18 | 19 | # Redis 相关 20 | redis: 21 | REDIS_HOST: "127.0.0.1" # 如果使用docker,则改为"redis_db" 22 | REDIS_PORT: "6379" 23 | REDIS_DB: "0" 24 | REDIS_PSW: "" 25 | 26 | # 日志相关 27 | log: 28 | LOG_FILE_PATH: ".logs/" 29 | LOG_FILE_NAME: "app.log" 30 | LOG_TIMESTAMP_FMT: "2006-01-02 15:04:05" 31 | LOG_MAX_AGE: 72 32 | LOG_ROTATION_TIME: 24 33 | LOG_LEVEL: "INFO" 34 | 35 | # Swagger 相关 36 | swagger: 37 | SWAGGER_HOST: "localhost:9010" 38 | SWAGGER_ENABLED: "true" # 是否启用Swagger,可选值: true, false 39 | -------------------------------------------------------------------------------- /configs/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | 6 | events { 7 | worker_connections 1024; 8 | multi_accept on; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | # 日志配置 16 | log_format main '$remote_addr [$time_local] "$request" $status $body_bytes_sent "$http_referer"'; 17 | access_log /var/log/nginx/access.log main; 18 | 19 | sendfile on; 20 | tcp_nopush on; 21 | keepalive_timeout 65; 22 | client_max_body_size 100M; 23 | server_tokens off; 24 | 25 | # 使用Docker内置DNS 26 | resolver 127.0.0.11 valid=30s ipv6=on; 27 | 28 | # WebSocket支持 29 | map $http_upgrade $connection_upgrade { 30 | default upgrade; 31 | '' close; 32 | } 33 | 34 | # HTTP 监听,重定向到 HTTPS 35 | server { 36 | listen 80; 37 | server_name jank.org.cn www.jank.org.cn; 38 | return 301 https://$host$request_uri; 39 | } 40 | 41 | # HTTPS 监听,启用 SSL 配置 42 | server { 43 | listen 443 ssl; 44 | server_name jank.org.cn www.jank.org.cn; 45 | 46 | ssl_certificate /etc/ssl/jank.org.cn.pem; 47 | ssl_certificate_key /etc/ssl/jank.org.cn.key; 48 | ssl_protocols TLSv1.2 TLSv1.3; 49 | ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384'; 50 | ssl_prefer_server_ciphers off; 51 | ssl_session_cache shared:SSL:10m; 52 | ssl_session_timeout 1d; 53 | ssl_session_tickets off; 54 | 55 | # 通用头部 56 | proxy_set_header Host $host; 57 | proxy_set_header X-Real-IP $remote_addr; 58 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 59 | proxy_set_header X-Forwarded-Proto $scheme; 60 | proxy_set_header Upgrade $http_upgrade; 61 | proxy_set_header Connection $connection_upgrade; 62 | 63 | # API 转发到后端 64 | location /api/ { 65 | set $backend_upstream "http://app:9010"; 66 | proxy_pass $backend_upstream; 67 | } 68 | 69 | # Swagger UI 文档 70 | location /swagger/ { 71 | set $backend_upstream "http://app:9010"; 72 | rewrite ^/swagger/(.*)$ /swagger/$1 break; 73 | proxy_pass $backend_upstream; 74 | proxy_redirect off; 75 | proxy_buffering off; 76 | } 77 | 78 | # 前端应用转发 79 | location / { 80 | set $frontend_upstream "http://frontend:3000"; 81 | proxy_pass $frontend_upstream; 82 | } 83 | } 84 | 85 | # 默认服务器,拒绝访问 86 | server { 87 | listen 80 default_server; 88 | server_name _; 89 | return 444; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | postgres: 5 | image: postgres:15 6 | container_name: postgres_db 7 | restart: always 8 | environment: 9 | - POSTGRES_DB=jank_db 10 | - POSTGRES_USER= 11 | - POSTGRES_PASSWORD= 12 | healthcheck: 13 | test: ["CMD", "pg_isready", "-U", "", "-d", "jank_db"] 14 | interval: 30s 15 | timeout: 10s 16 | retries: 30 17 | volumes: 18 | - postgres_data:/var/lib/postgresql/data 19 | networks: 20 | - jank_blog_network 21 | 22 | redis: 23 | image: redis:7.0 24 | container_name: redis_db 25 | restart: unless-stopped 26 | command: redis-server --save "" 27 | healthcheck: 28 | test: ["CMD", "redis-cli", "ping"] 29 | interval: 30s 30 | timeout: 10s 31 | retries: 5 32 | volumes: 33 | - redis_data:/data 34 | networks: 35 | - jank_blog_network 36 | 37 | app: 38 | build: ./backend 39 | container_name: app 40 | restart: unless-stopped 41 | ports: 42 | - "9010:9010" 43 | depends_on: 44 | postgres: 45 | condition: service_healthy 46 | redis: 47 | condition: service_healthy 48 | healthcheck: 49 | test: ["CMD-SHELL", "curl -f http://localhost:9010/ || exit 0"] 50 | interval: 30s 51 | timeout: 10s 52 | retries: 5 53 | start_period: 30s 54 | volumes: 55 | - ./backend/configs:/app/configs 56 | networks: 57 | - jank_blog_network 58 | 59 | frontend: 60 | build: ./frontend 61 | container_name: frontend 62 | restart: unless-stopped 63 | ports: 64 | - "3000:3000" 65 | healthcheck: 66 | test: ["CMD-SHELL", "curl -f http://localhost:3000/ || exit 0"] 67 | interval: 30s 68 | timeout: 10s 69 | retries: 5 70 | start_period: 30s 71 | networks: 72 | - jank_blog_network 73 | 74 | nginx: 75 | image: nginx:1.24.0 76 | container_name: nginx 77 | restart: always 78 | ports: 79 | - "80:80" 80 | - "443:443" 81 | volumes: 82 | - ./configs/nginx.conf:/etc/nginx/nginx.conf:ro 83 | - ./ssl:/etc/ssl:ro 84 | - ./logs/nginx:/var/log/nginx 85 | networks: 86 | - jank_blog_network 87 | command: > 88 | bash -c "mkdir -p /var/cache/nginx/proxy_cache && 89 | chown -R nginx:nginx /var/cache/nginx && 90 | nginx -g 'daemon off;'" 91 | depends_on: 92 | - frontend 93 | - app 94 | 95 | networks: 96 | jank_blog_network: 97 | driver: bridge 98 | 99 | volumes: 100 | postgres_data: 101 | redis_data: -------------------------------------------------------------------------------- /docs/standards.md: -------------------------------------------------------------------------------- 1 | # Go 语言编码规范 2 | 3 | ## 一、命名规范 4 | 5 | ### 基本原则 6 | 7 | - 命名必须以字母(A-Z、a-z)或下划线开头,后续可使用字母、下划线或数字(0-9)。 8 | - 严禁在命名中使用特殊符号,如 @、$、% 等。 9 | - Go 语言区分大小写,首字母大写的标识符可被外部包访问(公开),首字母小写则仅包内可访问(私有)。 10 | 11 | ### 1. 包命名(package) 12 | 13 | - 包名必须与目录名保持一致,应选择简洁、有意义且不与标准库冲突的名称。 14 | - 包名必须全部小写,多个单词可使用下划线分隔或采用混合式小写(不推荐使用驼峰式)。 15 | 16 | ```go 17 | package demo 18 | package main 19 | ``` 20 | 21 | ### 2. 文件命名 22 | 23 | - 文件名应有明确含义,简洁易懂。 24 | - 必须使用小写字母,多个单词间使用下划线分隔。 25 | 26 | ```go 27 | my_test.go 28 | ``` 29 | 30 | ### 3. 结构体命名 31 | 32 | - 必须采用驼峰命名法,首字母根据访问控制需求决定大小写。 33 | - 结构体声明和初始化必须采用多行格式,示例如下: 34 | 35 | ```go 36 | // 多行声明 37 | type User struct { 38 | Username string 39 | Email string 40 | } 41 | 42 | // 多行初始化 43 | user := User{ 44 | Username: "admin", 45 | Email: "admin@example.com", 46 | } 47 | ``` 48 | 49 | ### 4. 接口命名 50 | 51 | - 必须采用驼峰命名法,首字母根据访问控制需求决定大小写。 52 | - 单一功能的接口名应以 "er" 作为后缀,例如 Reader、Writer。 53 | 54 | ```go 55 | type Reader interface { 56 | Read(p []byte) (n int, err error) 57 | } 58 | ``` 59 | 60 | ### 5. 变量命名 61 | 62 | - 必须采用驼峰命名法,首字母根据访问控制需求决定大小写。 63 | - 特有名词的处理规则: 64 | - 如果变量为私有且特有名词为首个单词,则使用小写,如 apiClient。 65 | - 其他情况应保持该名词原有的写法,如 APIClient、repoID、UserID。 66 | - 错误示例:UrlArray,应写为 urlArray 或 URLArray。 67 | - 布尔类型变量名必须以 Has、Is、Can 或 Allow 开头。 68 | 69 | ```go 70 | var isExist bool 71 | var hasConflict bool 72 | var canManage bool 73 | var allowGitHook bool 74 | ``` 75 | 76 | ### 6. 常量命名 77 | 78 | - 常量必须全部使用大写字母,并使用下划线分隔单词。 79 | 80 | ```go 81 | const APP_VER = "1.0.0" 82 | ``` 83 | 84 | - 枚举类型的常量,应先创建相应类型: 85 | 86 | ```go 87 | type Scheme string 88 | 89 | const ( 90 | HTTP Scheme = "http" 91 | HTTPS Scheme = "https" 92 | ) 93 | ``` 94 | 95 | ### 7. 关键字 96 | 97 | Go 语言的关键字:break、case、chan、const、continue、default、defer、else、fallthrough、for、func、go、goto、if、import、interface、map、package、range、return、select、struct、switch、type、var 98 | 99 | ## 二、注释规范 100 | 101 | Go 语言支持 C 风格的注释语法,包括 `/**/` 和 `//`。 102 | 103 | - 行注释(//)是最常用的注释形式。 104 | - 块注释(/\* \*/)主要用于包注释,不可嵌套使用,通常用于文档说明或注释大段代码。 105 | 106 | ### 1. 包注释 107 | 108 | - 每个包必须有一个包注释,位于 package 子句之前。 109 | - 包内如果有多个文件,包注释只需在一个文件中出现(建议是与包同名的文件)。 110 | - 包注释必须包含以下信息(按顺序): 111 | - 包的基本简介(包名及功能说明) 112 | - 创建者信息,格式:创建者:[GitHub 用户名] 113 | - 创建时间,格式:创建时间:yyyy-MM-dd 114 | 115 | ```go 116 | // Package router Jank Blog 路由功能定义(路由注册/中间件加载) 117 | // 创建者:Done-0 118 | // 创建时间:2025-03-25 119 | ``` 120 | 121 | ### 2. 结构体与接口注释 122 | 123 | - 每个自定义结构体或接口必须有注释说明,放在定义的前一行。 124 | - 注释格式为:[结构体名/接口名],[说明]。 125 | - 结构体的每个成员变量必须有说明,放在成员变量后面并保持对齐。 126 | - 例如:下方的 `User` 为结构体名,`用户对象,定义了用户的基础信息` 为说明。 127 | 128 | ```go 129 | // User,用户对象,定义了用户的基础信息 130 | type User struct { 131 | Username string // 用户名 132 | Email string // 邮箱 133 | } 134 | ``` 135 | 136 | ### 3. 函数与方法注释 137 | 138 | 每个函数或方法必须有注释说明,包含以下内容(按顺序): 139 | - 简要说明:以函数名开头,使用空格分隔说明部分 140 | - 参数列表:每行一个参数,参数名开头,“: ”分隔说明部分 141 | - 返回值:每行一个返回值 142 | 143 | ```go 144 | // NewtAttrModel 属性数据层操作类的工厂方法 145 | // 参数: 146 | // ctx: 上下文信息 147 | // 148 | // 返回值: 149 | // *AttrModel: 属性操作类指针 150 | func NewAttrModel(ctx *common.Context) *AttrModel { 151 | } 152 | ``` 153 | 154 | ### 4. 代码逻辑注释 155 | 156 | - 对于关键位置或复杂逻辑处理,必须添加逻辑说明注释。 157 | 158 | ```go 159 | // 从 Redis 中批量读取属性,对于没有读取到的 id,记录到一个数组里面,准备从 DB 中读取 160 | // 后续代码... 161 | ``` 162 | 163 | ### 5. 注释风格 164 | 165 | - 统一使用中文注释。 166 | - 中英文字符之间必须使用空格分隔,包括中文与英文、中文与英文标点之间。 167 | 168 | ```go 169 | // 从 Redis 中批量读取属性,对于没有读取到的 id,记录到一个数组里面,准备从 DB 中读取 170 | ``` 171 | 172 | - 建议全部使用单行注释。 173 | - 单行注释不得超过 120 个字符。 174 | 175 | ## 三、代码风格 176 | 177 | ### 1. 缩进与折行 178 | 179 | - 缩进必须使用 gofmt 工具格式化(使用 tab 缩进)。 180 | - 每行代码不应超过 120 个字符,超过时应使用换行并保持格式优雅。 181 | 182 | > 使用 Goland 开发工具时,可通过快捷键 Control + Alt + L 格式化代码。 183 | 184 | ### 2. 语句结尾 185 | 186 | - Go 语言不需要使用分号结尾,一行代表一条语句。 187 | - 多条语句写在同一行时,必须使用分号分隔。 188 | 189 | 190 | ```go 191 | package main 192 | 193 | func main() { 194 | var a int = 5; var b int = 10 195 | // 多条语句写在同一行时,必须使用分号分隔 196 | c := a + b; fmt.Println(c) 197 | } 198 | ``` 199 | 200 | - 代码简单时可以使用多行语句,但建议使用单行语句 201 | 202 | ```go 203 | package main 204 | 205 | func main() { 206 | var a int = 5 207 | var b int = 10 208 | 209 | c := a + b 210 | fmt.Println(c) 211 | } 212 | ``` 213 | 214 | ### 3. 括号与空格 215 | 216 | - 左大括号不得换行(Go 语法强制要求)。 217 | - 所有运算符与操作数之间必须留有空格。 218 | 219 | ```go 220 | // 正确示例 221 | if a > 0 { 222 | // 代码块 223 | } 224 | 225 | // 错误示例 226 | if a>0 // a、0 和 > 之间应有空格 227 | { // 左大括号不可换行,会导致语法错误 228 | // 代码块 229 | } 230 | ``` 231 | 232 | ### 4. import 规范 233 | 234 | - 单个包引入时,建议使用括号格式: 235 | 236 | ```go 237 | import ( 238 | "fmt" 239 | ) 240 | ``` 241 | 242 | - 多个包引入时,应按以下顺序分组,并用空行分隔: 243 | 1. 标准库包 244 | 2. 第三方包 245 | 3. 项目内部包 246 | 247 | ```go 248 | import ( 249 | "context" 250 | "fmt" 251 | "sync" 252 | "time" 253 | 254 | "github.com/labstack/echo/v4" 255 | "golang.org/x/crypto/bcrypt" 256 | 257 | "jank.com/jank_blog/internal/global" 258 | model "jank.com/jank_blog/internal/model/account" 259 | "jank.com/jank_blog/internal/utils" 260 | "jank.com/jank_blog/pkg/serve/controller/account/dto" 261 | "jank.com/jank_blog/pkg/serve/mapper" 262 | "jank.com/jank_blog/pkg/vo/account" 263 | ) 264 | ``` 265 | 266 | - 禁止使用相对路径引入外部包: 267 | 268 | ```go 269 | // 错误示例 270 | import "../net" // 禁止使用相对路径引入外部包 271 | 272 | // 正确示例 273 | import "github.com/repo/proj/src/net" 274 | ``` 275 | 276 | - 包名和导入路径不匹配,建议使用别名: 277 | 278 | ```go 279 | // 错误示例 280 | import "jank.com/jank_blog/internal/model/account" // 此文件的实际包名为 model 281 | 282 | // 正确示例 283 | import model "jank.com/jank_blog/internal/model/account" // 使用 model 别名 284 | ``` 285 | 286 | ### 5. 错误处理 287 | 288 | - 不得丢弃任何有返回 err 的调用,禁止使用 `_` 丢弃错误,必须全部处理。 289 | - 错误处理原则: 290 | - 一旦发生错误,应立即返回(尽早 return)。 291 | - 除非确切了解后果,否则不要使用 panic。 292 | - 英文错误描述必须全部小写,不需要标点结尾。 293 | - 必须采用独立的错误流进行处理。 294 | 295 | ```go 296 | // 错误示例 297 | if err != nil { 298 | // 错误处理 299 | } else { 300 | // 正常代码 301 | } 302 | 303 | // 正确示例 304 | if err != nil { 305 | // 错误处理 306 | return // 或 continue 等 307 | } 308 | // 正常代码 309 | ``` 310 | 311 | ### 6. 测试规范 312 | 313 | - 测试文件命名必须以 `_test.go` 结尾,如 `example_test.go`。 314 | - 测试函数名称必须以 `Test` 开头,如 `TestExample`。 315 | - 每个重要函数都应编写测试用例,与正式代码一起提交,便于回归测试。 316 | 317 | ## 四、常用工具 318 | 319 | Go 语言提供了多种工具帮助开发者遵循代码规范: 320 | 321 | ### gofmt 322 | 323 | 大部分格式问题可通过 gofmt 解决,它能自动格式化代码,确保所有 Go 代码与官方推荐格式保持一致。所有格式相关问题均以 gofmt 结果为准。 324 | 325 | ### goimports 326 | 327 | 强烈建议使用 goimports,它在 gofmt 基础上增加了自动删除和引入包的功能。 328 | 329 | ```bash 330 | go get golang.org/x/tools/cmd/goimports 331 | ``` 332 | 333 | ### go vet 334 | 335 | vet 工具可静态分析源码中的各种问题,如多余代码、提前 return 的逻辑、struct 的 tag 是否符合标准等。 336 | 337 | ```bash 338 | go get golang.org/x/tools/cmd/vet 339 | ``` 340 | 341 | 使用方法: 342 | 343 | ```bash 344 | go vet . 345 | ``` 346 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module jank.com/jank_blog 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/bwmarrin/snowflake v0.3.0 7 | github.com/fsnotify/fsnotify v1.9.0 8 | github.com/go-playground/validator/v10 v10.26.0 9 | github.com/golang-jwt/jwt/v4 v4.5.2 10 | github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible 11 | github.com/labstack/echo/v4 v4.13.3 12 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible 13 | github.com/mojocn/base64Captcha v1.3.8 14 | github.com/redis/go-redis/v9 v9.7.3 15 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 16 | github.com/sirupsen/logrus v1.9.3 17 | github.com/spf13/viper v1.20.1 18 | github.com/swaggo/echo-swagger v1.4.1 19 | github.com/swaggo/swag v1.16.4 20 | github.com/yuin/goldmark v1.7.11 21 | golang.org/x/crypto v0.37.0 22 | gorm.io/driver/mysql v1.5.7 23 | gorm.io/driver/postgres v1.5.11 24 | gorm.io/driver/sqlite v1.5.7 25 | gorm.io/gorm v1.26.0 26 | ) 27 | 28 | require ( 29 | filippo.io/edwards25519 v1.1.0 // indirect 30 | github.com/KyleBanks/depth v1.2.1 // indirect 31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 32 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 33 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 34 | github.com/ghodss/yaml v1.0.0 // indirect 35 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 36 | github.com/go-openapi/jsonreference v0.21.0 // indirect 37 | github.com/go-openapi/spec v0.21.0 // indirect 38 | github.com/go-openapi/swag v0.23.1 // indirect 39 | github.com/go-playground/locales v0.14.1 // indirect 40 | github.com/go-playground/universal-translator v0.18.1 // indirect 41 | github.com/go-sql-driver/mysql v1.9.2 // indirect 42 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 43 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 44 | github.com/jackc/pgpassfile v1.0.0 // indirect 45 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 46 | github.com/jackc/pgx/v5 v5.7.4 // indirect 47 | github.com/jackc/puddle/v2 v2.2.2 // indirect 48 | github.com/jinzhu/inflection v1.0.0 // indirect 49 | github.com/jinzhu/now v1.1.5 // indirect 50 | github.com/jonboulle/clockwork v0.5.0 // indirect 51 | github.com/josharian/intern v1.0.0 // indirect 52 | github.com/labstack/gommon v0.4.2 // indirect 53 | github.com/leodido/go-urn v1.4.0 // indirect 54 | github.com/lestrrat-go/strftime v1.1.0 // indirect 55 | github.com/mailru/easyjson v0.9.0 // indirect 56 | github.com/mattn/go-colorable v0.1.14 // indirect 57 | github.com/mattn/go-isatty v0.0.20 // indirect 58 | github.com/mattn/go-sqlite3 v1.14.28 // indirect 59 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 60 | github.com/pkg/errors v0.9.1 // indirect 61 | github.com/rogpeppe/go-internal v1.14.1 // indirect 62 | github.com/sagikazarmark/locafero v0.9.0 // indirect 63 | github.com/sourcegraph/conc v0.3.0 // indirect 64 | github.com/spf13/afero v1.14.0 // indirect 65 | github.com/spf13/cast v1.7.1 // indirect 66 | github.com/spf13/pflag v1.0.6 // indirect 67 | github.com/subosito/gotenv v1.6.0 // indirect 68 | github.com/swaggo/files/v2 v2.0.2 // indirect 69 | github.com/valyala/bytebufferpool v1.0.0 // indirect 70 | github.com/valyala/fasttemplate v1.2.2 // indirect 71 | go.uber.org/multierr v1.11.0 // indirect 72 | golang.org/x/image v0.26.0 // indirect 73 | golang.org/x/net v0.39.0 // indirect 74 | golang.org/x/sync v0.13.0 // indirect 75 | golang.org/x/sys v0.32.0 // indirect 76 | golang.org/x/text v0.24.0 // indirect 77 | golang.org/x/time v0.11.0 // indirect 78 | golang.org/x/tools v0.32.0 // indirect 79 | gopkg.in/yaml.v2 v2.4.0 // indirect 80 | gopkg.in/yaml.v3 v3.0.1 // indirect 81 | ) 82 | -------------------------------------------------------------------------------- /internal/banner/README.md: -------------------------------------------------------------------------------- 1 | Banner 组件 2 | 3 | 启动时显示 Banner: 4 | 5 | ```bash 6 | ╔══════════════════════════════════════╗ 7 | ║ ██╗ █████╗ ███╗ ██╗██╗ ██╗ ║ 8 | ║ ██║██╔══██╗████╗ ██║██║ ██╔╝ ║ 9 | ║ ██║███████║██╔██╗ ██║█████╔╝ ║ 10 | ║ ██ ██║██╔══██║██║╚██╗██║██╔═██╗ ║ 11 | ║ ╚█████╔╝██║ ██║██║ ╚████║██║ ██╗ ║ 12 | ║ ╚════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ║ 13 | ╚══════════════════════════════════════╝ 14 | ▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀ 15 | ════════════════════════════════════════════ 16 | ``` -------------------------------------------------------------------------------- /internal/banner/banner.go: -------------------------------------------------------------------------------- 1 | // Package banner 提供应用程序启动横幅显示功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package banner 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | // New 初始化并显示应用程序启动横幅 13 | // 参数: 14 | // - app: Echo实例 15 | func New(app *echo.Echo) { 16 | app.HideBanner = true 17 | banner := ` 18 | ╔══════════════════════════════════════╗ 19 | ║ ██╗ █████╗ ███╗ ██╗██╗ ██╗ ║ 20 | ║ ██║██╔══██╗████╗ ██║██║ ██╔╝ ║ 21 | ║ ██║███████║██╔██╗ ██║█████╔╝ ║ 22 | ║ ██ ██║██╔══██║██║╚██╗██║██╔═██╗ ║ 23 | ║ ╚█████╔╝██║ ██║██║ ╚████║██║ ██╗ ║ 24 | ║ ╚════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ║ 25 | ╚══════════════════════════════════════╝ 26 | ▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀▄▀ 27 | ════════════════════════════════════════════ 28 | ` 29 | fmt.Print(banner) 30 | } 31 | -------------------------------------------------------------------------------- /internal/db/README.md: -------------------------------------------------------------------------------- 1 | # 数据库交互组件 (Database Interaction Component) 2 | 3 | ## 简介 4 | 5 | 数据库交互组件负责应用程序与各种数据库系统的连接和交互,支持多种数据库类型,包括 PostgreSQL、MySQL 和 SQLite。该组件提供了数据库连接、自动迁移和管理功能,为上层业务逻辑提供稳定的数据存储和访问支持。 6 | 7 | ## 支持的数据库类型 8 | 9 | - **PostgreSQL**: 企业级关系型数据库,适合大规模应用 10 | - **MySQL**: 流行的开源关系型数据库,性能优良 11 | - **SQLite**: 轻量级嵌入式数据库,适合开发和小型应用 12 | 13 | ## 核心功能 14 | 15 | - **数据库连接管理**: 建立和管理与不同类型数据库的连接 16 | - **自动创建数据库**: 在系统级数据库中自动创建应用数据库(针对 PostgreSQL 和 MySQL) 17 | - **自动迁移**: 根据模型定义自动创建和更新数据库表结构 18 | - **多数据库支持**: 根据配置灵活切换不同类型的数据库 19 | - **连接参数配置**: 支持连接超时、字符集等参数配置 20 | 21 | ## 实现细节 22 | 23 | - 基于 GORM 框架实现 ORM(对象关系映射)功能 24 | - 使用特定数据库驱动:`gorm.io/driver/postgres`、`gorm.io/driver/mysql` 和 `gorm.io/driver/sqlite` 25 | - 支持数据库方言设置,可以通过配置切换数据库类型 26 | - 提供数据库不存在时的自动创建逻辑 27 | - 支持数据库路径和文件权限管理(特别是 SQLite) 28 | 29 | ## 数据库配置参数 30 | 31 | 数据库组件从应用配置中读取以下参数: 32 | 33 | - **DBDialect**: 数据库类型(`POSTGRES`、`MYSQL` 或 `SQLITE`) 34 | - **DBHost**: 数据库服务器主机地址 35 | - **DBPort**: 数据库服务器端口 36 | - **DBUser**: 数据库用户名 37 | - **DBPassword**: 数据库密码 38 | - **DBName**: 应用数据库名称 39 | - **DBPath**: SQLite 数据库文件路径 40 | 41 | ## 使用方式 42 | 43 | 通过全局变量 `global.DB` 在应用的任何位置访问数据库: 44 | 45 | ```go 46 | // 查询数据 47 | user := new(model.Account) 48 | if err := global.DB.Where("email = ?", email).First(user).Error; err != nil { 49 | // 处理错误 50 | } 51 | 52 | // 创建数据 53 | newPost := &model.Post{Title: "新文章", ContentMarkdown: "# 标题"} 54 | if err := global.DB.Create(newPost).Error; err != nil { 55 | // 处理错误 56 | } 57 | ``` 58 | 59 | ## 自动迁移 60 | 61 | 应用启动时会自动执行数据库迁移,根据模型定义创建或更新表结构: 62 | 63 | ```go 64 | // 获取所有模型 65 | models := model.GetAllModels() 66 | 67 | // 执行迁移 68 | global.DB.AutoMigrate(models...) 69 | ``` 70 | -------------------------------------------------------------------------------- /internal/db/auto_migrate.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | 6 | "jank.com/jank_blog/internal/global" 7 | "jank.com/jank_blog/internal/model" 8 | ) 9 | 10 | // autoMigrate 执行数据库表结构自动迁移 11 | func autoMigrate() { 12 | if global.DB == nil { 13 | log.Fatal("数据库初始化失败,无法执行自动迁移...") 14 | } 15 | 16 | err := global.DB.AutoMigrate( 17 | model.GetAllModels()..., 18 | ) 19 | if err != nil { 20 | log.Fatalf("数据库自动迁移失败: %v", err) 21 | } 22 | 23 | log.Println("数据库自动迁移成功...") 24 | global.SysLog.Infof("数据库自动迁移成功...") 25 | } 26 | -------------------------------------------------------------------------------- /internal/db/conn.go: -------------------------------------------------------------------------------- 1 | // Package db 提供数据库连接和管理功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package db 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | 12 | "gorm.io/driver/mysql" 13 | "gorm.io/driver/postgres" 14 | "gorm.io/driver/sqlite" 15 | "gorm.io/gorm" 16 | 17 | "jank.com/jank_blog/configs" 18 | "jank.com/jank_blog/internal/global" 19 | ) 20 | 21 | // 数据库类型常量 22 | const ( 23 | DIALECT_POSTGRES = "postgres" // PostgreSQL 数据库 24 | DIALECT_SQLITE = "sqlite" // SQLite 数据库 25 | DIALECT_MYSQL = "mysql" // MySQL 数据库 26 | ) 27 | 28 | // New 初始化数据库连接 29 | // 参数: 30 | // - config: 应用配置 31 | func New(config *configs.Config) { 32 | var err error 33 | 34 | switch config.DBConfig.DBDialect { 35 | case DIALECT_SQLITE: 36 | global.DB, err = connectToDB(config, config.DBConfig.DBName) 37 | if err != nil { 38 | global.SysLog.Fatalf("连接 SQLite 数据库失败: %v", err) 39 | } 40 | case DIALECT_POSTGRES, DIALECT_MYSQL: 41 | systemDB, err := connectToSystemDB(config) 42 | if err != nil { 43 | global.SysLog.Fatalf("连接系统数据库失败: %v", err) 44 | } 45 | 46 | if err := ensureDBExists(systemDB, config); err != nil { 47 | global.SysLog.Fatalf("数据库不存在且创建失败: %v", err) 48 | } 49 | 50 | sqlDB, _ := systemDB.DB() 51 | sqlDB.Close() 52 | 53 | global.DB, err = connectToDB(config, config.DBConfig.DBName) 54 | if err != nil { 55 | global.SysLog.Fatalf("连接数据库失败: %v", err) 56 | } 57 | default: 58 | global.SysLog.Fatalf("不支持的数据库类型: %s", config.DBConfig.DBDialect) 59 | } 60 | 61 | log.Printf("「%s」数据库连接成功...", config.DBConfig.DBName) 62 | global.SysLog.Infof("「%s」数据库连接成功!", config.DBConfig.DBName) 63 | 64 | autoMigrate() 65 | } 66 | 67 | // connectToSystemDB 连接到系统数据库 68 | // 参数: 69 | // - config: 应用配置 70 | // 71 | // 返回值: 72 | // - *gorm.DB: 数据库连接 73 | // - error: 连接过程中的错误 74 | func connectToSystemDB(config *configs.Config) (*gorm.DB, error) { 75 | switch config.DBConfig.DBDialect { 76 | case DIALECT_POSTGRES: 77 | return connectToDB(config, "postgres") 78 | case DIALECT_MYSQL: 79 | return connectToDB(config, "information_schema") 80 | default: 81 | return nil, fmt.Errorf("不支持的数据库类型: %s", config.DBConfig.DBDialect) 82 | } 83 | } 84 | 85 | // ensureDBExists 确保数据库存在,不存在则创建 86 | // 参数: 87 | // - db: 数据库连接 88 | // - config: 应用配置 89 | // 90 | // 返回值: 91 | // - error: 创建过程中的错误 92 | func ensureDBExists(db *gorm.DB, config *configs.Config) error { 93 | switch config.DBConfig.DBDialect { 94 | case DIALECT_POSTGRES: 95 | return ensurePostgresDBExists(db, config.DBConfig.DBName, config.DBConfig.DBUser) 96 | case DIALECT_MYSQL: 97 | return ensureMySQLDBExists(db, config.DBConfig.DBName) 98 | default: 99 | return nil 100 | } 101 | } 102 | 103 | // connectToDB 连接到指定数据库 104 | // 参数: 105 | // - config: 应用配置 106 | // - dbName: 数据库名称 107 | // 108 | // 返回值: 109 | // - *gorm.DB: 数据库连接 110 | // - error: 连接过程中的错误 111 | func connectToDB(config *configs.Config, dbName string) (*gorm.DB, error) { 112 | dialector, err := getDialector(config, dbName) 113 | if err != nil { 114 | return nil, fmt.Errorf("获取数据库驱动器失败: %v", err) 115 | } 116 | 117 | return gorm.Open(dialector, &gorm.Config{}) 118 | } 119 | 120 | // getDialector 根据数据库类型获取对应的驱动器 121 | // 参数: 122 | // - config: 应用配置 123 | // - dbName: 数据库名称 124 | // 125 | // 返回值: 126 | // - gorm.Dialector: 数据库方言 127 | // - error: 获取方言过程中的错误 128 | func getDialector(config *configs.Config, dbName string) (gorm.Dialector, error) { 129 | switch config.DBConfig.DBDialect { 130 | case DIALECT_POSTGRES: 131 | return getPostgresDialector(config, dbName), nil 132 | case DIALECT_SQLITE: 133 | return getSqliteDialector(config, dbName) 134 | case DIALECT_MYSQL: 135 | return getMySQLDialector(config, dbName), nil 136 | default: 137 | return nil, fmt.Errorf("不支持的数据库类型: %s", config.DBConfig.DBDialect) 138 | } 139 | } 140 | 141 | // getPostgresDialector 获取 PostgreSQL 驱动器 142 | // 参数: 143 | // - config: 应用配置 144 | // - dbName: 数据库名称 145 | // 146 | // 返回值: 147 | // - gorm.Dialector: PostgreSQL 方言 148 | func getPostgresDialector(config *configs.Config, dbName string) gorm.Dialector { 149 | dsn := fmt.Sprintf( 150 | "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai", 151 | config.DBConfig.DBHost, 152 | config.DBConfig.DBUser, 153 | config.DBConfig.DBPassword, 154 | dbName, 155 | config.DBConfig.DBPort, 156 | ) 157 | return postgres.Open(dsn) 158 | } 159 | 160 | // getMySQLDialector 获取 MySQL 驱动器 161 | // 参数: 162 | // - config: 应用配置 163 | // - dbName: 数据库名称 164 | // 165 | // 返回值: 166 | // - gorm.Dialector: MySQL 方言 167 | func getMySQLDialector(config *configs.Config, dbName string) gorm.Dialector { 168 | dsn := fmt.Sprintf( 169 | "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", 170 | config.DBConfig.DBUser, 171 | config.DBConfig.DBPassword, 172 | config.DBConfig.DBHost, 173 | config.DBConfig.DBPort, 174 | dbName, 175 | ) 176 | return mysql.Open(dsn) 177 | } 178 | 179 | // getSqliteDialector 获取 SQLite 驱动器并确保目录存在 180 | // 参数: 181 | // - config: 应用配置 182 | // - dbName: 数据库名称 183 | // 184 | // 返回值: 185 | // - gorm.Dialector: SQLite 方言 186 | // - error: 创建目录过程中的错误 187 | func getSqliteDialector(config *configs.Config, dbName string) (gorm.Dialector, error) { 188 | if err := os.MkdirAll(config.DBConfig.DBPath, os.ModePerm); err != nil { 189 | return nil, fmt.Errorf("创建 SQLite 数据库目录失败: %v", err) 190 | } 191 | 192 | dbPath := filepath.Join(config.DBConfig.DBPath, dbName+".db") 193 | return sqlite.Open(dbPath), nil 194 | } 195 | 196 | // ensurePostgresDBExists 确保 PostgreSQL 数据库存在,不存在则创建 197 | // 参数: 198 | // - db: 数据库连接 199 | // - dbName: 数据库名称 200 | // - dbUser: 数据库用户 201 | // 202 | // 返回值: 203 | // - error: 创建过程中的错误 204 | func ensurePostgresDBExists(db *gorm.DB, dbName, dbUser string) error { 205 | var exists bool 206 | query := "SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = ?)" 207 | if err := db.Raw(query, dbName).Scan(&exists).Error; err != nil { 208 | log.Printf("查询「%s」数据库是否存在时失败: %v", dbName, err) 209 | return fmt.Errorf("查询「%s」数据库是否存在时失败: %v", dbName, err) 210 | } 211 | 212 | if !exists { 213 | log.Printf("「%s」数据库不存在,正在创建...", dbName) 214 | global.SysLog.Infof("「%s」数据库不存在,正在创建...", dbName) 215 | 216 | createSQL := fmt.Sprintf("CREATE DATABASE %s ENCODING 'UTF8' OWNER %s", dbName, dbUser) 217 | if err := db.Exec(createSQL).Error; err != nil { 218 | return fmt.Errorf("创建「%s」数据库失败: %v", dbName, err) 219 | } 220 | } 221 | return nil 222 | } 223 | 224 | // ensureMySQLDBExists 确保 MySQL 数据库存在,不存在则创建 225 | // 参数: 226 | // - db: 数据库连接 227 | // - dbName: 数据库名称 228 | // 229 | // 返回值: 230 | // - error: 创建过程中的错误 231 | func ensureMySQLDBExists(db *gorm.DB, dbName string) error { 232 | var count int64 233 | query := "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = ?" 234 | if err := db.Raw(query, dbName).Scan(&count).Error; err != nil { 235 | log.Printf("查询「%s」数据库是否存在时失败: %v", dbName, err) 236 | return fmt.Errorf("查询「%s」数据库是否存在时失败: %v", dbName, err) 237 | } 238 | 239 | if count == 0 { 240 | log.Printf("「%s」数据库不存在,正在创建...", dbName) 241 | global.SysLog.Infof("「%s」数据库不存在,正在创建...", dbName) 242 | createSQL := fmt.Sprintf("CREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci", dbName) 243 | if err := db.Exec(createSQL).Error; err != nil { 244 | return fmt.Errorf("创建「%s」数据库失败: %v", dbName, err) 245 | } 246 | } 247 | return nil 248 | } 249 | -------------------------------------------------------------------------------- /internal/error/README.md: -------------------------------------------------------------------------------- 1 | 异常处理组件 2 | -------------------------------------------------------------------------------- /internal/error/biz_error.go: -------------------------------------------------------------------------------- 1 | // Package biz_err 提供业务错误码和错误信息定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package biz_err 5 | 6 | // Err 业务错误结构体 7 | type Err struct { 8 | Code int `json:"code"` // 错误码 9 | Msg string `json:"msg"` // 错误信息 10 | } 11 | 12 | // Error 实现 error 接口的方法 13 | // 返回值: 14 | // - string: 错误信息 15 | func (b *Err) Error() string { 16 | return b.Msg 17 | } 18 | 19 | // New 创建一个 Err 实例,基于提供的错误代码和可选的错误信息 20 | // 参数: 21 | // - code: 错误码 22 | // - msg: 可选的错误信息,不提供则使用错误码对应的默认信息 23 | // 24 | // 返回值: 25 | // - *Err: 业务错误实例 26 | func New(code int, msg ...string) *Err { 27 | message := "" 28 | 29 | if len(msg) <= 0 { 30 | message = GetMessage(code) 31 | } else { 32 | message = msg[0] 33 | } 34 | 35 | return &Err{ 36 | Code: code, 37 | Msg: message, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/error/err_code.go: -------------------------------------------------------------------------------- 1 | // Package biz_err 提供业务错误码和错误信息定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package biz_err 5 | 6 | // 错误码常量定义 7 | const ( 8 | SUCCESS = 200 9 | UNKNOWN_ERR = 00000 10 | SERVER_ERR = 10000 11 | BAD_REQUEST = 20000 12 | 13 | SEND_IMG_VERIFICATION_CODE_FAIL = 10001 14 | SEND_EMAIL_VERIFICATION_CODE_FAIL = 10002 15 | ) 16 | 17 | // CodeMsg 错误码对应的错误信息 18 | var CodeMsg = map[int]string{ 19 | SUCCESS: "请求成功", 20 | UNKNOWN_ERR: "未知业务异常", 21 | SERVER_ERR: "服务端异常", 22 | BAD_REQUEST: "错误请求", 23 | 24 | SEND_IMG_VERIFICATION_CODE_FAIL: "图形验证码发送失败", 25 | SEND_EMAIL_VERIFICATION_CODE_FAIL: "邮箱验证码发送失败", 26 | } 27 | 28 | // GetMessage 根据错误码获取对应的错误信息 29 | // 参数: 30 | // - code: 错误码 31 | // 32 | // 返回值: 33 | // - string: 错误信息 34 | func GetMessage(code int) string { 35 | if msg, ok := CodeMsg[code]; ok { 36 | return msg 37 | } 38 | return CodeMsg[UNKNOWN_ERR] 39 | } 40 | -------------------------------------------------------------------------------- /internal/global/README.md: -------------------------------------------------------------------------------- 1 | 全局组件 2 | -------------------------------------------------------------------------------- /internal/global/global.go: -------------------------------------------------------------------------------- 1 | // Package global 提供全局变量和对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package global 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/redis/go-redis/v9" 10 | "github.com/sirupsen/logrus" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | // 数据库相关全局变量 15 | var ( 16 | DB *gorm.DB // 全局 db 对象,用于数据库操作 17 | RedisClient *redis.Client // 全局 redis 客户端对象,用于缓存操作 18 | ) 19 | 20 | // 日志相关全局变量 21 | var ( 22 | SysLog *logrus.Logger // 全局系统级日志对象,用于记录系统级日志 23 | BizLog *logrus.Entry // 全局业务级日志对象,用于记录业务级日志 24 | LogFile io.Closer // 全局日志文件对象,用于日志文件资源管理 25 | ) 26 | -------------------------------------------------------------------------------- /internal/logger/README.md: -------------------------------------------------------------------------------- 1 | # 统一日志组件 (Unified Logging Component) 2 | 3 | ## 简介 4 | 5 | 统一日志组件提供了应用程序的日志记录功能,支持日志轮转、级别控制和格式化输出。基于 logrus 库实现,提供结构化 JSON 格式的日志输出,便于后续日志分析和处理。 6 | 7 | ## 核心功能 8 | 9 | - **结构化日志**: 使用 JSON 格式输出日志,便于解析和分析 10 | - **日志级别控制**: 支持多种日志级别 (Panic, Fatal, Error, Warn, Info, Debug, Trace) 11 | - **日志轮转**: 按照时间自动切割日志文件,防止单个日志文件过大 12 | - **文件保留策略**: 自动清理超过保留期限的历史日志文件 13 | - **优雅降级**: 日志系统故障时会自动降级到标准输出 14 | 15 | ## 配置项 16 | 17 | 日志组件从应用配置中读取以下参数: 18 | 19 | - **LogFilePath**: 日志文件存储路径 20 | - **LogFileName**: 日志文件名称 21 | - **LogLevel**: 日志记录级别 22 | - **LogTimestampFmt**: 时间戳格式 23 | - **LogMaxAge**: 日志文件最大保留时间(小时) 24 | - **LogRotationTime**: 日志轮转时间间隔(小时) 25 | 26 | ## 文件权限说明 27 | 28 | **0755**: `Unix/Linux` 系统中常用的文件权限表示法。使用八进制(octal)数字系统来表示文件或目录的权限。每个数字表示一组权限,分别对应用户、用户组和其他人 29 | 30 | - 第一个数字(0):表示文件类型。对于常规文件,通常为 0 31 | - 第二个数字(7):表示文件所有者(用户)的权限 (这里 7 表示文件所有者拥有读(4)、写(2)和执行(1)的权限,合计 4 + 2 + 1 = 7) 32 | - 第三个数字(5):表示与文件所有者同组的用户组的权限 (这里 5 表示用户组和其他用户拥有读(4)和执行(1)的权限,合计 4 + 1 = 5) 33 | - 第四个数字(5):表示其他用户的权限 34 | - 因此 0755 表示: 35 | - 文件所有者可以读、写、执行。 36 | - 用户组成员可以读、执行。 37 | - 其他用户可以读、执行。 38 | 39 | ## 使用方式 40 | 41 | 通过全局变量 `global.SysLog` 在应用的任何位置使用日志功能: 42 | 43 | ```go 44 | // 记录信息日志 45 | global.SysLog.Info("应用启动成功") 46 | 47 | // 记录带字段的错误日志 48 | global.SysLog.WithFields(logrus.Fields{ 49 | "user": "admin", 50 | "action": "login", 51 | }).Error("登录失败: 密码错误") 52 | ``` 53 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | // Package logger 提供应用程序日志功能的初始化和配置 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package logger 5 | 6 | import ( 7 | "io" 8 | "log" 9 | "os" 10 | "path" 11 | "time" 12 | 13 | rotatelogs "github.com/lestrrat-go/file-rotatelogs" 14 | "github.com/rifflock/lfshook" 15 | "github.com/sirupsen/logrus" 16 | 17 | "jank.com/jank_blog/configs" 18 | "jank.com/jank_blog/internal/global" 19 | ) 20 | 21 | // New 初始化日志组件 22 | func New() { 23 | cfg, err := configs.LoadConfig() 24 | if err != nil { 25 | log.Fatalf("初始化日志组件时加载配置失败: %v", err) 26 | return 27 | } 28 | 29 | // 设置路径 30 | logFilePath := cfg.LogConfig.LogFilePath 31 | logFileName := cfg.LogConfig.LogFileName 32 | fileName := path.Join(logFilePath, logFileName) 33 | _ = os.MkdirAll(logFilePath, 0755) 34 | 35 | // 初始化 logger 36 | formatter := &logrus.JSONFormatter{TimestampFormat: cfg.LogConfig.LogTimestampFmt} 37 | logger := logrus.New() 38 | logger.SetFormatter(formatter) 39 | logger.SetOutput(io.Discard) 40 | 41 | // 设置日志级别 42 | logLevel, err := logrus.ParseLevel(cfg.LogConfig.LogLevel) 43 | switch err { 44 | case nil: 45 | logger.SetLevel(logLevel) 46 | default: 47 | logger.SetLevel(logrus.InfoLevel) 48 | } 49 | 50 | // 配置日志轮转 51 | writer, err := rotatelogs.New( 52 | path.Join(logFilePath, "%Y%m%d.log"), 53 | rotatelogs.WithLinkName(fileName), 54 | rotatelogs.WithMaxAge(time.Duration(cfg.LogConfig.LogMaxAge)*time.Hour), 55 | rotatelogs.WithRotationTime(time.Duration(cfg.LogConfig.LogRotationTime)*time.Hour), 56 | ) 57 | 58 | switch { 59 | case err != nil: 60 | log.Printf("配置日志轮转失败: %v,使用标准文件", err) 61 | fileHandle, fileErr := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755) 62 | 63 | switch { 64 | case fileErr != nil: 65 | log.Printf("创建日志文件失败: %v,使用标准输出", fileErr) 66 | logger.SetOutput(os.Stdout) 67 | global.LogFile = nil 68 | default: 69 | logger.SetOutput(fileHandle) 70 | global.LogFile = fileHandle 71 | } 72 | default: 73 | allLevels := []logrus.Level{ 74 | logrus.PanicLevel, 75 | logrus.FatalLevel, 76 | logrus.ErrorLevel, 77 | logrus.WarnLevel, 78 | logrus.InfoLevel, 79 | logrus.DebugLevel, 80 | logrus.TraceLevel, 81 | } 82 | 83 | writeMap := make(lfshook.WriterMap, len(allLevels)) 84 | for _, level := range allLevels { 85 | writeMap[level] = writer 86 | } 87 | 88 | logger.AddHook(lfshook.NewHook(writeMap, formatter)) 89 | 90 | global.LogFile = writer 91 | } 92 | 93 | global.SysLog = logger 94 | } 95 | -------------------------------------------------------------------------------- /internal/middleware/README.md: -------------------------------------------------------------------------------- 1 | # 中间件组件 (Middleware Components) 2 | 3 | ## 简介 4 | 5 | 中间件组件用于处理 HTTP 请求的预处理和后处理,提供了一系列可复用的功能模块,如认证、日志记录、安全防护等。中间件按照注册顺序依次执行,形成请求处理管道。 6 | 7 | ## 中间件目录结构 8 | 9 | - **auth/**: 认证相关中间件,包含 JWT 认证实现,用于保护需要登录的 API 接口 10 | - **cors/**: 跨域资源共享(CORS)中间件,处理跨域请求的安全访问策略 11 | - **error/**: 全局错误处理中间件,统一处理和格式化 API 错误响应 12 | - **logger/**: 日志记录中间件,记录 HTTP 请求和响应信息 13 | - **recover/**: 全局异常恢复中间件,防止服务因未捕获的异常而崩溃 14 | - **secure/**: 安全相关中间件,包含 XSS 防御和 CSRF 防御功能 15 | - **swagger/**: Swagger API 文档中间件,提供 API 文档访问支持 16 | 17 | ## 核心功能 18 | 19 | - 请求与响应的预处理和后处理 20 | - 全局错误处理和异常恢复 21 | - 访问认证与鉴权 22 | - 跨域资源共享控制 23 | - 安全防御(XSS、CSRF) 24 | - 请求日志记录 25 | - API 文档支持 26 | 27 | ## 使用方式 28 | 29 | 中间件在主程序启动时通过 `middleware.New()` 函数统一注册到 Echo 框架: 30 | 31 | ```go 32 | // 初始化并注册所有中间件 33 | middleware.New(app) 34 | ``` 35 | 36 | 中间件执行顺序: 37 | 38 | 1. 全局错误处理 39 | 2. CORS 跨域资源共享 40 | 3. 请求 ID 生成 41 | 4. 日志记录 42 | 5. XSS 防御 43 | 6. CSRF 防御 44 | 7. 异常恢复 45 | 8. Swagger 文档支持 46 | 47 | 特定路由的认证中间件可以单独应用: 48 | 49 | ```go 50 | // 对特定路由组应用 JWT 认证 51 | api := app.Group("/api") 52 | api.Use(auth_middleware.AuthMiddleware()) 53 | ``` 54 | -------------------------------------------------------------------------------- /internal/middleware/auth/README.md: -------------------------------------------------------------------------------- 1 | JWT 身份验证中间件 2 | -------------------------------------------------------------------------------- /internal/middleware/auth/jwt_authentication.go: -------------------------------------------------------------------------------- 1 | // Package auth_middleware 提供JWT认证相关中间件 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package auth_middleware 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/labstack/echo/v4" 12 | 13 | "jank.com/jank_blog/internal/global" 14 | "jank.com/jank_blog/internal/utils" 15 | ) 16 | 17 | // JWTConfig 定义了 Token 相关的配置 18 | type JWTConfig struct { 19 | Authorization string // 认证头名称 20 | TokenPrefix string // Token前缀 21 | RefreshToken string // 刷新令牌头名称 22 | UserCache string // 用户缓存键前缀 23 | } 24 | 25 | // DefaultJWTConfig 默认配置 26 | var DefaultJWTConfig = JWTConfig{ 27 | Authorization: "Authorization", 28 | TokenPrefix: "Bearer ", 29 | RefreshToken: "REFRESH_TOKEN", 30 | UserCache: "USER_CACHE", 31 | } 32 | 33 | // AuthMiddleware 处理 JWT 认证中间件 34 | // 返回值: 35 | // - echo.MiddlewareFunc: Echo 框架中间件函数 36 | func AuthMiddleware() echo.MiddlewareFunc { 37 | return func(next echo.HandlerFunc) echo.HandlerFunc { 38 | return func(c echo.Context) error { 39 | // 从请求头中提取 Access Token 40 | authHeader := c.Request().Header.Get(DefaultJWTConfig.Authorization) 41 | if authHeader == "" { 42 | return echo.NewHTTPError(http.StatusUnauthorized, "缺少 Authorization 请求头") 43 | } 44 | tokenString := strings.TrimPrefix(authHeader, DefaultJWTConfig.TokenPrefix) 45 | 46 | // 验证 JWT Token;若验证失败则尝试使用 Refresh Token 刷新 47 | _, err := utils.ValidateJWTToken(tokenString, false) 48 | if err != nil { 49 | refreshHeader := c.Request().Header.Get(DefaultJWTConfig.RefreshToken) 50 | if refreshHeader == "" { 51 | return echo.NewHTTPError(http.StatusUnauthorized, "无效 Access Token,请重新登录") 52 | } 53 | refreshTokenString := strings.TrimPrefix(refreshHeader, DefaultJWTConfig.TokenPrefix) 54 | newTokens, refreshErr := utils.RefreshTokenLogic(refreshTokenString) 55 | if refreshErr != nil { 56 | return echo.NewHTTPError(http.StatusUnauthorized, "无效 Access 和 Refresh Token,请重新登录") 57 | } 58 | c.Response().Header().Set(DefaultJWTConfig.Authorization, DefaultJWTConfig.TokenPrefix+newTokens["accessToken"]) 59 | c.Response().Header().Set(DefaultJWTConfig.RefreshToken, DefaultJWTConfig.TokenPrefix+newTokens["refreshToken"]) 60 | tokenString = newTokens["accessToken"] 61 | } 62 | 63 | // 从 Token 中解析 accountID 64 | accountID, err := utils.ParseAccountAndRoleIDFromJWT(tokenString) 65 | if err != nil { 66 | return echo.NewHTTPError(http.StatusUnauthorized, "无效的 Access Token,请重新登录") 67 | } 68 | 69 | sessionCacheKey := fmt.Sprintf("%s:%d", DefaultJWTConfig.UserCache, accountID) 70 | if sessionVal, err := global.RedisClient.Get(c.Request().Context(), sessionCacheKey).Result(); err != nil || sessionVal == "" { 71 | return echo.NewHTTPError(http.StatusUnauthorized, "无效会话,请重新登录") 72 | } 73 | 74 | return next(c) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/middleware/cors/README.md: -------------------------------------------------------------------------------- 1 | CORS 跨域请求中间件 2 | -------------------------------------------------------------------------------- /internal/middleware/cors/cors.go: -------------------------------------------------------------------------------- 1 | // Package cors_middleware 提供跨域资源共享中间件 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package cors_middleware 5 | 6 | import ( 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/labstack/echo/v4" 11 | 12 | "jank.com/jank_blog/internal/global" 13 | ) 14 | 15 | // InitCORS 初始化 CORS 中间件 16 | // 返回值: 17 | // - echo.MiddlewareFunc: Echo 框架中间件函数 18 | func InitCORS() echo.MiddlewareFunc { 19 | return corsWithConfig(defaultCORSConfig()) 20 | } 21 | 22 | // CORSConfig 定义 CORS 中间件的配置 23 | type corsConfig struct { 24 | AllowedOrigins []string // 允许的源 25 | AllowedMethods []string // 允许的方法 26 | AllowedHeaders []string // 允许的头部 27 | AllowCredentials bool // 是否允许携带证书 28 | } 29 | 30 | // DefaultCORSConfig 提供了默认的 CORS 配置 31 | // 返回值: 32 | // - corsConfig: CORS 配置 33 | func defaultCORSConfig() corsConfig { 34 | return corsConfig{ 35 | AllowedOrigins: []string{"*"}, // 默认允许所有域名 36 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // 默认允许的请求方法 37 | AllowedHeaders: []string{"Content-Type", "Authorization", "X-Client-Info", "X-Client-Version", "X-Client-Data", "X-Request-Id"}, // 默认允许的请求头 38 | AllowCredentials: false, // 默认不允许携带证书 39 | } 40 | } 41 | 42 | // corsWithConfig 返回一个 CORS 中间件函数 43 | // 参数: 44 | // - config: CORS配置 45 | // 46 | // 返回值: 47 | // - echo.MiddlewareFunc: Echo 框架中间件函数 48 | func corsWithConfig(config corsConfig) echo.MiddlewareFunc { 49 | return func(next echo.HandlerFunc) echo.HandlerFunc { 50 | return func(c echo.Context) error { 51 | c.Response().Header().Set("Access-Control-Allow-Origin", strings.Join(config.AllowedOrigins, ",")) 52 | c.Response().Header().Set("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ",")) 53 | c.Response().Header().Set("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ",")) 54 | 55 | if config.AllowCredentials { 56 | c.Response().Header().Set("Access-Control-Allow-Credentials", "true") 57 | } 58 | 59 | // 处理预检请求,缓存预检请求结果 24 小时 60 | if c.Request().Method == "OPTIONS" { 61 | c.Set("Access-Control-Max-Age", "86400") 62 | return c.NoContent(http.StatusNoContent) 63 | } 64 | 65 | // 记录 CORS 请求 66 | if global.BizLog != nil { 67 | global.BizLog.Info("CORS request", 68 | "method", c.Request().Method, 69 | "path", c.Request().URL.Path, 70 | "origin", c.Request().Header.Get("Origin"), 71 | ) 72 | } 73 | 74 | return next(c) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/middleware/error/README.md: -------------------------------------------------------------------------------- 1 | 全局错误处理中间件 2 | -------------------------------------------------------------------------------- /internal/middleware/error/error_handler.go: -------------------------------------------------------------------------------- 1 | // Package error_middleware 提供全局错误处理中间件 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package error_middleware 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | 11 | "github.com/labstack/echo/v4" 12 | 13 | bizerr "jank.com/jank_blog/internal/error" 14 | "jank.com/jank_blog/internal/global" 15 | "jank.com/jank_blog/pkg/vo" 16 | ) 17 | 18 | // InitError 全局错误处理中间件 19 | // 返回值: 20 | // - echo.MiddlewareFunc: Echo 框架中间件函数 21 | func InitError() echo.MiddlewareFunc { 22 | return func(next echo.HandlerFunc) echo.HandlerFunc { 23 | return func(c echo.Context) error { 24 | if err := next(c); err != nil { 25 | code := http.StatusInternalServerError 26 | var e *bizerr.Err 27 | if errors.As(err, &e) { 28 | code = e.Code 29 | } 30 | 31 | // 捕获请求信息:请求方法、请求URI、客户端IP、User-Agent 32 | requestMethod := c.Request().Method 33 | requestURI := c.Request().RequestURI 34 | clientIP := c.Request().RemoteAddr 35 | userAgent := c.Request().UserAgent() 36 | 37 | // 构建日志消息 38 | logMessage := fmt.Sprintf("请求异常: %v | Method: %s | URI: %s | IP: %s | User-Agent: %s", err, requestMethod, requestURI, clientIP, userAgent) 39 | global.SysLog.Error(logMessage) 40 | 41 | return c.JSON(code, vo.Fail(c, nil, bizerr.New(code, err.Error()))) 42 | } 43 | return nil 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/middleware/logger/logger.go: -------------------------------------------------------------------------------- 1 | // Package logger_middleware 提供HTTP请求日志记录中间件 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package logger_middleware 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "io" 10 | "net/http" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/labstack/echo/v4" 16 | "github.com/labstack/echo/v4/middleware" 17 | "github.com/sirupsen/logrus" 18 | 19 | biz_err "jank.com/jank_blog/internal/error" 20 | "jank.com/jank_blog/internal/global" 21 | ) 22 | 23 | const CTX_KEY = "logger_processed" 24 | 25 | // InitLogger 返回 HTTP 请求日志中间件,使用默认配置 26 | func InitLogger() echo.MiddlewareFunc { 27 | return loggerWithConfig(defaultConfig) 28 | } 29 | 30 | // 日志字段键定义 31 | const ( 32 | LOG_KEY_REQ_ID = "requestId" // 请求ID 33 | LOG_KEY_METHOD = "method" // HTTP方法 34 | LOG_KEY_URI = "uri" // 请求路径 35 | LOG_KEY_IP = "ip" // 客户端IP 36 | LOG_KEY_HOST = "host" // 主机名 37 | LOG_KEY_UA = "ua" // User-Agent 38 | LOG_KEY_STATUS = "status" // 状态码 39 | LOG_KEY_BYTES = "bytes" // 响应大小 40 | LOG_KEY_LATENCY = "latency" // 响应时间(毫秒) 41 | LOG_KEY_BODY = "body" // 请求体 42 | LOG_KEY_ERROR = "error" // 错误信息 43 | ) 44 | 45 | // loggerConfig 日志中间件配置 46 | type loggerConfig struct { 47 | Skipper func(echo.Context) bool // 跳过中间件的条件 48 | LogRequestBody bool // 是否记录请求体 49 | LogResponseSize bool // 是否记录响应大小 50 | MaskSensitive bool // 是否屏蔽敏感字段 51 | MaxBodySize int // 最大记录请求体大小 52 | MaskValue string // 敏感信息掩码值 53 | SensitiveFields map[string]struct{} // 敏感字段列表 54 | } 55 | 56 | // 默认日志配置 57 | var defaultConfig = loggerConfig{ 58 | Skipper: func(c echo.Context) bool { return false }, // 默认不跳过 59 | LogRequestBody: true, // 默认记录请求体 60 | LogResponseSize: true, // 默认记录响应大小 61 | MaskSensitive: true, // 默认屏蔽敏感信息 62 | MaxBodySize: 20 * 1024, // 默认最大10KB 63 | MaskValue: "********", // 默认掩码 64 | SensitiveFields: map[string]struct{}{ // 默认敏感字段 65 | "password": {}, "token": {}, "secret": {}, 66 | "auth": {}, "key": {}, "credential": {}, 67 | }, 68 | } 69 | 70 | // sizeWriter 用于跟踪 HTTP 响应大小 71 | type sizeWriter struct { 72 | http.ResponseWriter 73 | size int 74 | } 75 | 76 | // Write 实现 ResponseWriter 接口,记录写入的字节数 77 | func (w *sizeWriter) Write(b []byte) (int, error) { 78 | n, err := w.ResponseWriter.Write(b) 79 | w.size += n 80 | return n, err 81 | } 82 | 83 | // writerPool 响应写入器对象池 84 | var writerPool = sync.Pool{New: func() interface{} { return &sizeWriter{} }} 85 | 86 | // loggerWithConfig 返回带自定义配置的日志中间件 87 | func loggerWithConfig(config loggerConfig) echo.MiddlewareFunc { 88 | if config.Skipper == nil { 89 | config.Skipper = defaultConfig.Skipper 90 | } 91 | if config.MaxBodySize <= 0 { 92 | config.MaxBodySize = defaultConfig.MaxBodySize 93 | } 94 | if config.MaskValue == "" { 95 | config.MaskValue = defaultConfig.MaskValue 96 | } 97 | if len(config.SensitiveFields) == 0 && config.MaskSensitive { 98 | config.SensitiveFields = defaultConfig.SensitiveFields 99 | } 100 | 101 | return func(next echo.HandlerFunc) echo.HandlerFunc { 102 | return func(c echo.Context) error { 103 | if config.Skipper(c) { 104 | return next(c) 105 | } 106 | 107 | // 避免重复处理 108 | if _, ok := c.Get(CTX_KEY).(bool); ok { 109 | return next(c) 110 | } 111 | c.Set(CTX_KEY, true) 112 | 113 | // 获取请求信息 114 | req := c.Request() 115 | reqID := c.Response().Header().Get(echo.HeaderXRequestID) 116 | if reqID == "" { 117 | reqID = middleware.DefaultRequestIDConfig.Generator() 118 | c.Response().Header().Set(echo.HeaderXRequestID, reqID) 119 | } 120 | 121 | // 初始化日志字段 122 | fields := logrus.Fields{ 123 | LOG_KEY_REQ_ID: reqID, 124 | LOG_KEY_METHOD: req.Method, 125 | LOG_KEY_URI: req.RequestURI, 126 | LOG_KEY_IP: c.RealIP(), 127 | LOG_KEY_HOST: req.Host, 128 | LOG_KEY_UA: req.UserAgent(), 129 | } 130 | 131 | // 处理请求体 132 | if config.LogRequestBody && req.Body != nil && req.ContentLength > 0 && req.ContentLength < int64(config.MaxBodySize) { 133 | if body, _ := io.ReadAll(io.LimitReader(req.Body, int64(config.MaxBodySize))); len(body) > 0 { 134 | req.Body.Close() 135 | req.Body = io.NopCloser(bytes.NewReader(body)) 136 | 137 | // 处理 JSON 请求体 138 | if len(body) > 2 && body[0] == '{' { 139 | var data map[string]interface{} 140 | if json.Unmarshal(body, &data) == nil { 141 | // 屏蔽敏感数据 142 | if config.MaskSensitive { 143 | var maskData func(map[string]interface{}) 144 | maskData = func(data map[string]interface{}) { 145 | for k, v := range data { 146 | kl := strings.ToLower(k) 147 | for s := range config.SensitiveFields { 148 | if strings.Contains(kl, s) { 149 | data[k] = config.MaskValue 150 | break 151 | } 152 | } 153 | if m, ok := v.(map[string]interface{}); ok { 154 | maskData(m) 155 | } 156 | } 157 | } 158 | maskData(data) 159 | } 160 | 161 | if j, err := json.Marshal(data); err == nil { 162 | fields[LOG_KEY_BODY] = string(j) 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | // 设置响应跟踪 170 | var sw *sizeWriter 171 | if config.LogResponseSize { 172 | sw = writerPool.Get().(*sizeWriter) 173 | sw.ResponseWriter = c.Response().Writer 174 | sw.size = 0 175 | c.Response().Writer = sw 176 | defer writerPool.Put(sw) 177 | } 178 | 179 | // 执行请求处理 180 | start := time.Now() 181 | err := next(c) 182 | latency := time.Since(start) 183 | 184 | // 记录响应信息 185 | status := c.Response().Status 186 | fields[LOG_KEY_STATUS] = status 187 | fields[LOG_KEY_LATENCY] = float64(latency.Nanoseconds()) / 1e6 188 | 189 | if config.LogResponseSize && sw != nil { 190 | fields[LOG_KEY_BYTES] = sw.size 191 | } 192 | if err != nil { 193 | fields[LOG_KEY_ERROR] = err.Error() 194 | } 195 | 196 | log := global.SysLog.WithFields(fields) 197 | switch { 198 | case status >= 500: 199 | log.Error(biz_err.GetMessage(biz_err.SERVER_ERR)) 200 | case status >= 400: 201 | log.Warn(biz_err.GetMessage(biz_err.BAD_REQUEST)) 202 | default: 203 | log.Info(biz_err.GetMessage(biz_err.SUCCESS)) 204 | } 205 | 206 | return err 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /internal/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | // Package middleware 提供中间件集成和初始化功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package middleware 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | "github.com/labstack/echo/v4/middleware" 9 | 10 | "jank.com/jank_blog/configs" 11 | "jank.com/jank_blog/internal/global" 12 | cors_middleware "jank.com/jank_blog/internal/middleware/cors" 13 | error_middleware "jank.com/jank_blog/internal/middleware/error" 14 | logger_middleware "jank.com/jank_blog/internal/middleware/logger" 15 | recover_middleware "jank.com/jank_blog/internal/middleware/recover" 16 | secure_middleware "jank.com/jank_blog/internal/middleware/secure" 17 | swagger_middleware "jank.com/jank_blog/internal/middleware/swagger" 18 | ) 19 | 20 | // New 初始化并注册所有中间件 21 | // 参数: 22 | // - app: Echo 实例 23 | func New(app *echo.Echo) { 24 | // 设置全局错误处理 25 | app.Use(error_middleware.InitError()) 26 | // 配置 CORS 中间件 27 | app.Use(cors_middleware.InitCORS()) 28 | // 全局请求 ID 中间件 29 | app.Use(middleware.RequestID()) 30 | // 日志中间件 31 | app.Use(logger_middleware.InitLogger()) 32 | // 配置 xss 防御中间件 33 | app.Use(secure_middleware.InitXss()) 34 | // 配置 csrf 防御中间件 35 | app.Use(secure_middleware.InitCSRF()) 36 | // 全局异常恢复中间件 37 | app.Use(recover_middleware.InitRecover()) 38 | 39 | // Swagger中间件初始化 40 | initSwagger(app) 41 | } 42 | 43 | // initSwagger 根据配置初始化 Swagger 文档中间件 44 | // 参数: 45 | // - app: Echo实例 46 | func initSwagger(app *echo.Echo) { 47 | cfg, err := configs.LoadConfig() 48 | if err != nil { 49 | global.SysLog.Errorf("加载 Swagger 配置失败: %v", err) 50 | return 51 | } 52 | 53 | switch cfg.SwaggerConfig.SwaggerEnabled { 54 | case "true": 55 | app.Use(swagger_middleware.InitSwagger()) 56 | global.SysLog.Info("Swagger 已启用") 57 | default: 58 | global.SysLog.Info("Swagger 已禁用") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/middleware/recover/README.md: -------------------------------------------------------------------------------- 1 | 异常恢复中间件 2 | -------------------------------------------------------------------------------- /internal/middleware/recover/recover.go: -------------------------------------------------------------------------------- 1 | // Package recover_middleware 提供全局异常恢复中间件 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package recover_middleware 5 | 6 | import ( 7 | "runtime" 8 | 9 | "github.com/labstack/echo/v4" 10 | "github.com/labstack/echo/v4/middleware" 11 | 12 | "jank.com/jank_blog/internal/global" 13 | ) 14 | 15 | // InitRecover 初始化全局异常恢复中间件 16 | // 返回值: 17 | // - echo.MiddlewareFunc: Echo 框架中间件函数 18 | func InitRecover() echo.MiddlewareFunc { 19 | return middleware.RecoverWithConfig(middleware.RecoverConfig{ 20 | StackSize: 4096, // 堆栈大小 21 | LogErrorFunc: func(c echo.Context, err error, stack []byte) error { 22 | stackSize := 4096 23 | var buf []byte 24 | for { 25 | buf = make([]byte, stackSize) 26 | n := runtime.Stack(buf, false) 27 | if n < stackSize { 28 | buf = buf[:n] 29 | break 30 | } 31 | stackSize *= 2 32 | } 33 | 34 | // 将完整的堆栈轨迹信息记录到日志 35 | global.SysLog.WithFields(map[string]interface{}{ 36 | "stack_trace": string(buf), 37 | }).Errorf("发生运行时异常: %v", err) 38 | return nil 39 | }, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /internal/middleware/secure/README.md: -------------------------------------------------------------------------------- 1 | 各类安全中间件 2 | -------------------------------------------------------------------------------- /internal/middleware/secure/csrf.go: -------------------------------------------------------------------------------- 1 | // Package secure_middleware 提供安全相关中间件 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package secure_middleware 5 | 6 | import ( 7 | "crypto/rand" 8 | "encoding/base64" 9 | "errors" 10 | "net/http" 11 | "strings" 12 | "time" 13 | 14 | "github.com/labstack/echo/v4" 15 | ) 16 | 17 | // InitCSRF 初始化 CSRF 中间件,使用默认配置 18 | // 返回值: 19 | // - echo.MiddlewareFunc: Echo 框架中间件函数 20 | func InitCSRF() echo.MiddlewareFunc { 21 | return csrfWithConfig(defaultCSRFConfig) 22 | } 23 | 24 | // csrfConfig 定义了 CSRF 中间件的配置 25 | type csrfConfig struct { 26 | Skipper func(echo.Context) bool // 用于跳过中间件的配置 27 | TokenLength uint8 // Token 的长度 28 | TokenLookup string // Token 查找方式,默认 "header:X-CSRF-Token" 29 | ContextKey string // 上下文存储 CSRF Token 的键 30 | CookieName string // Cookie 名称 31 | CookiePath string // Cookie 路径 32 | CookieDomain string // Cookie 域 33 | CookieSecure bool // 是否启用 Secure Cookie 34 | CookieHTTPOnly bool // 是否启用 HttpOnly Cookie 35 | CookieSameSite http.SameSite // Cookie 的 SameSite 设置 36 | CookieMaxAge int // Cookie 有效期,单位为秒 37 | } 38 | 39 | // defaultCSRFConfig 提供默认的 CSRF 配置 40 | var defaultCSRFConfig = csrfConfig{ 41 | Skipper: func(c echo.Context) bool { return false }, 42 | TokenLength: 32, // Token 默认长度 32 字节 43 | TokenLookup: "header:" + echo.HeaderXCSRFToken, // 默认从 Header 查找 X-CSRF-Token 44 | ContextKey: "csrf", // 上下文中的 CSRF Token 键 45 | CookieName: "_csrf", // 默认 CSRF Cookie 名称 46 | CookiePath: "/", // Cookie 默认路径 47 | CookieDomain: "", // 默认不设置 Cookie 域 48 | CookieSecure: false, // 默认不开启 Secure 49 | CookieHTTPOnly: true, // 默认启用 HttpOnly 50 | CookieSameSite: http.SameSiteLaxMode, // 默认 SameSite 设置为 Lax 51 | CookieMaxAge: 86400, // Cookie 默认 24 小时有效期 52 | } 53 | 54 | // csrfWithConfig 使用传入的配置生成 CSRF 中间件 55 | // 参数: 56 | // - config: CSRF 配置 57 | // 58 | // 返回值: 59 | // - echo.MiddlewareFunc: Echo 框架中间件函数 60 | func csrfWithConfig(config csrfConfig) echo.MiddlewareFunc { 61 | return func(next echo.HandlerFunc) echo.HandlerFunc { 62 | return func(c echo.Context) error { 63 | if config.Skipper(c) { 64 | return next(c) 65 | } 66 | 67 | token, err := getTokenFromRequest(c, config.TokenLookup) 68 | if err != nil || token == "" { 69 | token = generateCSRFToken(config.TokenLength) 70 | setCSRFCookie(c, config, token) 71 | } else { 72 | csrfCookie, err := c.Cookie(config.CookieName) 73 | if err != nil || csrfCookie.Value != token { 74 | return echo.NewHTTPError(http.StatusForbidden, "CSRF token 验证失败") 75 | } 76 | } 77 | 78 | c.Set(config.ContextKey, token) 79 | 80 | return next(c) 81 | } 82 | } 83 | } 84 | 85 | // getTokenFromRequest 从请求中获取 CSRF token 86 | // 参数: 87 | // - c: Echo 上下文 88 | // - lookup: Token 查找方式 89 | // 90 | // 返回值: 91 | // - string: CSRF Token 92 | // - error: 获取过程中的错误 93 | func getTokenFromRequest(c echo.Context, lookup string) (string, error) { 94 | parts := strings.Split(lookup, ":") 95 | if len(parts) != 2 { 96 | return "", errors.New("无效的 Token 查找方式") 97 | } 98 | switch parts[0] { 99 | case "header": 100 | return c.Request().Header.Get(parts[1]), nil 101 | case "form": 102 | return c.FormValue(parts[1]), nil 103 | case "query": 104 | return c.QueryParam(parts[1]), nil 105 | default: 106 | return "", errors.New("不支持的 Token 查找类型") 107 | } 108 | } 109 | 110 | // generateCSRFToken 生成随机的 CSRF token 111 | // 参数: 112 | // - length: Token长度 113 | // 114 | // 返回值: 115 | // - string: 生成的 CSRF Token 116 | func generateCSRFToken(length uint8) string { 117 | token := make([]byte, length) 118 | rand.Read(token) 119 | return base64.StdEncoding.EncodeToString(token) 120 | } 121 | 122 | // setCSRFCookie 设置 CSRF Token 到 Cookie 123 | // 参数: 124 | // - c: Echo 上下文 125 | // - config: CSRF 配置 126 | // - token: CSRF Token 127 | func setCSRFCookie(c echo.Context, config csrfConfig, token string) { 128 | cookie := &http.Cookie{ 129 | Name: config.CookieName, 130 | Value: token, 131 | Path: config.CookiePath, 132 | Domain: config.CookieDomain, 133 | Secure: config.CookieSecure, 134 | HttpOnly: config.CookieHTTPOnly, 135 | SameSite: config.CookieSameSite, 136 | Expires: time.Now().Add(time.Duration(config.CookieMaxAge) * time.Second), 137 | } 138 | c.SetCookie(cookie) 139 | } 140 | -------------------------------------------------------------------------------- /internal/middleware/secure/xss.go: -------------------------------------------------------------------------------- 1 | // Package secure_middleware 提供安全相关中间件 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package secure_middleware 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | // InitXss 返回一个 XSS 防护中间件,使用默认配置 13 | // 返回值: 14 | // - echo.MiddlewareFunc: Echo 框架中间件函数 15 | func InitXss() echo.MiddlewareFunc { 16 | return xssWithConfig(defaultXSSConfig) 17 | } 18 | 19 | // xssConfig 用于配置 XSS 防护中间件 20 | type xssConfig struct { 21 | Skipper func(echo.Context) bool // 用于跳过中间件的配置 22 | XSSPrevention string // X-XSS-Protection 头部配置 23 | ContentTypeNosniff string // X-Content-Type-Options 头部配置 24 | XFrameOptions string // X-Frame-Options 头部配置 25 | HSTSMaxAge int // Strict-Transport-Security 头部配置 26 | HSTSExcludeSubdomains bool // 是否排除子域名的 HSTS 配置 27 | ContentSecurityPolicy string // Content-Security-Policy 头部配置 28 | } 29 | 30 | // defaultXSSConfig 默认的 XSS 防护配置 31 | var defaultXSSConfig = xssConfig{ 32 | Skipper: func(c echo.Context) bool { return false }, // 默认不跳过 33 | XSSPrevention: "1; mode=block", // 开启 XSS 防护 34 | ContentTypeNosniff: "nosniff", // 禁止浏览器自动猜测内容类型 35 | XFrameOptions: "SAMEORIGIN", // 允许来自同一来源的嵌入式框架 36 | HSTSMaxAge: 0, // 只能通过HTTPS来访问的时间(单位秒) 37 | HSTSExcludeSubdomains: false, // 是否排除子域名的 HSTS 配置 38 | ContentSecurityPolicy: "", // Content-Security-Policy 头部配置 39 | } 40 | 41 | // xssWithConfig 返回一个 XSS 防护中间件函数 42 | // 参数: 43 | // - config: XSS 防护配置 44 | // 45 | // 返回值: 46 | // - echo.MiddlewareFunc: Echo 框架中间件函数 47 | func xssWithConfig(config xssConfig) echo.MiddlewareFunc { 48 | if config.Skipper == nil { 49 | config.Skipper = defaultXSSConfig.Skipper 50 | } 51 | 52 | return func(next echo.HandlerFunc) echo.HandlerFunc { 53 | return func(c echo.Context) error { 54 | if config.Skipper(c) { 55 | return next(c) 56 | } 57 | 58 | // 设置 X-XSS-Protection 头部 59 | c.Response().Header().Set("X-XSS-Protection", config.XSSPrevention) 60 | // 设置 X-Content-Type-Options 头部 61 | c.Response().Header().Set("X-Content-Type-Options", config.ContentTypeNosniff) 62 | // 设置 X-Frame-Options 头部 63 | c.Response().Header().Set("X-Frame-Options", config.XFrameOptions) 64 | // 设置 Strict-Transport-Security 头部 65 | if config.HSTSMaxAge > 0 { 66 | hstsHeader := "max-age=" + strconv.Itoa(config.HSTSMaxAge) 67 | if config.HSTSExcludeSubdomains { 68 | hstsHeader += "; includeSubDomains" 69 | } 70 | c.Response().Header().Set("Strict-Transport-Security", hstsHeader) 71 | } 72 | // 设置 Content-Security-Policy 头部 73 | if config.ContentSecurityPolicy != "" { 74 | c.Response().Header().Set("Content-Security-Policy", config.ContentSecurityPolicy) 75 | } 76 | 77 | return next(c) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/middleware/swagger/README.md: -------------------------------------------------------------------------------- 1 | swagger API 文档组件 2 | 3 | - 启动后,默认访问地址:http://localhost:9010/swagger/index.html 4 | 5 | > 如果不想使用 swagger,前往 `internal/middleware/middleware.go` 中注释掉 `app.Use(swagger_middleware.InitSwagger())` 函数即可。 6 | -------------------------------------------------------------------------------- /internal/middleware/swagger/swagger.go: -------------------------------------------------------------------------------- 1 | package swagger_middleware 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/labstack/echo/v4" 10 | echoSwagger "github.com/swaggo/echo-swagger" 11 | 12 | "jank.com/jank_blog/configs" 13 | "jank.com/jank_blog/docs" 14 | "jank.com/jank_blog/internal/global" 15 | ) 16 | 17 | var swaggerOnce sync.Once 18 | 19 | func InitSwagger() echo.MiddlewareFunc { 20 | initSwagger() 21 | 22 | return func(next echo.HandlerFunc) echo.HandlerFunc { 23 | return func(c echo.Context) error { 24 | if strings.HasPrefix(c.Request().URL.Path, "/swagger/") { 25 | return echoSwagger.WrapHandler(c) 26 | } 27 | return next(c) 28 | } 29 | } 30 | } 31 | 32 | // initSwagger 初始化 Swagger 配置信息 33 | func initSwagger() { 34 | swaggerOnce.Do(func() { 35 | config, err := configs.LoadConfig() 36 | if err != nil { 37 | global.SysLog.Fatalf("加载 Swagger 配置失败: %v", err) 38 | return 39 | } 40 | 41 | docs.SwaggerInfo.Title = "Jank Blog API" 42 | docs.SwaggerInfo.Description = "这是 Jank Blog 的 API 文档,适用于账户管理、用户认证、角色权限管理,文章管理,类目管理、评论管理等功能。" 43 | docs.SwaggerInfo.Version = "1.0" 44 | docs.SwaggerInfo.Host = config.SwaggerConfig.SwaggerHost 45 | if docs.SwaggerInfo.Host == "" { 46 | docs.SwaggerInfo.Host = "localhost:9010" 47 | } 48 | 49 | docs.SwaggerInfo.BasePath = "/" 50 | docs.SwaggerInfo.Schemes = []string{"http", "https"} 51 | 52 | cmd := exec.Command("swag", "init", "-g", "pkg/router/router.go") 53 | output, err := cmd.CombinedOutput() 54 | if err != nil { 55 | global.SysLog.Errorf("初始化 Swagger 文档失败,错误: %v\n输出信息: %s", err, string(output)) 56 | global.SysLog.Info("继续启动服务,但 Swagger 文档可能不可用") 57 | } else { 58 | global.SysLog.Info("成功生成 Swagger 文档") 59 | } 60 | 61 | fmt.Printf("Swagger service started on: http://%s/swagger/index.html\n", docs.SwaggerInfo.Host) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /internal/model/README.md: -------------------------------------------------------------------------------- 1 | # 业务模型 (Business Models) 2 | 3 | ## 简介 4 | 5 | 本目录包含系统的所有业务数据模型,这些模型用于数据库交互和业务逻辑处理。每个模型都映射到数据库中的一个表,并定义了表的结构和关系。 6 | 7 | ## 模型目录结构 8 | 9 | - **account/**: 用户账户相关模型,包含手机号、邮箱、密码、昵称等信息 10 | - **association/**: 模型之间的关联关系模型,如 `PostCategory` 用于处理文章与分类的多对多关系 11 | - **base/**: 基础模型类,包含所有模型共有的字段如自增 ID、创建时间(GmtCreate)、修改时间(GmtModified)、扩展字段(Ext)和逻辑删除(Deleted) 12 | - **category/**: 分类模型,支持类目名称、描述、父子关系和路径,支持树形结构 13 | - **comment/**: 评论模型,用于管理博客评论 14 | - **post/**: 博客文章模型,包含标题、图片、可见性、Markdown 内容和渲染后的 HTML 内容 15 | 16 | ## 核心功能 17 | 18 | - 定义数据库表结构和关系 19 | - 实现 ORM(对象关系映射)功能 20 | - 支持 GORM 框架的自动迁移和钩子函数(BeforeCreate, BeforeUpdate) 21 | - 提供表名自定义和字段约束 22 | - 支持 JSON 类型字段及其序列化/反序列化 23 | - 实现逻辑删除功能,避免物理删除数据 24 | 25 | ## 使用方式 26 | 27 | 所有模型通过 `GetAllModels()` 函数集中注册,用于数据库迁移和初始化: 28 | 29 | ```go 30 | // 获取所有模型用于数据库迁移 31 | models := model.GetAllModels() 32 | 33 | // 数据库自动迁移 34 | db.AutoMigrate(models...) 35 | ``` 36 | -------------------------------------------------------------------------------- /internal/model/account/README.md: -------------------------------------------------------------------------------- 1 | 账户模型 2 | -------------------------------------------------------------------------------- /internal/model/account/account.go: -------------------------------------------------------------------------------- 1 | // Package model 提供用户账户数据模型定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package model 5 | 6 | import "jank.com/jank_blog/internal/model/base" 7 | 8 | // Account 用户账户模型 9 | type Account struct { 10 | base.Base 11 | Phone string `gorm:"type:varchar(32);unique;default:null" json:"phone"` // 手机号,次登录方式 12 | Email string `gorm:"type:varchar(64);unique;not null" json:"email"` // 邮箱,主登录方式 13 | Password string `gorm:"type:varchar(255);not null" json:"password"` // 加密密码 14 | Nickname string `gorm:"type:varchar(64);not null" json:"nickname"` // 昵称 15 | Avatar string `gorm:"type:varchar(255);default:null" json:"avatar"` // 用户头像 16 | } 17 | 18 | // TableName 指定表名 19 | // 返回值: 20 | // - string: 表名 21 | func (Account) TableName() string { 22 | return "accounts" 23 | } 24 | -------------------------------------------------------------------------------- /internal/model/association/README.md: -------------------------------------------------------------------------------- 1 | # 跨模块中间表 (Cross-Module Association Tables) 2 | 3 | ## 简介 4 | 5 | 本目录包含跨模块之间的关联关系模型,用于表示不同业务实体之间的关系。这些模型通常作为中间表实现多对多关系。 6 | 7 | ## 命名规范 8 | 9 | 关联模型的命名遵循以下规则: 10 | 11 | 1. **表名**: 采用 `主表名_关联表名` 的格式,如 `post_categories` 12 | 2. **结构体名**: 采用 `主表实体关联表实体` 的 Pascal 命名法,如 `PostCategory` 13 | 3. **字段命名**: 14 | - 包含两个关联实体的 ID 字段,命名为 `主表名ID` 和 `关联表名ID` 15 | - 例如: `PostID` 和 `CategoryID` 16 | 4. **索引**: 关联字段通常需要创建索引以提高查询性能 17 | 18 | ## 模型实现 19 | 20 | 关联模型通常包含以下要素: 21 | 22 | - 继承自 `base.Base` 基础模型 23 | - 包含关联实体的外键字段 24 | - 实现 `TableName()` 方法指定表名 25 | - 使用 `gorm` 标签定义字段属性和索引 26 | 27 | ## 示例 28 | 29 | ```go 30 | // PostCategory 文章-类目关联模型 31 | type PostCategory struct { 32 | base.Base 33 | PostID int64 `gorm:"type:bigint;not null;index" json:"post_id"` // 文章ID 34 | CategoryID int64 `gorm:"type:bigint;not null;index" json:"category_id"` // 类目ID 35 | } 36 | 37 | func (PostCategory) TableName() string { 38 | return "post_categories" 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /internal/model/association/post_category.go: -------------------------------------------------------------------------------- 1 | // Package model 提供实体关联数据模型定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package model 5 | 6 | import ( 7 | "jank.com/jank_blog/internal/model/base" 8 | ) 9 | 10 | // PostCategory 文章-类目关联模型 11 | type PostCategory struct { 12 | base.Base 13 | PostID int64 `gorm:"type:bigint;not null;index" json:"post_id"` // 文章ID 14 | CategoryID int64 `gorm:"type:bigint;index" json:"category_id"` // 类目ID 15 | } 16 | 17 | // TableName 指定表名 18 | // 返回值: 19 | // - string: 表名 20 | func (PostCategory) TableName() string { 21 | return "post_categories" 22 | } 23 | -------------------------------------------------------------------------------- /internal/model/base/READEME.md: -------------------------------------------------------------------------------- 1 | 基础模型 2 | -------------------------------------------------------------------------------- /internal/model/base/base.go: -------------------------------------------------------------------------------- 1 | // Package base 提供基础模型定义和通用数据库操作方法 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package base 5 | 6 | import ( 7 | "database/sql/driver" 8 | "encoding/json" 9 | "errors" 10 | "log" 11 | "time" 12 | 13 | "gorm.io/gorm" 14 | 15 | "jank.com/jank_blog/internal/utils" 16 | ) 17 | 18 | // Base 包含通用字段 19 | type Base struct { 20 | ID int64 `gorm:"primaryKey;type:bigint" json:"id"` // 主键(雪花算法) 21 | GmtCreate int64 `gorm:"type:bigint" json:"gmt_create"` // 创建时间 22 | GmtModified int64 `gorm:"type:bigint" json:"gmt_modified"` // 更新时间 23 | Ext JSONMap `gorm:"type:json" json:"ext"` // 扩展字段 24 | Deleted bool `gorm:"type:boolean;default:false" json:"deleted"` // 逻辑删除 25 | } 26 | 27 | // JSONMap 处理 json 类型字段 28 | type JSONMap map[string]interface{} 29 | 30 | // Scan 从数据库读取 json 数据 31 | // 参数: 32 | // - value: 数据库返回的值 33 | // 34 | // 返回值: 35 | // - error: 操作过程中的错误 36 | func (j *JSONMap) Scan(value interface{}) error { 37 | bytes, ok := value.([]byte) 38 | if !ok { 39 | return errors.New("数据类型错误,无法转换为 []byte 类型") 40 | } 41 | return json.Unmarshal(bytes, j) 42 | } 43 | 44 | // Value 将 JSONMap 转换为 json 数据存储到数据库 45 | // 返回值: 46 | // - driver.Value: 数据库驱动值 47 | // - error: 操作过程中的错误 48 | func (j JSONMap) Value() (driver.Value, error) { 49 | if j == nil { 50 | return "{}", nil 51 | } 52 | return json.Marshal(j) 53 | } 54 | 55 | // BeforeCreate 创建前操作,设置时间戳等 56 | // 参数: 57 | // - db: GORM数据库连接 58 | // 59 | // 返回值: 60 | // - error: 操作过程中的错误 61 | func (m *Base) BeforeCreate(db *gorm.DB) (err error) { 62 | currentTime := time.Now().Unix() 63 | m.GmtCreate = currentTime 64 | m.GmtModified = currentTime 65 | m.Deleted = false 66 | 67 | // 使用雪花算法生成ID 68 | id, err := utils.GenerateID() 69 | if err != nil { 70 | log.Printf("生成雪花ID时出错: %v", err) 71 | } 72 | m.ID = id 73 | 74 | if m.Ext == nil { 75 | m.Ext = make(map[string]interface{}) 76 | } 77 | return nil 78 | } 79 | 80 | // BeforeUpdate 更新前操作,更新修改时间 81 | // 参数: 82 | // - db: GORM数据库连接 83 | // 84 | // 返回值: 85 | // - error: 操作过程中的错误 86 | func (m *Base) BeforeUpdate(db *gorm.DB) (err error) { 87 | m.GmtModified = time.Now().Unix() 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/model/category/README.md: -------------------------------------------------------------------------------- 1 | 文章分类模型 2 | -------------------------------------------------------------------------------- /internal/model/category/category.go: -------------------------------------------------------------------------------- 1 | // Package model 提供类目数据模型定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package model 5 | 6 | import "jank.com/jank_blog/internal/model/base" 7 | 8 | // Category 类目模型 9 | type Category struct { 10 | base.Base 11 | Name string `gorm:"type:varchar(255);not null;index" json:"name"` // 类目名称 12 | Description string `gorm:"type:varchar(255);default:''" json:"description"` // 类目描述 13 | ParentID int64 `gorm:"index;default:null" json:"parent_id"` // 父类目ID 14 | Path string `gorm:"type:varchar(225);not null;index" json:"path"` // 类目路径 15 | Children []*Category `gorm:"-" json:"children"` // 子类目,不存储在数据库,用于递归构建树结构 16 | } 17 | 18 | // TableName 指定表名 19 | // 返回值: 20 | // - string: 表名 21 | func (Category) TableName() string { 22 | return "categories" 23 | } 24 | -------------------------------------------------------------------------------- /internal/model/comment/README.md: -------------------------------------------------------------------------------- 1 | 评论模型 2 | -------------------------------------------------------------------------------- /internal/model/comment/comment.go: -------------------------------------------------------------------------------- 1 | // Package model 提供评论数据模型定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package model 5 | 6 | import "jank.com/jank_blog/internal/model/base" 7 | 8 | // Comment 评论模型 9 | type Comment struct { 10 | base.Base 11 | Content string `gorm:"type:varchar(1024);not null" json:"content"` // 评论内容 12 | UserId int64 `gorm:"type:bigint;not null;index" json:"user_id"` // 所属用户ID 13 | PostId int64 `gorm:"type:bigint;not null;index" json:"post_id"` // 所属文章ID 14 | ReplyToCommentId int64 `gorm:"type:bigint;default:null" json:"reply_to_comment_id"` // 目标评论ID 15 | Replies []*Comment `gorm:"-" json:"replies"` // 子评论列表,用于构建图结构 16 | } 17 | 18 | // TableName 指定表名 19 | // 返回值: 20 | // - string: 表名 21 | func (Comment) TableName() string { 22 | return "comments" 23 | } 24 | -------------------------------------------------------------------------------- /internal/model/get_all_models.go: -------------------------------------------------------------------------------- 1 | // Package model 提供应用程序的数据模型定义和聚合 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package model 5 | 6 | import ( 7 | account "jank.com/jank_blog/internal/model/account" 8 | association "jank.com/jank_blog/internal/model/association" 9 | category "jank.com/jank_blog/internal/model/category" 10 | comment "jank.com/jank_blog/internal/model/comment" 11 | post "jank.com/jank_blog/internal/model/post" 12 | ) 13 | 14 | // GetAllModels 获取并注册所有模型 15 | // 返回值: 16 | // - []interface{}: 所有需要注册到数据库的模型列表 17 | func GetAllModels() []interface{} { 18 | return []interface{}{ 19 | // account 模块 20 | &account.Account{}, 21 | 22 | // post 模块 23 | &post.Post{}, 24 | 25 | // category 模块 26 | &category.Category{}, 27 | 28 | // comment 模块 29 | &comment.Comment{}, 30 | 31 | // association 跨模块中间表 32 | &association.PostCategory{}, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/model/post/README.md: -------------------------------------------------------------------------------- 1 | 文章模型 2 | -------------------------------------------------------------------------------- /internal/model/post/post.go: -------------------------------------------------------------------------------- 1 | // Package model 提供博客文章数据模型定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package model 5 | 6 | import ( 7 | "jank.com/jank_blog/internal/model/base" 8 | ) 9 | 10 | // Post 博客文章模型 11 | type Post struct { 12 | base.Base 13 | Title string `gorm:"type:varchar(255);not null;index" json:"title"` // 标题 14 | Image string `gorm:"type:varchar(255)" json:"image"` // 图片 15 | Visibility bool `gorm:"type:boolean;not null;default:false;index" json:"visibility"` // 可见性,默认不可见 16 | ContentMarkdown string `gorm:"type:text" json:"contentMarkdown"` // Markdown 内容 17 | ContentHTML string `gorm:"type:text" json:"contentHtml"` // 渲染后的 HTML 内容 18 | } 19 | 20 | // TableName 指定表名 21 | // 返回值: 22 | // - string: 表名 23 | func (Post) TableName() string { 24 | return "posts" 25 | } 26 | -------------------------------------------------------------------------------- /internal/redis/README.md: -------------------------------------------------------------------------------- 1 | # Redis 组件 2 | 3 | Redis 组件用于处理应用程序与 Redis 缓存服务器的连接和交互。该组件提供了一个全局 Redis 客户端实例,可以在整个应用程序中使用。 4 | 5 | ## 功能 6 | 7 | - **连接管理**: 创建并维护与 Redis 服务器的连接 8 | - **连接池**: 配置最优的连接池设置,包括最大连接数和最小空闲连接数 9 | - **超时控制**: 设置合理的连接、读取和写入超时时间 10 | - **健康检查**: 确保 Redis 连接正常工作 11 | 12 | ## 配置项 13 | 14 | Redis 连接从应用配置中读取以下参数: 15 | 16 | - 主机地址 (RedisHost) 17 | - 端口 (RedisPort) 18 | - 密码 (RedisPassword) 19 | - 数据库索引 (RedisDB) 20 | 21 | ## 使用方式 22 | 23 | 通过全局变量 `global.RedisClient` 在应用的任何位置访问 Redis 客户端,例如: 24 | 25 | ```go 26 | // 设置缓存 27 | global.RedisClient.Set(context.Background(), key, value, expiration) 28 | 29 | // 获取缓存 30 | global.RedisClient.Get(context.Background(), key) 31 | ``` 32 | -------------------------------------------------------------------------------- /internal/redis/redis.go: -------------------------------------------------------------------------------- 1 | // Package redis 提供Redis连接和管理功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package redis 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "runtime" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/redis/go-redis/v9" 14 | 15 | "jank.com/jank_blog/configs" 16 | "jank.com/jank_blog/internal/global" 17 | ) 18 | 19 | // New 初始化Redis连接 20 | // 参数: 21 | // - config: 应用配置 22 | func New(config *configs.Config) { 23 | client := newRedisClient(config) 24 | if err := client.Ping(context.Background()).Err(); err != nil { 25 | global.SysLog.Errorf("Redis 连接失败: %v", err) 26 | return 27 | } 28 | global.RedisClient = client 29 | global.SysLog.Infof("Redis 连接成功!") 30 | } 31 | 32 | // newRedisClient 创建新的Redis客户端 33 | // 参数: 34 | // - config: 应用配置 35 | // 36 | // 返回值: 37 | // - *redis.Client: Redis客户端实例 38 | func newRedisClient(config *configs.Config) *redis.Client { 39 | db, _ := strconv.Atoi(config.RedisConfig.RedisDB) 40 | return redis.NewClient(&redis.Options{ 41 | Addr: fmt.Sprintf("%s:%s", config.RedisConfig.RedisHost, config.RedisConfig.RedisPort), 42 | Password: config.RedisConfig.RedisPassword, // 数据库密码,默认为空字符串 43 | DB: db, // 数据库索引 44 | DialTimeout: 10 * time.Second, // 连接超时时间 45 | ReadTimeout: 1 * time.Second, // 读超时时间 46 | WriteTimeout: 2 * time.Second, // 写超时时间 47 | PoolSize: runtime.GOMAXPROCS(10), // 最大连接池大小 48 | MinIdleConns: 50, // 最小空闲连接数 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /internal/utils/README.md: -------------------------------------------------------------------------------- 1 | # 全局工具类 2 | 3 | 这个目录包含系统中使用的全局工具类和辅助函数。 4 | 5 | ## 工具类列表 6 | 7 | - **db_transaction_utils**:数据库事务管理工具 8 | - **email_utils**: 邮箱相关工具,用于发送验证码和其它电子邮件 9 | - **img_verification_utils**: 图形验证码生成工具 10 | - **jwt_utils**: JWT 令牌生成、验证和刷新工具 11 | - **logger_utils**: 日志记录工具 12 | - **markdown_utils**: Markdown 文本处理工具 13 | - **validator_utils**: 数据验证工具 14 | - **MapModelToVO_utils**: 模型对象到视图对象的映射工具,将 model 字段映射为 vo 字段 15 | -------------------------------------------------------------------------------- /internal/utils/db_transaction_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供各种工具函数,包括数据库事务管理 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/labstack/echo/v4" 10 | "gorm.io/gorm" 11 | 12 | "jank.com/jank_blog/internal/global" 13 | ) 14 | 15 | // DB_TRANSACTION_CONTEXT_KEY 事务相关常量 16 | const DB_TRANSACTION_CONTEXT_KEY = "tx" // 存储在Echo 上下文中的数据库事务键名 17 | 18 | // GetDBFromContext 从上下文中获取数据库连接 19 | // 参数: 20 | // - c: Echo 上下文 21 | // 22 | // 返回值: 23 | // - *gorm.DB: 数据库连接(事务优先,无事务则返回全局连接) 24 | func GetDBFromContext(c echo.Context) *gorm.DB { 25 | if tx, ok := c.Get(DB_TRANSACTION_CONTEXT_KEY).(*gorm.DB); ok && tx != nil { 26 | return tx 27 | } 28 | return global.DB 29 | } 30 | 31 | // RunDBTransaction 在事务中执行函数 32 | // 参数: 33 | // - c: Echo 上下文 34 | // - fn: 事务内执行的函数 35 | // 36 | // 返回值: 37 | // - error: 执行过程中的错误 38 | func RunDBTransaction(c echo.Context, fn func(error) error) error { 39 | tx := global.DB.Begin() 40 | if tx.Error != nil { 41 | return fmt.Errorf("开始事务失败: %w", tx.Error) 42 | } 43 | 44 | c.Set(DB_TRANSACTION_CONTEXT_KEY, tx) 45 | defer c.Set(DB_TRANSACTION_CONTEXT_KEY, nil) 46 | 47 | // panic 处理 48 | defer func() { 49 | if r := recover(); r != nil { 50 | tx.Rollback() 51 | panic(r) 52 | } 53 | }() 54 | 55 | // 执行业务逻辑 56 | if err := fn(nil); err != nil { 57 | tx.Rollback() 58 | return err 59 | } 60 | 61 | // 提交事务 62 | if err := tx.Commit().Error; err != nil { 63 | tx.Rollback() 64 | return fmt.Errorf("提交事务失败: %w", err) 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/utils/email_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供邮件操作相关工具 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "math/rand" 9 | "net/smtp" 10 | "regexp" 11 | "time" 12 | 13 | "github.com/jordan-wright/email" 14 | 15 | "jank.com/jank_blog/configs" 16 | "jank.com/jank_blog/internal/global" 17 | ) 18 | 19 | const SUBJECT = "【Jank Blog】注册验证码" 20 | 21 | // 邮箱服务器配置 22 | var emailServers = map[string]struct { 23 | Server, Port string 24 | }{ 25 | "qq": {"smtp.qq.com", ":587"}, 26 | "gmail": {"smtp.gmail.com", ":587"}, 27 | "outlook": {"smtp.office365.com", ":587"}, 28 | } 29 | 30 | // SendEmail 发送邮件到指定邮箱 31 | // 参数: 32 | // - content: 邮件内容 33 | // - toEmail: 接收邮件的邮箱地址数组 34 | // 35 | // 返回值: 36 | // - bool: 发送成功返回 true,失败返回 false 37 | // - error: 发送过程中的错误 38 | func SendEmail(content string, toEmail []string) (bool, error) { 39 | config, err := configs.LoadConfig() 40 | if err != nil { 41 | global.SysLog.Errorf("加载邮件配置失败, toEmail: %v, 错误信息: %v", toEmail, err) 42 | return false, fmt.Errorf("加载邮件配置失败: %v", err) 43 | } 44 | 45 | // 获取SMTP相关配置 46 | fromEmail := config.AppConfig.FromEmail 47 | emailType := config.AppConfig.EmailType 48 | 49 | // 获取邮箱类型和对应的服务器配置 50 | serverConfig, ok := emailServers[emailType] 51 | if !ok || emailType == "" { 52 | emailType = "qq" 53 | serverConfig = emailServers[emailType] 54 | global.SysLog.Warnf("邮箱类型无效或为空, 原类型: %s, 默认使用 QQ 邮箱替代", emailType) 55 | } 56 | 57 | e := email.NewEmail() 58 | e.From = fromEmail 59 | e.To = toEmail 60 | e.Subject = SUBJECT 61 | e.Text = []byte(content) 62 | 63 | smtpAddr := serverConfig.Server + serverConfig.Port 64 | auth := smtp.PlainAuth("", fromEmail, config.AppConfig.EmailSmtp, serverConfig.Server) 65 | 66 | if err := e.Send(smtpAddr, auth); err != nil { 67 | global.SysLog.Errorf("发送邮件失败, toEmail: %v, 错误信息: %v", toEmail, err) 68 | return false, fmt.Errorf("发送邮件失败: %v", err) 69 | } 70 | 71 | return true, nil 72 | } 73 | 74 | // NewRand 生成六位数随机验证码 75 | // 返回值: 76 | // - int: 六位数随机验证码 77 | func NewRand() int { 78 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 79 | return r.Intn(900000) + 100000 80 | } 81 | 82 | // ValidEmail 检查邮箱格式是否有效 83 | // 参数: 84 | // - email: 待验证的邮箱地址 85 | // 86 | // 返回值: 87 | // - bool: 邮箱格式有效返回 true,无效返回 false 88 | func ValidEmail(email string) bool { 89 | pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` 90 | return regexp.MustCompile(pattern).MatchString(email) 91 | } 92 | -------------------------------------------------------------------------------- /internal/utils/img_verification_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供图形验证码生成工具 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/mojocn/base64Captcha" 10 | ) 11 | 12 | const ( 13 | CaptchaSource = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // 验证码字符源 14 | FontFile = "wqy-microhei.ttc" // 字体文件 15 | ImgHeight = 80 // 验证码图片高度 16 | ImgWidth = 200 // 验证码图片宽度 17 | NoiseCount = 0 // 干扰点数量 18 | CaptchaLength = 4 // 验证码字符长度 19 | ) 20 | 21 | var store = base64Captcha.DefaultMemStore 22 | 23 | // GenImgVerificationCode 生成图形验证码 24 | // 返回值: 25 | // - string: 图片验证码的 Base64 编码 26 | // - string: 验证码答案 27 | // - error: 生成过程中的错误 28 | func GenImgVerificationCode() (string, string, error) { 29 | driver := createDriver() 30 | captcha := base64Captcha.NewCaptcha(driver, store) 31 | _, content, answer := captcha.Driver.GenerateIdQuestionAnswer() 32 | item, err := captcha.Driver.DrawCaptcha(content) 33 | if err != nil { 34 | return "", "", fmt.Errorf("生成图形验证码失败: %v", err) 35 | } 36 | return item.EncodeB64string(), answer, nil 37 | } 38 | 39 | // createDriver 创建验证码的驱动配置 40 | // 返回值: 41 | // - *base64Captcha.DriverString: 验证码驱动对象 42 | func createDriver() *base64Captcha.DriverString { 43 | return &base64Captcha.DriverString{ 44 | Height: ImgHeight, 45 | Width: ImgWidth, 46 | NoiseCount: NoiseCount, 47 | ShowLineOptions: base64Captcha.OptionShowSineLine, 48 | Length: CaptchaLength, 49 | Source: CaptchaSource, 50 | Fonts: []string{FontFile}, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/utils/jwt_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供JWT令牌生成与验证工具 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | "github.com/golang-jwt/jwt/v4" 12 | ) 13 | 14 | var ( 15 | // 密钥和有效期配置 16 | accessSecret = []byte("jank-blog-secret") // Access Token 使用的密钥 17 | refreshSecret = []byte("jank-blog-refresh-secret") // Refresh Token 使用的密钥 18 | accessExpireTime = time.Hour * 2 // Access Token 有效期 19 | refreshExpireTime = time.Hour * 48 // Refresh Token 有效期 20 | clockSkew = 5 * time.Second // 允许的时间偏差量 21 | ) 22 | 23 | // GenerateJWT 生成 Access Token 和 Refresh Token 24 | // 参数: 25 | // - accountID: 账户ID 26 | // 27 | // 返回值: 28 | // - string: Access Token 29 | // - string: Refresh Token 30 | // - error: 生成过程中的错误 31 | func GenerateJWT(accountID int64) (string, string, error) { 32 | accessTokenString, err := generateToken(accountID, accessSecret, accessExpireTime) 33 | if err != nil { 34 | return "", "", err 35 | } 36 | 37 | refreshTokenString, err := generateToken(accountID, refreshSecret, refreshExpireTime) 38 | if err != nil { 39 | return "", "", err 40 | } 41 | 42 | return accessTokenString, refreshTokenString, nil 43 | } 44 | 45 | // ValidateJWTToken 验证 Access Token 或 Refresh Token 46 | // 参数: 47 | // - tokenString: 令牌字符串 48 | // - isRefreshToken: 是否为刷新令牌 49 | // 50 | // 返回值: 51 | // - *jwt.Token: 验证通过的令牌 52 | // - error: 验证过程中的错误 53 | func ValidateJWTToken(tokenString string, isRefreshToken bool) (*jwt.Token, error) { 54 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") 55 | 56 | secret := accessSecret 57 | if isRefreshToken { 58 | secret = refreshSecret 59 | } 60 | 61 | token, err := validateToken(tokenString, secret) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if claims, ok := token.Claims.(jwt.MapClaims); !ok || !token.Valid { 67 | return nil, fmt.Errorf("无效 token") 68 | } else { 69 | if exp, ok := claims["exp"].(float64); ok { 70 | if time.Now().UTC().Add(clockSkew).Unix() > int64(exp) { 71 | if isRefreshToken { 72 | return nil, fmt.Errorf("refresh token 已过期,请重新登录") 73 | } 74 | return nil, fmt.Errorf("access token 已过期,请使用 refresh token 获取新的 access token") 75 | } 76 | } else { 77 | return nil, fmt.Errorf("缺少 exp 字段") 78 | } 79 | } 80 | 81 | return token, nil 82 | } 83 | 84 | // RefreshTokenLogic 负责刷新 Token 85 | // 参数: 86 | // - refreshTokenString: 刷新令牌字符串 87 | // 88 | // 返回值: 89 | // - map[string]string: 包含新的 Access Token 和 Refresh Token 的映射 90 | // - error: 刷新过程中的错误 91 | func RefreshTokenLogic(refreshTokenString string) (map[string]string, error) { 92 | token, err := ValidateJWTToken(refreshTokenString, true) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 98 | accountID := int64(claims["account_id"].(float64)) 99 | 100 | newAccessToken, newRefreshToken, err := GenerateJWT(accountID) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return map[string]string{ 106 | "accessToken": newAccessToken, 107 | "refreshToken": newRefreshToken, 108 | }, nil 109 | } 110 | 111 | return nil, fmt.Errorf("refresh token 验证失败") 112 | } 113 | 114 | // ParseAccountAndRoleIDFromJWT 从 JWT 中提取 accountID 和 roleID 115 | // 参数: 116 | // - tokenString: 令牌字符串 117 | // 118 | // 返回值: 119 | // - int64: 账户ID 120 | // - error: 解析过程中的错误 121 | func ParseAccountAndRoleIDFromJWT(tokenString string) (int64, error) { 122 | tokenString = strings.TrimPrefix(tokenString, "Bearer ") 123 | 124 | token, err := ValidateJWTToken(tokenString, false) 125 | if err != nil { 126 | return 0, err 127 | } 128 | 129 | claims, ok := token.Claims.(jwt.MapClaims) 130 | if !ok { 131 | return 0, fmt.Errorf("无法解析 access token 中的 claims") 132 | } 133 | 134 | accountID, ok := claims["account_id"].(float64) 135 | if !ok { 136 | return 0, fmt.Errorf("access token 中缺少 account_id") 137 | } 138 | 139 | return int64(accountID), nil 140 | } 141 | 142 | // generateToken 通用的 token 生成函数 143 | // 参数: 144 | // - accountID: 账户ID 145 | // - secret: 密钥 146 | // - expireTime: 过期时间 147 | // 148 | // 返回值: 149 | // - string: 生成的令牌 150 | // - error: 生成过程中的错误 151 | func generateToken(accountID int64, secret []byte, expireTime time.Duration) (string, error) { 152 | claims := jwt.MapClaims{ 153 | "account_id": accountID, 154 | "exp": time.Now().UTC().Add(expireTime).Unix(), 155 | } 156 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 157 | tokenString, err := token.SignedString(secret) 158 | if err != nil { 159 | return "", err 160 | } 161 | return tokenString, nil 162 | } 163 | 164 | // validateToken 验证 token 是否有效 165 | // 参数: 166 | // - tokenString: 令牌字符串 167 | // - secret: 密钥 168 | // 169 | // 返回值: 170 | // - *jwt.Token: 验证通过的令牌 171 | // - error: 验证过程中的错误 172 | func validateToken(tokenString string, secret []byte) (*jwt.Token, error) { 173 | token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { 174 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 175 | return nil, jwt.ErrSignatureInvalid 176 | } 177 | return secret, nil 178 | }) 179 | 180 | if err != nil { 181 | return nil, err 182 | } 183 | return token, nil 184 | } 185 | -------------------------------------------------------------------------------- /internal/utils/logger_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供日志记录工具 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | "github.com/sirupsen/logrus" 9 | 10 | "jank.com/jank_blog/internal/global" 11 | ) 12 | 13 | const ( 14 | BIZLOG = "Bizlog" // 业务日志键名 15 | ) 16 | 17 | // BizLogger 业务日志记录器 18 | // 参数: 19 | // - c: Echo 上下文 20 | // 21 | // 返回值: 22 | // - *logrus.Entry: 日志条目 23 | func BizLogger(c echo.Context) *logrus.Entry { 24 | if bizLog, ok := c.Get(BIZLOG).(*logrus.Entry); ok { 25 | return bizLog 26 | } 27 | 28 | return logrus.NewEntry(global.SysLog) 29 | } 30 | -------------------------------------------------------------------------------- /internal/utils/map_model_to_vo_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供模型对象到视图对象的映射工具 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | // MapModelToVO 将模型数据映射到对应的 VO,返回具体类型的指针 15 | // 参数: 16 | // - modelData: 源模型数据 17 | // - voPtr: 目标视图对象指针 18 | // 19 | // 返回值: 20 | // - interface{}: 映射后的视图对象指针 21 | // - error: 映射过程中的错误 22 | func MapModelToVO(modelData interface{}, voPtr interface{}) (interface{}, error) { 23 | modelVal := reflect.ValueOf(modelData) 24 | voVal := reflect.ValueOf(voPtr) 25 | 26 | // 检查 voPtr 是否为指向结构体的指针 27 | if voVal.Kind() != reflect.Ptr || voVal.IsNil() { 28 | return nil, fmt.Errorf("voPtr 必须为指向结构体的指针") 29 | } 30 | voVal = voVal.Elem() 31 | 32 | // 如果 modelData 是指针类型,解引用获取实际的结构体值 33 | if modelVal.Kind() == reflect.Ptr { 34 | modelVal = modelVal.Elem() 35 | } 36 | 37 | // 确保 modelData 和 voPtr 都是结构体类型 38 | if modelVal.Kind() != reflect.Struct || voVal.Kind() != reflect.Struct { 39 | return nil, fmt.Errorf("modelData 和 voPtr 必须为结构体类型") 40 | } 41 | 42 | numFields := modelVal.NumField() 43 | modelType := modelVal.Type() 44 | 45 | // 控制并发的最大数量 46 | maxConcurrency := 8 47 | sem := make(chan struct{}, maxConcurrency) 48 | 49 | var wg sync.WaitGroup 50 | var mu sync.Mutex 51 | 52 | // 遍历 modelData 中的字段并并行处理 53 | for i := 0; i < numFields; i++ { 54 | wg.Add(1) 55 | sem <- struct{}{} // 控制并发 56 | 57 | go func(i int) { 58 | defer wg.Done() 59 | defer func() { <-sem }() // 释放信号量 60 | 61 | modelField := modelVal.Field(i) 62 | fieldType := modelType.Field(i) 63 | voField := voVal.FieldByName(fieldType.Name) 64 | 65 | if voField.IsValid() && voField.CanSet() { 66 | // 赋值操作 67 | if modelField.Type().AssignableTo(voField.Type()) { 68 | mu.Lock() 69 | voField.Set(modelField) 70 | mu.Unlock() 71 | } else if modelField.Kind() == reflect.String && voField.Kind() == reflect.Slice && voField.Type().Elem().Kind() == reflect.Int64 { 72 | str := modelField.String() 73 | if str != "" { 74 | strArray := strings.Split(str, ",") 75 | intArray := make([]int64, len(strArray)) 76 | for j, s := range strArray { 77 | if val, err := strconv.ParseInt(s, 10, 64); err == nil { 78 | intArray[j] = val 79 | } 80 | } 81 | mu.Lock() 82 | voField.Set(reflect.ValueOf(intArray)) 83 | mu.Unlock() 84 | } 85 | } 86 | } 87 | 88 | // 处理嵌套结构体字段 89 | if modelField.Kind() == reflect.Struct { 90 | embeddedModelType := modelField.Type() 91 | embeddedNumFields := embeddedModelType.NumField() 92 | 93 | for j := 0; j < embeddedNumFields; j++ { 94 | embeddedField := embeddedModelType.Field(j) 95 | voField := voVal.FieldByName(embeddedField.Name) 96 | 97 | if voField.IsValid() && voField.CanSet() { 98 | mu.Lock() 99 | voField.Set(modelField.Field(j)) 100 | mu.Unlock() 101 | } 102 | } 103 | } 104 | }(i) 105 | } 106 | 107 | wg.Wait() 108 | 109 | return voVal.Addr().Interface(), nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/utils/markdown_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供Markdown渲染工具 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import ( 7 | "bytes" 8 | "sync" 9 | 10 | "github.com/yuin/goldmark" 11 | "github.com/yuin/goldmark/extension" 12 | "github.com/yuin/goldmark/parser" 13 | "github.com/yuin/goldmark/renderer" 14 | "github.com/yuin/goldmark/renderer/html" 15 | ) 16 | 17 | // 使用 sync.Pool 复用 buffer 18 | var bufferPool = sync.Pool{ 19 | New: func() interface{} { 20 | return new(bytes.Buffer) 21 | }, 22 | } 23 | 24 | // MarkdownConfig 用于配置 Goldmark 渲染器 25 | type MarkdownConfig struct { 26 | Extensions []goldmark.Extender // Goldmark 扩展 27 | ParserOptions []parser.Option // 解析器选项 28 | RendererOptions []renderer.Option // 渲染器选项 29 | } 30 | 31 | // NewMarkdownRenderer 创建一个新的 Markdown 渲染器 32 | // 参数: 33 | // - config: Markdown配置 34 | // 35 | // 返回值: 36 | // - goldmark.Markdown: Markdown渲染器 37 | func NewMarkdownRenderer(config MarkdownConfig) goldmark.Markdown { 38 | return goldmark.New( 39 | goldmark.WithExtensions(config.Extensions...), 40 | goldmark.WithParserOptions(config.ParserOptions...), 41 | goldmark.WithRendererOptions(config.RendererOptions...), 42 | ) 43 | } 44 | 45 | // defaultMarkdownConfig 返回默认的 Markdown 配置 46 | // 返回值: 47 | // - MarkdownConfig: 默认Markdown配置 48 | func defaultMarkdownConfig() MarkdownConfig { 49 | return MarkdownConfig{ 50 | Extensions: []goldmark.Extender{ 51 | extension.Linkify, // 自动链接支持 52 | extension.GFM, // 启用 GitHub Flavored Markdown 53 | extension.Table, // 表格支持 54 | extension.TaskList, // 任务列表支持 55 | extension.Strikethrough, // 删除线支持 56 | extension.Footnote, // 脚注支持 57 | extension.DefinitionList, // 定义列表支持 58 | extension.Typographer, // Typography support 59 | extension.CJK, // CJK 支持 60 | }, 61 | ParserOptions: []parser.Option{ 62 | parser.WithAutoHeadingID(), // 自动生成标题 ID 63 | parser.WithBlockParsers(), // 块解析器 64 | parser.WithInlineParsers(), // 内联解析器 65 | parser.WithParagraphTransformers(), // 段落转换器 66 | parser.WithASTTransformers(), // AST 转换器 67 | parser.WithAttribute(), // 启用自定义属性,目前只有标题支持属性。 68 | }, 69 | RendererOptions: []renderer.Option{ 70 | html.WithHardWraps(), // 硬换行 71 | html.WithXHTML(), // 生成 XHTML 72 | }, 73 | } 74 | } 75 | 76 | // RenderMarkdown 将 Markdown 渲染为 HTML 77 | // 参数: 78 | // - content: Markdown内容 79 | // 80 | // 返回值: 81 | // - string: 渲染后的 HTML 82 | // - error: 渲染过程中的错误 83 | func RenderMarkdown(content []byte) (string, error) { 84 | md := NewMarkdownRenderer(defaultMarkdownConfig()) 85 | buf := bufferPool.Get().(*bytes.Buffer) 86 | buf.Reset() 87 | defer bufferPool.Put(buf) 88 | 89 | if err := md.Convert(content, buf); err != nil { 90 | return "", err 91 | } 92 | 93 | return buf.String(), nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/utils/snowflake_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供雪花算法ID生成工具 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "sync" 9 | "time" 10 | 11 | "github.com/bwmarrin/snowflake" 12 | ) 13 | 14 | var ( 15 | node *snowflake.Node 16 | once sync.Once 17 | ) 18 | 19 | // GenerateID 生成雪花算法 ID 20 | // 返回值: 21 | // - int64: 生成的雪花算法 ID 22 | // - error: 操作过程中的错误 23 | func GenerateID() (int64, error) { 24 | once.Do(func() { 25 | var err error 26 | node, err = snowflake.NewNode(1) 27 | if err != nil { 28 | fmt.Printf("初始化雪花算法节点失败: %v", err) 29 | } 30 | }) 31 | 32 | switch { 33 | case node != nil: 34 | return node.Generate().Int64(), nil 35 | 36 | default: 37 | // 雪花格式: 41 位时间戳 + 10 位节点 ID + 12 位序列号 38 | // 标准雪花纪元,节点 ID 1,序列号使用当前纳秒的低 12 位 39 | ts := time.Now().UnixMilli() - 1288834974657 40 | nodeID := int64(1) 41 | seq := time.Now().UnixNano() & 0xFFF 42 | 43 | return (ts << 22) | (nodeID << 12) | seq, nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/utils/validator_utils.go: -------------------------------------------------------------------------------- 1 | // Package utils 提供参数验证工具 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package utils 5 | 6 | import "github.com/go-playground/validator/v10" 7 | 8 | // ValidErrRes 验证错误结果结构体 9 | type ValidErrRes struct { 10 | Error bool // 是否存在错误 11 | Field string // 错误字段名 12 | Tag string // 错误标签 13 | Value interface{} // 错误值 14 | } 15 | 16 | // NewValidator 全局验证器实例 17 | var NewValidator = validator.New() 18 | 19 | // Validator 参数验证器 20 | // 参数: 21 | // - data: 待验证的数据 22 | // 23 | // 返回值: 24 | // - []ValidErrRes: 验证错误结果数组 25 | func Validator(data interface{}) []ValidErrRes { 26 | var Errors []ValidErrRes 27 | errs := NewValidator.Struct(data) 28 | if errs != nil { 29 | for _, err := range errs.(validator.ValidationErrors) { 30 | var el ValidErrRes 31 | el.Error = true 32 | el.Field = err.Field() 33 | el.Tag = err.Tag() 34 | el.Value = err.Value() 35 | 36 | Errors = append(Errors, el) 37 | } 38 | } 39 | return Errors 40 | } 41 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main 程序入口,是应用程序的主入口点 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package main 5 | 6 | import ( 7 | "jank.com/jank_blog/cmd" 8 | ) 9 | 10 | // main 程序主入口函数 11 | func main() { 12 | cmd.Start() 13 | } 14 | -------------------------------------------------------------------------------- /pkg/router/README.md: -------------------------------------------------------------------------------- 1 | 统一路由组件 2 | -------------------------------------------------------------------------------- /pkg/router/router.go: -------------------------------------------------------------------------------- 1 | // Package router 提供应用程序路由注册功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package router 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | 9 | "jank.com/jank_blog/pkg/router/routes" 10 | ) 11 | 12 | // New @title Jank Blog API 13 | // @version 1.0 14 | // @description This is the API documentation for Jank Blog. 15 | // @host localhost:9010 16 | // @BasePath / 17 | // @securityDefinitions.apikey BearerAuth 18 | // @in header 19 | // @name Authorization 20 | // @description 输入格式: Bearer {token} 21 | // New 函数用于注册应用程序的路由 22 | // 参数: 23 | // - app: Echo 实例 24 | func New(app *echo.Echo) { 25 | // 创建多版本 API 路由组 26 | api1 := app.Group("/api/v1") 27 | api2 := app.Group("/api/v2") 28 | 29 | // 注册测试相关的路由 30 | routes.RegisterTestRoutes(api1, api2) 31 | // 注册账户相关的路由 32 | routes.RegisterAccountRoutes(api1) 33 | // 注册验证相关的路由 34 | routes.RegisterVerificationRoutes(api1) 35 | // 注册文章相关的路由 36 | routes.RegisterPostRoutes(api1) 37 | // 注册类目相关的路由 38 | routes.RegisterCategoryRoutes(api1) 39 | // 注册评论相关的路由 40 | routes.RegisterCommentRoutes(api1) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/router/routes/account.go: -------------------------------------------------------------------------------- 1 | // Package routes 提供路由注册功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package routes 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | 9 | auth_middleware "jank.com/jank_blog/internal/middleware/auth" 10 | "jank.com/jank_blog/pkg/serve/controller/account" 11 | ) 12 | 13 | // RegisterAccountRoutes 注册账户相关路由 14 | // 参数: 15 | // - r: Echo 路由组数组,r[0] 为 API v1 版本组 16 | func RegisterAccountRoutes(r ...*echo.Group) { 17 | // api v1 group 18 | apiV1 := r[0] 19 | accountGroupV1 := apiV1.Group("/account") 20 | accountGroupV1.POST("/getAccount", account.GetAccount, auth_middleware.AuthMiddleware()) 21 | accountGroupV1.POST("/registerAccount", account.RegisterAcc) 22 | accountGroupV1.POST("/loginAccount", account.LoginAccount) 23 | accountGroupV1.POST("/logoutAccount", account.LogoutAccount, auth_middleware.AuthMiddleware()) 24 | accountGroupV1.POST("/resetPassword", account.ResetPassword, auth_middleware.AuthMiddleware()) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/router/routes/category.go: -------------------------------------------------------------------------------- 1 | // Package routes 提供路由注册功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package routes 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | 9 | auth_middleware "jank.com/jank_blog/internal/middleware/auth" 10 | "jank.com/jank_blog/pkg/serve/controller/category" 11 | ) 12 | 13 | // RegisterCategoryRoutes 注册类目相关路由 14 | // 参数: 15 | // - r: Echo 路由组数组,r[0] 为 API v1 版本组 16 | func RegisterCategoryRoutes(r ...*echo.Group) { 17 | // api v1 group 18 | apiV1 := r[0] 19 | categoryGroupV1 := apiV1.Group("/category") 20 | categoryGroupV1.GET("/getOneCategory", category.GetOneCategory) 21 | categoryGroupV1.GET("/getCategoryTree", category.GetCategoryTree) 22 | categoryGroupV1.GET("/getCategoryChildrenTree", category.GetCategoryChildrenTree) 23 | categoryGroupV1.POST("/createOneCategory", category.CreateOneCategory, auth_middleware.AuthMiddleware()) 24 | categoryGroupV1.POST("/updateOneCategory", category.UpdateOneCategory, auth_middleware.AuthMiddleware()) 25 | categoryGroupV1.POST("/deleteOneCategory", category.DeleteOneCategory, auth_middleware.AuthMiddleware()) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/router/routes/comment.go: -------------------------------------------------------------------------------- 1 | // Package routes 提供路由注册功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package routes 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | 9 | auth_middleware "jank.com/jank_blog/internal/middleware/auth" 10 | "jank.com/jank_blog/pkg/serve/controller/comment" 11 | ) 12 | 13 | // RegisterCommentRoutes 注册评论相关路由 14 | // 参数: 15 | // - r: Echo 路由组数组,r[0] 为 API v1 版本组 16 | func RegisterCommentRoutes(r ...*echo.Group) { 17 | // api v1 group 18 | apiV1 := r[0] 19 | commentGroupV1 := apiV1.Group("/comment") 20 | commentGroupV1.GET("/getOneComment", comment.GetOneComment) 21 | commentGroupV1.GET("/getCommentGraph", comment.GetCommentGraph) 22 | commentGroupV1.POST("/createOneComment", comment.CreateOneComment, auth_middleware.AuthMiddleware()) 23 | commentGroupV1.POST("/deleteOneComment", comment.DeleteOneComment, auth_middleware.AuthMiddleware()) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/router/routes/post.go: -------------------------------------------------------------------------------- 1 | // Package routes 提供路由注册功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package routes 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | 9 | auth_middleware "jank.com/jank_blog/internal/middleware/auth" 10 | "jank.com/jank_blog/pkg/serve/controller/post" 11 | ) 12 | 13 | // RegisterPostRoutes 注册文章相关路由 14 | // 参数: 15 | // - r: Echo 路由组数组,r[0] 为 API v1 版本组 16 | func RegisterPostRoutes(r ...*echo.Group) { 17 | // api v1 group 18 | apiV1 := r[0] 19 | postGroupV1 := apiV1.Group("/post") 20 | postGroupV1.POST("/getOnePost", post.GetOnePost) 21 | postGroupV1.GET("/getAllPosts", post.GetAllPosts) 22 | postGroupV1.POST("/createOnePost", post.CreateOnePost, auth_middleware.AuthMiddleware()) 23 | postGroupV1.POST("/updateOnePost", post.UpdateOnePost, auth_middleware.AuthMiddleware()) 24 | postGroupV1.POST("/deleteOnePost", post.DeleteOnePost, auth_middleware.AuthMiddleware()) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/router/routes/test.go: -------------------------------------------------------------------------------- 1 | // Package routes 提供路由注册功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package routes 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | 9 | "jank.com/jank_blog/pkg/serve/controller/test" 10 | ) 11 | 12 | // RegisterTestRoutes 注册测试相关路由 13 | // 参数: 14 | // - r: Echo 路由组数组,r[0] 为 API v1 版本组,r[1] 为 API v2 版本组 15 | func RegisterTestRoutes(r ...*echo.Group) { 16 | // api v1 group 17 | apiV1 := r[0] 18 | testGroupV1 := apiV1.Group("/test") 19 | testGroupV1.GET("/testPing", test.TestPing) 20 | testGroupV1.GET("/testHello", test.TestHello) 21 | testGroupV1.GET("/testLogger", test.TestLogger) 22 | testGroupV1.GET("/testRedis", test.TestRedis) 23 | testGroupV1.GET("/testSuccessRes", test.TestSuccRes) 24 | testGroupV1.GET("/testErrRes", test.TestErrRes) 25 | testGroupV1.GET("/testErrorMiddleware", test.TestErrorMiddleware) 26 | 27 | // api v2 group 28 | apiV2 := r[1] 29 | testGroupV2 := apiV2.Group("/test") 30 | testGroupV2.GET("/testLongReq", test.TestLongReq) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/router/routes/verification.go: -------------------------------------------------------------------------------- 1 | // Package routes 提供路由注册功能 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package routes 5 | 6 | import ( 7 | "github.com/labstack/echo/v4" 8 | 9 | "jank.com/jank_blog/pkg/serve/controller/verification" 10 | ) 11 | 12 | // RegisterVerificationRoutes 注册验证码相关路由 13 | // 参数: 14 | // - r: Echo 路由组数组,r[0] 为 API v1 版本组 15 | func RegisterVerificationRoutes(r ...*echo.Group) { 16 | // api v1 group 17 | apiV1 := r[0] 18 | accountGroupV1 := apiV1.Group("/verification") 19 | accountGroupV1.GET("/sendImgVerificationCode", verification.SendImgVerificationCode) 20 | accountGroupV1.GET("/sendEmailVerificationCode", verification.SendEmailVerificationCode) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/serve/controller/account/account.go: -------------------------------------------------------------------------------- 1 | // Package account 提供账户相关的HTTP接口处理 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package account 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | bizErr "jank.com/jank_blog/internal/error" 12 | "jank.com/jank_blog/internal/utils" 13 | "jank.com/jank_blog/pkg/serve/controller/account/dto" 14 | "jank.com/jank_blog/pkg/serve/controller/verification" 15 | service "jank.com/jank_blog/pkg/serve/service/account" 16 | "jank.com/jank_blog/pkg/vo" 17 | ) 18 | 19 | // GetAccount godoc 20 | // @Summary 获取账户信息 21 | // @Description 根据提供的邮箱获取对应用户的详细信息 22 | // @Tags 账户 23 | // @Accept json 24 | // @Produce json 25 | // @Param request body dto.GetAccountRequest true "获取账户请求参数" 26 | // @Success 200 {object} vo.Result{data=account.GetAccountVO} "获取成功" 27 | // @Failure 400 {object} vo.Result "请求参数错误" 28 | // @Failure 404 {object} vo.Result "用户不存在" 29 | // @Router /account/getAccount [post] 30 | // 参数: 31 | // - c: Echo 上下文 32 | // 33 | // 返回值: 34 | // - error: 操作过程中的错误 35 | func GetAccount(c echo.Context) error { 36 | req := new(dto.GetAccountRequest) 37 | if err := c.Bind(req); err != nil { 38 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 39 | } 40 | 41 | errors := utils.Validator(*req) 42 | if errors != nil { 43 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST, "请求参数校验失败"))) 44 | } 45 | 46 | response, err := service.GetAccount(c, req) 47 | if err != nil { 48 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 49 | } 50 | 51 | return c.JSON(http.StatusOK, vo.Success(c, response)) 52 | } 53 | 54 | // RegisterAcc godoc 55 | // @Summary 用户注册 56 | // @Description 注册新用户账号,支持图形验证码和邮箱验证码校验 57 | // @Tags 账户 58 | // @Accept json 59 | // @Produce json 60 | // @Param request body dto.RegisterRequest true "注册信息" 61 | // @Param ImgVerificationCode query string true "图形验证码" 62 | // @Param EmailVerificationCode query string true "邮箱验证码" 63 | // @Success 200 {object} vo.Result{data=dto.RegisterRequest} "注册成功" 64 | // @Failure 400 {object} vo.Result "参数错误,验证码校验失败" 65 | // @Failure 500 {object} vo.Result "服务器错误" 66 | // @Router /account/registerAccount [post] 67 | // 参数: 68 | // - c: Echo 上下文 69 | // 70 | // 返回值: 71 | // - error: 操作过程中的错误 72 | func RegisterAcc(c echo.Context) error { 73 | req := new(dto.RegisterRequest) 74 | if err := c.Bind(req); err != nil { 75 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 76 | } 77 | 78 | errors := utils.Validator(*req) 79 | if errors != nil { 80 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST, "请求参数校验失败"))) 81 | } 82 | 83 | if !verification.VerifyImgCode(c, req.ImgVerificationCode, req.Email) { 84 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST, "图形验证码校验失败"))) 85 | } 86 | 87 | if !verification.VerifyEmailCode(c, req.EmailVerificationCode, req.Email) { 88 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST, "邮箱验证码校验失败"))) 89 | } 90 | 91 | acc, err := service.RegisterAcc(c, req) 92 | if err != nil { 93 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 94 | } 95 | 96 | return c.JSON(http.StatusOK, vo.Success(c, acc)) 97 | } 98 | 99 | // LoginAccount godoc 100 | // @Summary 用户登录 101 | // @Description 用户登录并获取访问令牌,支持图形验证码校验 102 | // @Tags 账户 103 | // @Accept json 104 | // @Produce json 105 | // @Param request body dto.LoginRequest true "登录信息" 106 | // @Param ImgVerificationCode query string true "图形验证码" 107 | // @Success 200 {object} vo.Result{data=account.LoginVO} "登录成功,返回访问令牌" 108 | // @Failure 400 {object} vo.Result "参数错误,验证码校验失败" 109 | // @Failure 401 {object} vo.Result "登录失败,凭证无效" 110 | // @Router /account/loginAccount [post] 111 | // 参数: 112 | // - c: Echo 上下文 113 | // 114 | // 返回值: 115 | // - error: 操作过程中的错误 116 | func LoginAccount(c echo.Context) error { 117 | req := new(dto.LoginRequest) 118 | if err := c.Bind(req); err != nil { 119 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 120 | } 121 | 122 | errors := utils.Validator(*req) 123 | if errors != nil { 124 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST, "请求参数校验失败"))) 125 | } 126 | 127 | if !verification.VerifyImgCode(c, req.ImgVerificationCode, req.Email) { 128 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST, "图形验证码校验失败"))) 129 | } 130 | 131 | response, err := service.LoginAcc(c, req) 132 | if err != nil { 133 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 134 | } 135 | 136 | return c.JSON(http.StatusOK, vo.Success(c, response)) 137 | } 138 | 139 | // LogoutAccount godoc 140 | // @Summary 用户登出 141 | // @Description 退出当前用户登录状态 142 | // @Tags 账户 143 | // @Produce json 144 | // @Success 200 {object} vo.Result{data=string} "登出成功" 145 | // @Failure 401 {object} vo.Result "未授权" 146 | // @Failure 500 {object} vo.Result "服务器错误" 147 | // @Security BearerAuth 148 | // @Router /account/logoutAccount [post] 149 | // 参数: 150 | // - c: Echo 上下文 151 | // 152 | // 返回值: 153 | // - error: 操作过程中的错误 154 | func LogoutAccount(c echo.Context) error { 155 | if err := service.LogoutAcc(c); err != nil { 156 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 157 | } 158 | 159 | return c.JSON(http.StatusOK, vo.Success(c, "用户注销成功")) 160 | } 161 | 162 | // ResetPassword godoc 163 | // @Summary 重置密码 164 | // @Description 重置用户账户密码,支持邮箱验证码校验 165 | // @Tags 账户 166 | // @Accept json 167 | // @Produce json 168 | // @Param request body dto.ResetPwdRequest true "重置密码信息" 169 | // @Success 200 {object} vo.Result{data=string} "密码重置成功" 170 | // @Failure 400 {object} vo.Result "参数错误,验证码校验失败" 171 | // @Failure 401 {object} vo.Result "未授权,用户未登录" 172 | // @Failure 500 {object} vo.Result "服务器错误" 173 | // @Security BearerAuth 174 | // @Router /account/resetPassword [post] 175 | // 参数: 176 | // - c: Echo 上下文 177 | // 178 | // 返回值: 179 | // - error: 操作过程中的错误 180 | func ResetPassword(c echo.Context) error { 181 | req := new(dto.ResetPwdRequest) 182 | if err := c.Bind(req); err != nil { 183 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 184 | } 185 | 186 | errors := utils.Validator(*req) 187 | if errors != nil { 188 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST, "请求参数校验失败"))) 189 | } 190 | 191 | if !verification.VerifyEmailCode(c, req.EmailVerificationCode, req.Email) { 192 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST, "邮箱验证码校验失败"))) 193 | } 194 | 195 | err := service.ResetPassword(c, req) 196 | if err != nil { 197 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 198 | } 199 | 200 | return c.JSON(http.StatusOK, vo.Success(c, "密码重置成功")) 201 | } 202 | -------------------------------------------------------------------------------- /pkg/serve/controller/account/dto/README.md: -------------------------------------------------------------------------------- 1 | 账户模块 DTO 2 | -------------------------------------------------------------------------------- /pkg/serve/controller/account/dto/get_acc_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供账户相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // GetAccountRequest 获取账户信息请求体 7 | // @Description 请求获取账户信息时所需参数 8 | // @Param email body string true "用户邮箱" 9 | type GetAccountRequest struct { 10 | Email string `json:"email" xml:"email" form:"email" query:"email" validate:"required,email"` 11 | } 12 | -------------------------------------------------------------------------------- /pkg/serve/controller/account/dto/login_acc_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供账户相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // LoginRequest 用户登录请求体 7 | // @Description 用户登录请求所需参数 8 | // @Param email body string true "用户邮箱" 9 | // @Param password body string true "用户密码" 10 | // @Param img_verification_code body string true "图片验证码" 11 | type LoginRequest struct { 12 | Email string `json:"email" xml:"email" form:"email" query:"email" validate:"required,email"` 13 | Password string `json:"password" xml:"password" form:"password" query:"password" validate:"required"` 14 | ImgVerificationCode string `json:"img_verification_code" xml:"img_verification_code" form:"img_verification_code" query:"img_verification_code" validate:"required"` 15 | } 16 | -------------------------------------------------------------------------------- /pkg/serve/controller/account/dto/register_acc_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供账户相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // RegisterRequest 用户注册请求体 7 | // @Description 用户注册所需参数 8 | // @Param email body string true "用户邮箱" 9 | // @Param phone body string true "用户手机号" 10 | // @Param nickname body string true "用户昵称" 11 | // @Param password body string true "用户密码" 12 | // @Param email_verification_code body string true "用户邮箱验证码" 13 | // @Param img_verification_code body string true "用户图片验证码" 14 | type RegisterRequest struct { 15 | Email string `json:"email" xml:"email" form:"email" query:"email" validate:"required"` 16 | Phone string `json:"phone" xml:"phone" form:"phone" query:"phone" default:""` 17 | Nickname string `json:"nickname" xml:"nickname" form:"nickname" query:"nickname" validate:"required,min=1,max=20"` 18 | Password string `json:"password" xml:"password" form:"password" query:"password" validate:"required,min=6,max=20"` 19 | EmailVerificationCode string `json:"email_verification_code" xml:"email_verification_code" form:"email_verification_code" query:"email_verification_code" validate:"required"` 20 | ImgVerificationCode string `json:"img_verification_code" xml:"img_verification_code" form:"img_verification_code" query:"img_verification_code" validate:"required"` 21 | } 22 | -------------------------------------------------------------------------------- /pkg/serve/controller/account/dto/reset_acc_pwd_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供账户相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // ResetPwdRequest 重置密码请求体 7 | // @Description 用户重置密码所需参数 8 | // @Param email body string true "用户邮箱" 9 | // @Param new_password body string true "新密码" 10 | // @Param again_new_password body string true "再次输入新密码" 11 | // @Param email_verification_code body string true "邮箱验证码" 12 | type ResetPwdRequest struct { 13 | Email string `json:"email" xml:"email" form:"email" query:"email" validate:"required,email"` 14 | NewPassword string `json:"new_password" xml:"new_password" form:"new_password" query:"new_password" validate:"required,min=6,max=20"` 15 | AgainNewPassword string `json:"again_new_password" xml:"again_new_password" form:"again_new_password" query:"again_new_password" validate:"required,min=6,max=20"` 16 | EmailVerificationCode string `json:"email_verification_code" xml:"email_verification_code" form:"email_verification_code" query:"email_verification_code" validate:"required"` 17 | } 18 | -------------------------------------------------------------------------------- /pkg/serve/controller/category/category.go: -------------------------------------------------------------------------------- 1 | // Package category 提供类目相关的HTTP接口处理 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package category 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | bizErr "jank.com/jank_blog/internal/error" 12 | "jank.com/jank_blog/internal/utils" 13 | "jank.com/jank_blog/pkg/serve/controller/category/dto" 14 | service "jank.com/jank_blog/pkg/serve/service/category" 15 | "jank.com/jank_blog/pkg/vo" 16 | ) 17 | 18 | // GetOneCategory godoc 19 | // @Summary 获取单个类目详情 20 | // @Description 根据类目 ID 获取单个类目的详细信息 21 | // @Tags 类目 22 | // @Accept json 23 | // @Produce json 24 | // @Param id path int true "类目ID" 25 | // @Success 200 {object} vo.Result{data=category.CategoriesVO} "获取成功" 26 | // @Failure 400 {object} vo.Result "请求参数错误" 27 | // @Failure 404 {object} vo.Result "类目不存在" 28 | // @Router /category/getOneCategory [get] 29 | func GetOneCategory(c echo.Context) error { 30 | req := new(dto.GetOneCategoryRequest) 31 | if err := c.Bind(req); err != nil { 32 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 33 | } 34 | 35 | errors := utils.Validator(*req) 36 | if errors != nil { 37 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 38 | } 39 | 40 | category, err := service.GetCategoryByID(c, req) 41 | if err != nil { 42 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 43 | } 44 | 45 | return c.JSON(http.StatusOK, vo.Success(c, category)) 46 | } 47 | 48 | // GetCategoryTree godoc 49 | // @Summary 获取类目树 50 | // @Description 获取类目树 51 | // @Tags 类目 52 | // @Accept json 53 | // @Produce json 54 | // @Success 200 {object} vo.Result{data=[]category.CategoriesVO} "获取成功" 55 | // @Failure 500 {object} vo.Result "服务器错误" 56 | // @Router /category/getCategoryTree [get] 57 | func GetCategoryTree(c echo.Context) error { 58 | categories, err := service.GetCategoryTree(c) 59 | if err != nil { 60 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 61 | } 62 | 63 | return c.JSON(http.StatusOK, vo.Success(c, categories)) 64 | } 65 | 66 | // GetCategoryChildrenTree godoc 67 | // @Summary 获取子类目树 68 | // @Description 根据类目 ID 获取子类目树 69 | // @Tags 类目 70 | // @Accept json 71 | // @Produce json 72 | // @Param id path int true "类目ID" 73 | // @Success 200 {object} vo.Result{data=[]category.CategoriesVO} "获取成功" 74 | // @Failure 400 {object} vo.Result "请求参数错误" 75 | // @Failure 404 {object} vo.Result "类目不存在" 76 | // @Failure 500 {object} vo.Result "服务器错误" 77 | // @Router /category/getCategoryChildrenTree [post] 78 | func GetCategoryChildrenTree(c echo.Context) error { 79 | req := new(dto.GetOneCategoryRequest) 80 | if err := c.Bind(req); err != nil { 81 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 82 | } 83 | 84 | errors := utils.Validator(*req) 85 | if errors != nil { 86 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 87 | } 88 | 89 | childrenCategories, err := service.GetCategoryChildrenByID(c, req) 90 | if err != nil { 91 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 92 | } 93 | 94 | return c.JSON(http.StatusOK, vo.Success(c, childrenCategories)) 95 | } 96 | 97 | // CreateOneCategory godoc 98 | // @Summary 创建类目 99 | // @Description 创建新的类目 100 | // @Tags 类目 101 | // @Accept json 102 | // @Produce json 103 | // @Param request body dto.CreateOneCategoryRequest true "创建类目请求参数" 104 | // @Success 200 {object} vo.Result{data=category.CategoriesVO} "创建成功" 105 | // @Failure 400 {object} vo.Result "请求参数错误" 106 | // @Security BearerAuth 107 | // @Router /category/createOneCategory [post] 108 | func CreateOneCategory(c echo.Context) error { 109 | req := new(dto.CreateOneCategoryRequest) 110 | if err := c.Bind(req); err != nil { 111 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 112 | } 113 | 114 | errors := utils.Validator(*req) 115 | if errors != nil { 116 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 117 | } 118 | 119 | createdCategory, err := service.CreateCategory(c, req) 120 | if err != nil { 121 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 122 | } 123 | 124 | return c.JSON(http.StatusOK, vo.Success(c, createdCategory)) 125 | } 126 | 127 | // UpdateOneCategory godoc 128 | // @Summary 更新类目 129 | // @Description 更新已存在的类目信息 130 | // @Tags 类目 131 | // @Accept json 132 | // @Produce json 133 | // @Param id path int true "类目ID" 134 | // @Param request body dto.UpdateOneCategoryRequest true "更新类目请求参数" 135 | // @Success 200 {object} vo.Result{data=category.CategoriesVO} "更新成功" 136 | // @Failure 400 {object} vo.Result "请求参数错误" 137 | // @Failure 404 {object} vo.Result "类目不存在" 138 | // @Failure 500 {object} vo.Result "服务器错误" 139 | // @Security BearerAuth 140 | // @Router /category/updateOneCategory [post] 141 | func UpdateOneCategory(c echo.Context) error { 142 | req := new(dto.UpdateOneCategoryRequest) 143 | if err := c.Bind(req); err != nil { 144 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 145 | } 146 | 147 | errors := utils.Validator(*req) 148 | if errors != nil { 149 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 150 | } 151 | 152 | updatedCategory, err := service.UpdateCategory(c, req) 153 | if err != nil { 154 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 155 | } 156 | 157 | return c.JSON(http.StatusOK, vo.Success(c, updatedCategory)) 158 | } 159 | 160 | // DeleteOneCategory godoc 161 | // @Summary 删除类目 162 | // @Description 根据类目 ID 删除类目 163 | // @Tags 类目 164 | // @Accept json 165 | // @Produce json 166 | // @Param id path int true "类目ID" 167 | // @Success 200 {object} vo.Result{data=category.CategoriesVO} "删除成功" 168 | // @Failure 400 {object} vo.Result "请求参数错误" 169 | // @Failure 404 {object} vo.Result "类目不存在" 170 | // @Failure 500 {object} vo.Result "服务器错误" 171 | // @Security BearerAuth 172 | // @Router /category/deleteOneCategory [post] 173 | func DeleteOneCategory(c echo.Context) error { 174 | req := new(dto.DeleteOneCategoryRequest) 175 | if err := c.Bind(req); err != nil { 176 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 177 | } 178 | 179 | errors := utils.Validator(*req) 180 | if errors != nil { 181 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 182 | } 183 | 184 | category, err := service.DeleteCategory(c, req) 185 | if err != nil { 186 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 187 | } 188 | 189 | return c.JSON(http.StatusOK, vo.Success(c, category)) 190 | } 191 | -------------------------------------------------------------------------------- /pkg/serve/controller/category/dto/create_category_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供类目相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // CreateOneCategoryRequest 创建类目请求 7 | // @Param name body string true "类目名称" 8 | // @Param description body string false "类目描述" 9 | // @Param parent_id body int64 false "父类目ID" 10 | type CreateOneCategoryRequest struct { 11 | Name string `json:"name" xml:"name" form:"name" query:"name" validate:"required,min=1"` 12 | Description string `json:"description" xml:"description" form:"description" query:"description" default:""` 13 | ParentID int64 `json:"parent_id" xml:"parent_id" form:"parent_id" query:"parent_id" validate:"omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /pkg/serve/controller/category/dto/delete_category_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供类目相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // DeleteOneCategoryRequest 删除类目请求 7 | // @Param id path int64 true "类目ID" 8 | type DeleteOneCategoryRequest struct { 9 | ID int64 `json:"id" xml:"id" form:"id" query:"id" validate:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/serve/controller/category/dto/get_category_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供类目相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // GetOneCategoryRequest 更新类目请求 7 | // @Param id path int true "类目ID" 8 | type GetOneCategoryRequest struct { 9 | ID int64 `json:"id" xml:"id" form:"id" query:"id" validate:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/serve/controller/category/dto/update_category_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供类目相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // UpdateOneCategoryRequest 更新类目请求 7 | // @Param id path int64 true "类目ID" 8 | // @Param name body string true "类目名称" 9 | // @Param description body string false "类目描述" 10 | // @Param parent_id body int64 false "父类目ID" 11 | // @Param path body string false "类目路径" 12 | // @Param children body array false "子类目" 13 | type UpdateOneCategoryRequest struct { 14 | ID int64 `json:"id" xml:"id" form:"id" query:"id" validate:"required"` 15 | Name string `json:"name" xml:"name" form:"name" query:"name" validate:"required,min=1,max=255"` 16 | Description string `json:"description" xml:"description" form:"description" query:"description" default:""` 17 | ParentID int64 `json:"parent_id" xml:"parent_id" form:"parent_id" query:"parent_id" validate:"omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /pkg/serve/controller/comment/comment.go: -------------------------------------------------------------------------------- 1 | // Package comment 提供评论相关的HTTP接口处理 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package comment 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | bizErr "jank.com/jank_blog/internal/error" 12 | "jank.com/jank_blog/internal/utils" 13 | "jank.com/jank_blog/pkg/serve/controller/comment/dto" 14 | service "jank.com/jank_blog/pkg/serve/service/comment" 15 | "jank.com/jank_blog/pkg/vo" 16 | ) 17 | 18 | // GetOneComment godoc 19 | // @Summary 获取评论详情 20 | // @Description 根据评论 ID 获取单个评论以及子评论 21 | // @Tags 评论 22 | // @Accept json 23 | // @Produce json 24 | // @Param id query int true "评论ID" 25 | // @Success 200 {object} vo.Result{data=comment.CommentsVO} "获取成功" 26 | // @Failure 400 {object} vo.Result "请求参数错误" 27 | // @Failure 404 {object} vo.Result "评论不存在" 28 | // @Router /comment/getOneComment [get] 29 | func GetOneComment(c echo.Context) error { 30 | req := new(dto.GetOneCommentRequest) 31 | if err := c.Bind(req); err != nil { 32 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 33 | } 34 | 35 | errors := utils.Validator(*req) 36 | if errors != nil { 37 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 38 | } 39 | 40 | comment, err := service.GetCommentWithReplies(c, req) 41 | if err != nil { 42 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 43 | } 44 | 45 | return c.JSON(http.StatusOK, vo.Success(c, comment)) 46 | } 47 | 48 | // GetCommentGraph godoc 49 | // @Summary 获取评论图 50 | // @Description 根据文章 ID 获取评论图结构 51 | // @Tags 评论 52 | // @Accept json 53 | // @Produce json 54 | // @Param post_id query int true "文章ID" 55 | // @Success 200 {object} vo.Result{data=[]comment.CommentsVO} "获取成功" 56 | // @Failure 500 {object} vo.Result "服务器错误" 57 | // @Router /comment/getOneComment [get] 58 | func GetCommentGraph(c echo.Context) error { 59 | req := new(dto.GetCommentGraphRequest) 60 | if err := c.Bind(req); err != nil { 61 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 62 | } 63 | 64 | errors := utils.Validator(*req) 65 | if errors != nil { 66 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 67 | } 68 | 69 | comments, err := service.GetCommentGraphByPostID(c, req) 70 | if err != nil { 71 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 72 | } 73 | 74 | return c.JSON(http.StatusOK, vo.Success(c, comments)) 75 | } 76 | 77 | // CreateOneComment godoc 78 | // @Summary 创建评论 79 | // @Description 创建一条新的评论 80 | // @Tags 评论 81 | // @Accept json 82 | // @Produce json 83 | // @Param request body dto.CreateCommentRequest true "创建评论请求参数" 84 | // @Success 200 {object} vo.Result{data=comment.CommentsVO} "创建成功" 85 | // @Failure 400 {object} vo.Result "请求参数错误" 86 | // @Router /comment/createOneComment [post] 87 | func CreateOneComment(c echo.Context) error { 88 | req := new(dto.CreateCommentRequest) 89 | if err := c.Bind(req); err != nil { 90 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 91 | } 92 | 93 | errors := utils.Validator(*req) 94 | if errors != nil { 95 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 96 | } 97 | 98 | comment, err := service.CreateComment(c, req) 99 | if err != nil { 100 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 101 | } 102 | 103 | return c.JSON(http.StatusOK, vo.Success(c, comment)) 104 | } 105 | 106 | // DeleteOneComment godoc 107 | // @Summary 软删除评论 108 | // @Description 通过评论 ID 进行软删除 109 | // @Tags 评论 110 | // @Accept json 111 | // @Produce json 112 | // @Param id path int true "评论ID" 113 | // @Success 200 {object} vo.Result{data=comment.CommentsVO} "软删除成功" 114 | // @Failure 400 {object} vo.Result "请求参数错误" 115 | // @Failure 404 {object} vo.Result "评论不存在" 116 | // @Router /comment/deleteOneComment [post] 117 | func DeleteOneComment(c echo.Context) error { 118 | req := new(dto.DeleteCommentRequest) 119 | if err := c.Bind(req); err != nil { 120 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 121 | } 122 | 123 | errors := utils.Validator(*req) 124 | if errors != nil { 125 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 126 | } 127 | 128 | comment, err := service.DeleteComment(c, req) 129 | if err != nil { 130 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 131 | } 132 | 133 | return c.JSON(http.StatusOK, vo.Success(c, comment)) 134 | } 135 | -------------------------------------------------------------------------------- /pkg/serve/controller/comment/dto/create_comment_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供评论相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // CreateCommentRequest 创建评论请求 7 | // @Param content body string true "评论内容" 8 | // @Param user_id body int64 true "用户ID" 9 | // @Param post_id body int64 true "文章ID" 10 | // @Param reply_to_comment_id body int64 false "回复的评论ID" 11 | type CreateCommentRequest struct { 12 | Content string `json:"content" xml:"content" form:"content" query:"content" validate:"required,min=1,max=1024"` 13 | UserId int64 `json:"user_id" xml:"user_id" form:"user_id" query:"user_id" validate:"required"` 14 | PostId int64 `json:"post_id" xml:"post_id" form:"post_id" query:"post_id" validate:"required"` 15 | ReplyToCommentId int64 `json:"reply_to_comment_id" xml:"reply_to_comment_id" form:"reply_to_comment_id" query:"reply_to_comment_id" validate:"omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /pkg/serve/controller/comment/dto/delete_comment_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供评论相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // DeleteCommentRequest 删除评论请求 7 | // @Param id path int64 true "评论ID" 8 | type DeleteCommentRequest struct { 9 | ID int64 `json:"id" xml:"id" form:"id" query:"id" validate:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/serve/controller/comment/dto/get_comment_graph_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供评论相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // GetCommentGraphRequest 获取评论请求 7 | // @Param post_id path int true "帖子ID" 8 | type GetCommentGraphRequest struct { 9 | PostID int64 `json:"post_id" xml:"post_id" form:"post_id" query:"post_id" validate:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/serve/controller/comment/dto/get_one_comment.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供评论相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // GetOneCommentRequest 获取评论请求 7 | // @Param comment_id path int true "评论ID" 8 | type GetOneCommentRequest struct { 9 | CommentID int64 `json:"comment_id" xml:"comment_id" form:"comment_id" query:"comment_id" validate:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/serve/controller/post/dto/README.md: -------------------------------------------------------------------------------- 1 | 文章模块 DTO 2 | -------------------------------------------------------------------------------- /pkg/serve/controller/post/dto/create_post_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供文章相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // CreateOnePostRequest 发布文章的请求结构体 7 | // @Param title body string true "文章标题" 8 | // @Param image body string true "文章图片(可选)" 9 | // @Param visibility body string true "文章可见性(可选,默认 private)" 10 | // @Param content_html body string true "文章内容(markdown格式)" 11 | // @Param category_id body int64 true "文章分类ID" 12 | type CreateOnePostRequest struct { 13 | Title string `json:"title" xml:"title" form:"title" query:"title" validate:"required,min=1,max=225"` 14 | Image string `json:"image" xml:"image" form:"image" query:"image" default:""` 15 | Visibility bool `json:"visibility" xml:"visibility" form:"visibility" query:"visibility" validate:"omitempty,boolean" default:"false"` 16 | ContentMarkdown string `json:"content_markdown" xml:"content_markdown" form:"content_markdown" query:"content_markdown" default:""` 17 | CategoryID int64 `json:"category_id" xml:"category_id" form:"category_id" query:"category_id" validate:"omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /pkg/serve/controller/post/dto/delete_post_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供文章相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // DeleteOnePostRequest 文章删除请求 7 | // @Param id path int true "文章 ID" 8 | type DeleteOnePostRequest struct { 9 | ID int64 `json:"id" xml:"id" form:"id" query:"id" validate:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/serve/controller/post/dto/get_post_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供文章相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // GetOnePostRequest 获取文章的请求结构体 7 | // @Param id path string true "文章 ID" 8 | type GetOnePostRequest struct { 9 | ID int64 `json:"id" xml:"id" form:"id" query:"id" validate:"omitempty" default:"0"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/serve/controller/post/dto/update_post_request.go: -------------------------------------------------------------------------------- 1 | // Package dto 提供文章相关的数据传输对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package dto 5 | 6 | // UpdateOnePostRequest 更新文章请求参数结构体 7 | // @Param id body int true "文章 ID" 8 | // @Param title body string false "文章标题" 9 | // @Param image body string false "文章图片(可选)" 10 | // @Param visibility body string false "文章可见性(可选)" 11 | // @Param content_markdown body string false "文章内容(markdown格式)" 12 | // @Param category_id body int64 false "文章分类ID列表(可选)" 13 | type UpdateOnePostRequest struct { 14 | ID int64 `json:"id" xml:"id" form:"id" query:"id" validate:"required"` 15 | Title string `json:"title" xml:"title" form:"title" query:"title" validate:"min=0,max=255" default:""` 16 | Image string `json:"image" xml:"image" form:"image" query:"image" default:""` 17 | Visibility bool `json:"visibility" xml:"visibility" form:"visibility" query:"visibility" validate:"omitempty,boolean" default:"false"` 18 | ContentMarkdown string `json:"content_markdown" xml:"content_markdown" form:"content_markdown" query:"content_markdown" default:""` 19 | CategoryID int64 `json:"category_id" xml:"category_id" form:"category_id" query:"category_id" validate:"omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /pkg/serve/controller/post/post.go: -------------------------------------------------------------------------------- 1 | // Package post 提供文章相关的HTTP接口处理 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package post 5 | 6 | import ( 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/labstack/echo/v4" 11 | 12 | bizErr "jank.com/jank_blog/internal/error" 13 | "jank.com/jank_blog/internal/utils" 14 | "jank.com/jank_blog/pkg/serve/controller/post/dto" 15 | service "jank.com/jank_blog/pkg/serve/service/post" 16 | "jank.com/jank_blog/pkg/vo" 17 | ) 18 | 19 | // GetOnePost godoc 20 | // @Summary 获取文章详情 21 | // @Description 根据文章 ID 获取文章的详细信息 22 | // @Tags 文章 23 | // @Accept json 24 | // @Produce json 25 | // @Param request body dto.GetOnePostRequest true "获取文章请求参数" 26 | // @Success 200 {object} vo.Result{data=post.PostsVO} "获取成功" 27 | // @Failure 400 {object} vo.Result "请求参数错误" 28 | // @Failure 404 {object} vo.Result "文章不存在" 29 | // @Failure 500 {object} vo.Result "服务器错误" 30 | // @Router /post/getOnePost [get] 31 | func GetOnePost(c echo.Context) error { 32 | req := new(dto.GetOnePostRequest) 33 | if err := c.Bind(req); err != nil { 34 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 35 | } 36 | 37 | errors := utils.Validator(*req) 38 | if errors != nil { 39 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 40 | } 41 | 42 | pos, err := service.GetOnePostByID(c, req) 43 | if err != nil { 44 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 45 | } 46 | 47 | return c.JSON(http.StatusOK, vo.Success(c, pos)) 48 | } 49 | 50 | // GetAllPosts godoc 51 | // @Summary 获取文章列表 52 | // @Description 获取所有的文章列表,按创建时间倒序排序 53 | // @Tags 文章 54 | // @Accept json 55 | // @Produce json 56 | // @Param page query int true "页码" 57 | // @Param page_size query int true "每页条数" 58 | // @Success 200 {object} vo.Result{data=[]post.PostsVO} "获取成功" 59 | // @Failure 500 {object} vo.Result "服务器错误" 60 | // @Router /post/getAllPosts [get] 61 | func GetAllPosts(c echo.Context) error { 62 | page, err := strconv.Atoi(c.QueryParam("page")) 63 | if err != nil || page < 1 { 64 | page = 1 65 | } 66 | 67 | pageSize, err := strconv.Atoi(c.QueryParam("page_size")) 68 | if err != nil || pageSize < 1 { 69 | pageSize = 5 70 | } 71 | 72 | posts, err := service.GetAllPostsWithPagingAndFormat(c, page, pageSize) 73 | if err != nil { 74 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 75 | } 76 | 77 | return c.JSON(http.StatusOK, vo.Success(c, posts)) 78 | } 79 | 80 | // CreateOnePost godoc 81 | // @Summary 创建文章 82 | // @Description 创建新的文章,支持 Markdown 格式内容,系统会自动转换为 HTML 83 | // @Tags 文章 84 | // @Accept json 85 | // @Produce json 86 | // @Param request body dto.CreateOnePostRequest true "创建文章请求参数" 87 | // @Success 200 {object} vo.Result{data=post.PostsVO} "创建成功" 88 | // @Failure 400 {object} vo.Result "请求参数错误" 89 | // @Failure 500 {object} vo.Result "服务器错误" 90 | // @Security BearerAuth 91 | // @Router /post/createOnePost [post] 92 | func CreateOnePost(c echo.Context) error { 93 | req := new(dto.CreateOnePostRequest) 94 | if err := c.Bind(req); err != nil { 95 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 96 | } 97 | 98 | errors := utils.Validator(*req) 99 | if errors != nil { 100 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 101 | } 102 | 103 | createdPost, err := service.CreateOnePost(c, req) 104 | if err != nil { 105 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 106 | } 107 | 108 | return c.JSON(http.StatusOK, vo.Success(c, createdPost)) 109 | } 110 | 111 | // UpdateOnePost godoc 112 | // @Summary 更新文章 113 | // @Description 更新已存在的文章内容 114 | // @Tags 文章 115 | // @Accept json 116 | // @Produce json 117 | // @Param request body dto.UpdateOnePostRequest true "更新文章请求参数" 118 | // @Success 200 {object} vo.Result{data=post.PostsVO} "更新成功" 119 | // @Failure 400 {object} vo.Result "请求参数错误" 120 | // @Failure 404 {object} vo.Result "文章不存在" 121 | // @Failure 500 {object} vo.Result "服务器错误" 122 | // @Security BearerAuth 123 | // @Router /post/updateOnePost [post] 124 | func UpdateOnePost(c echo.Context) error { 125 | req := new(dto.UpdateOnePostRequest) 126 | if err := c.Bind(req); err != nil { 127 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 128 | } 129 | 130 | errors := utils.Validator(*req) 131 | if errors != nil { 132 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 133 | } 134 | 135 | updatedPost, err := service.UpdateOnePost(c, req) 136 | if err != nil { 137 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 138 | } 139 | 140 | return c.JSON(http.StatusOK, vo.Success(c, updatedPost)) 141 | } 142 | 143 | // DeleteOnePost godoc 144 | // @Summary 删除文章 145 | // @Description 根据文章 ID 删除指定文章 146 | // @Tags 文章 147 | // @Accept json 148 | // @Produce json 149 | // @Param request body dto.DeleteOnePostRequest true "删除文章请求参数" 150 | // @Success 200 {object} vo.Result "删除成功" 151 | // @Failure 400 {object} vo.Result "请求参数错误" 152 | // @Failure 404 {object} vo.Result "文章不存在" 153 | // @Failure 500 {object} vo.Result "服务器错误" 154 | // @Security BearerAuth 155 | // @Router /post/deleteOnePost [post] 156 | func DeleteOnePost(c echo.Context) error { 157 | req := new(dto.DeleteOnePostRequest) 158 | if err := c.Bind(&req); err != nil { 159 | return c.JSON(http.StatusBadRequest, vo.Fail(c, err, bizErr.New(bizErr.BAD_REQUEST, err.Error()))) 160 | } 161 | 162 | errors := utils.Validator(*req) 163 | if errors != nil { 164 | return c.JSON(http.StatusBadRequest, vo.Fail(c, errors, bizErr.New(bizErr.BAD_REQUEST))) 165 | } 166 | 167 | err := service.DeleteOnePost(c, req) 168 | if err != nil { 169 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR, err.Error()))) 170 | } 171 | 172 | return c.JSON(http.StatusOK, vo.Success(c, "文章删除成功")) 173 | } 174 | -------------------------------------------------------------------------------- /pkg/serve/controller/test/README.md: -------------------------------------------------------------------------------- 1 | 测试接口 2 | -------------------------------------------------------------------------------- /pkg/serve/controller/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/labstack/echo/v4" 8 | 9 | bizErr "jank.com/jank_blog/internal/error" 10 | "jank.com/jank_blog/internal/global" 11 | "jank.com/jank_blog/internal/utils" 12 | "jank.com/jank_blog/pkg/vo" 13 | ) 14 | 15 | // TestPing @Summary Ping API 16 | // @Description 测试接口 17 | // @Tags test 18 | // @Accept json 19 | // @Produce json 20 | // @Success 200 {string} string "Pong successfully!\n" 21 | // @Router /test/testPing [get] 22 | func TestPing(c echo.Context) error { 23 | utils.BizLogger(c).Info("Ping...") 24 | return c.String(http.StatusOK, "Pong successfully!\n") 25 | } 26 | 27 | // TestHello @Summary Hello API 28 | // @Description 测试接口 29 | // @Tags test 30 | // @Accept json 31 | // @Produce json 32 | // @Success 200 {string} string "Hello, Jank 🎉!\n" 33 | // @Router /test/testHello [get] 34 | func TestHello(c echo.Context) error { 35 | utils.BizLogger(c).Info("Hello, Jank!") 36 | return c.String(http.StatusOK, "Hello, Jank 🎉!\n") 37 | } 38 | 39 | // TestLogger @Summary 测试日志接口 40 | // @Description 用于测试日志功能 41 | // @Tags test 42 | // @Accept json 43 | // @Produce json 44 | // @Success 200 {string} string "测试日志成功!" 45 | // @Router /test/testLogger [get] 46 | func TestLogger(c echo.Context) error { 47 | utils.BizLogger(c).Infof("测试日志...") 48 | return c.String(http.StatusOK, "测试日志成功!") 49 | } 50 | 51 | // TestRedis @Summary 测试 Redis 接口 52 | // @Description 用于测试 Redis 功能 53 | // @Tags test 54 | // @Accept json 55 | // @Produce json 56 | // @Success 200 {string} string "测试缓存功能完成!" 57 | // @Router /test/testRedis [get] 58 | func TestRedis(c echo.Context) error { 59 | utils.BizLogger(c).Infof("开始写入缓存...") 60 | err := global.RedisClient.Set(c.Request().Context(), "TEST:", "测试 value", 0).Err() 61 | if err != nil { 62 | utils.BizLogger(c).Errorf("测试写入缓存失败: %v", err) 63 | return err 64 | } 65 | utils.BizLogger(c).Infof("写入缓存成功...") 66 | 67 | utils.BizLogger(c).Infof("开始读取缓存...") 68 | articlesCache, err := global.RedisClient.Get(c.Request().Context(), "TEST:").Result() 69 | if err != nil { 70 | utils.BizLogger(c).Errorf("测试读取缓存失败: %v", err) 71 | return err 72 | } 73 | utils.BizLogger(c).Infof("读取缓存成功, key: %s , value: %s", "TEST:", articlesCache) 74 | return c.String(http.StatusOK, "测试缓存功能完成!") 75 | } 76 | 77 | // TestSuccRes @Summary 测试成功响应接口 78 | // @Description 用于测试成功响应 79 | // @Tags test 80 | // @Accept json 81 | // @Produce json 82 | // @Success 200 {object} vo.Result "测试成功响应成功!" 83 | // @Router /test/testSuccessRes [get] 84 | func TestSuccRes(c echo.Context) error { 85 | utils.BizLogger(c).Info("测试成功响应...") 86 | return c.JSON(http.StatusOK, vo.Success(c, "测试成功响应成功!")) 87 | } 88 | 89 | // TestErrRes @Summary 测试错误响应接口 90 | // @Description 用于测试错误响应 91 | // @Tags test 92 | // @Accept json 93 | // @Produce json 94 | // @Success 500 {object} vo.Result 95 | // @Router /test/testErrRes [get] 96 | func TestErrRes(c echo.Context) error { 97 | utils.BizLogger(c).Info("测试失败响应...") 98 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, nil, bizErr.New(bizErr.SERVER_ERR))) 99 | } 100 | 101 | // TestErrorMiddleware @Summary 测试错误处理中间件接口 102 | // @Description 用于测试错误中间件 103 | // @Tags test 104 | // @Accept json 105 | // @Produce json 106 | // @Success 500 {string} nil 107 | // @Router /test/testErrorMiddleware [get] 108 | func TestErrorMiddleware(c echo.Context) error { 109 | utils.BizLogger(c).Info("测试错误处理中间件...") 110 | panic("测试错误处理中间件...") 111 | } 112 | 113 | // TestLongReq @Summary 长时间请求接口 114 | // @Description 模拟一个耗时请求 115 | // @Tags test 116 | // @Accept json 117 | // @Produce json 118 | // @Success 200 {string} string "模拟耗时请求处理完成!\n" 119 | // @Router /test/testLongReq [get] 120 | func TestLongReq(c echo.Context) error { 121 | utils.BizLogger(c).Info("开始测试耗时请求...") 122 | time.Sleep(20 * time.Second) 123 | return c.String(http.StatusOK, "模拟耗时请求处理完成!\n") 124 | } 125 | -------------------------------------------------------------------------------- /pkg/serve/controller/verification/verification.go: -------------------------------------------------------------------------------- 1 | // Package verification 提供验证码相关的HTTP接口处理 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package verification 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/labstack/echo/v4" 15 | 16 | bizErr "jank.com/jank_blog/internal/error" 17 | "jank.com/jank_blog/internal/global" 18 | "jank.com/jank_blog/internal/utils" 19 | "jank.com/jank_blog/pkg/vo" 20 | "jank.com/jank_blog/pkg/vo/verification" 21 | ) 22 | 23 | const ( 24 | EMAIL_VERIFICATION_CODE_CACHE_KEY_PREFIX = "EMAIL:VERIFICATION:CODE:" // 邮箱验证码缓存前缀 25 | EMAIL_VERIFICATION_CODE_CACHE_EXPIRATION = 3 * time.Minute // 邮箱验证码缓存过期时间 26 | IMG_VERIFICATION_CODE_CACHE_PREFIX = "IMG:VERIFICATION:CODE:CACHE:" // 图形验证码缓存前缀 27 | IMG_VERIFICATION_CODE_CACHE_EXPIRATION = 3 * time.Minute // 图形验证码缓存过期时间 28 | ) 29 | 30 | // SendImgVerificationCode godoc 31 | // @Summary 生成图形验证码并返回Base64编码 32 | // @Description 生成单个图形验证码并将其返回为Base64编码字符串,用户可以用该验证码进行校验。 33 | // @Tags 账户 34 | // @Accept json 35 | // @Produce json 36 | // @Param email query string true "邮箱地址,用于生成验证码" 37 | // @Success 200 {object} vo.Result{data=map[string]string} "成功返回验证码的Base64编码" 38 | // @Failure 400 {object} vo.Result{data=string} "请求参数错误,邮箱地址为空" 39 | // @Failure 500 {object} vo.Result{data=string} "服务器错误,生成验证码失败" 40 | // @Router /verification/sendImgVerificationCode [get] 41 | func SendImgVerificationCode(c echo.Context) error { 42 | email := c.QueryParam("email") 43 | if email == "" { 44 | utils.BizLogger(c).Errorf("请求参数错误,邮箱地址为空") 45 | return c.JSON(http.StatusBadRequest, vo.Fail(c, "请求参数错误,邮箱地址为空", bizErr.New(bizErr.BAD_REQUEST))) 46 | } 47 | 48 | key := IMG_VERIFICATION_CODE_CACHE_PREFIX + email 49 | 50 | // 生成单个图形验证码 51 | imgBase64, answer, err := utils.GenImgVerificationCode() 52 | if err != nil { 53 | utils.BizLogger(c).Errorf("生成图片验证码失败: %v", err) 54 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR))) 55 | } 56 | 57 | err = global.RedisClient.Set(context.Background(), key, answer, IMG_VERIFICATION_CODE_CACHE_EXPIRATION).Err() 58 | if err != nil { 59 | utils.BizLogger(c).Errorf("图形验证码写入缓存失败,key: %v, 错误: %v", key, err) 60 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR))) 61 | } 62 | 63 | return c.JSON(http.StatusOK, vo.Success(c, verification.ImgVerificationVO{ImgBase64: imgBase64})) 64 | } 65 | 66 | // SendEmailVerificationCode godoc 67 | // @Summary 发送邮箱验证码 68 | // @Description 向指定邮箱发送验证码,验证码有效期为3分钟 69 | // @Tags 账户 70 | // @Accept json 71 | // @Produce json 72 | // @Param email query string true "邮箱地址,用于发送验证码" 73 | // @Success 200 {object} vo.Result "邮箱验证码发送成功, 请注意查收邮件" 74 | // @Failure 400 {object} vo.Result "请求参数错误,邮箱地址为空" 75 | // @Failure 500 {object} vo.Result "服务器错误,邮箱验证码发送失败" 76 | // @Router /verification/sendEmailVerificationCode [get] 77 | func SendEmailVerificationCode(c echo.Context) error { 78 | email := c.QueryParam("email") 79 | if email == "" { 80 | utils.BizLogger(c).Errorf("请求参数错误,邮箱地址为空") 81 | return c.JSON(http.StatusBadRequest, vo.Fail(c, "请求参数错误,邮箱地址为空", bizErr.New(bizErr.BAD_REQUEST))) 82 | } 83 | 84 | if !utils.ValidEmail(email) { 85 | utils.BizLogger(c).Errorf("邮箱格式无效: %s", email) 86 | return c.JSON(http.StatusBadRequest, vo.Fail(c, "邮箱格式无效", bizErr.New(bizErr.BAD_REQUEST))) 87 | } 88 | 89 | key := EMAIL_VERIFICATION_CODE_CACHE_KEY_PREFIX + email 90 | 91 | // 检查验证码是否存在 92 | exists, err := global.RedisClient.Exists(context.Background(), key).Result() 93 | if err != nil { 94 | utils.BizLogger(c).Errorf("检查邮箱验证码是否有效失败: %v", err) 95 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR))) 96 | } 97 | if exists > 0 { 98 | return c.JSON(http.StatusBadRequest, vo.Fail(c, "邮箱验证码已存在", bizErr.New(bizErr.SERVER_ERR))) 99 | } 100 | 101 | // 生成并缓存验证码 102 | code := utils.NewRand() 103 | err = global.RedisClient.Set(context.Background(), key, strconv.Itoa(code), EMAIL_VERIFICATION_CODE_CACHE_EXPIRATION).Err() 104 | if err != nil { 105 | utils.BizLogger(c).Errorf("邮箱验证码写入缓存失败: %v", err) 106 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SERVER_ERR))) 107 | } 108 | 109 | // 发送验证码邮件 110 | expirationInMinutes := int(EMAIL_VERIFICATION_CODE_CACHE_EXPIRATION.Round(time.Minute).Minutes()) 111 | emailContent := fmt.Sprintf("您的注册验证码是: %d , 有效期为 %d 分钟。", code, expirationInMinutes) 112 | success, err := utils.SendEmail(emailContent, []string{email}) 113 | if !success { 114 | utils.BizLogger(c).Errorf("邮箱验证码发送失败,邮箱地址: %s, 错误: %v", email, err) 115 | global.RedisClient.Del(context.Background(), key) 116 | return c.JSON(http.StatusInternalServerError, vo.Fail(c, err, bizErr.New(bizErr.SEND_EMAIL_VERIFICATION_CODE_FAIL))) 117 | } 118 | 119 | return c.JSON(http.StatusOK, vo.Success(c, "邮箱验证码发送成功, 请注意查收!")) 120 | } 121 | 122 | // VerifyEmailCode 校验邮箱验证码 123 | // 参数: 124 | // - c: Echo 上下文 125 | // - code: 验证码 126 | // - email: 邮箱地址 127 | // 128 | // 返回值: 129 | // - bool: 验证成功返回 true,失败返回 false 130 | func VerifyEmailCode(c echo.Context, code, email string) bool { 131 | return verifyCode(c, code, email, EMAIL_VERIFICATION_CODE_CACHE_KEY_PREFIX) 132 | } 133 | 134 | // VerifyImgCode 校验图形验证码 135 | // 参数: 136 | // - c: Echo 上下文 137 | // - code: 验证码 138 | // - email: 邮箱地址 139 | // 140 | // 返回值: 141 | // - bool: 验证成功返回 true,失败返回 false 142 | func VerifyImgCode(c echo.Context, code, email string) bool { 143 | return verifyCode(c, code, email, IMG_VERIFICATION_CODE_CACHE_PREFIX) 144 | } 145 | 146 | // verifyCode 通用验证码校验 147 | // 参数: 148 | // - c: Echo 上下文 149 | // - code: 验证码 150 | // - email: 邮箱地址 151 | // - prefix: 缓存键前缀 152 | // 153 | // 返回值: 154 | // - bool: 验证成功返回 true,失败返回 false 155 | func verifyCode(c echo.Context, code, email, prefix string) bool { 156 | key := prefix + email 157 | 158 | storedCode, err := global.RedisClient.Get(c.Request().Context(), key).Result() 159 | if err != nil { 160 | if err.Error() == "redis: nil" { 161 | utils.BizLogger(c).Error("验证码不存在或已过期") 162 | } else { 163 | utils.BizLogger(c).Errorf("验证码校验失败: %v", err) 164 | } 165 | return false 166 | } 167 | 168 | storedCode = strings.ToUpper(strings.TrimSpace(storedCode)) 169 | code = strings.ToUpper(strings.TrimSpace(code)) 170 | 171 | if storedCode != code { 172 | utils.BizLogger(c).Error("用户验证码错误") 173 | return false 174 | } 175 | 176 | if err := global.RedisClient.Del(context.Background(), key).Err(); err != nil { 177 | utils.BizLogger(c).Errorf("删除验证码缓存失败: %v", err) 178 | } 179 | 180 | return true 181 | } 182 | -------------------------------------------------------------------------------- /pkg/serve/mapper/account.go: -------------------------------------------------------------------------------- 1 | // Package mapper 提供数据模型与数据库交互的映射层,处理账户相关数据操作 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package mapper 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | model "jank.com/jank_blog/internal/model/account" 12 | "jank.com/jank_blog/internal/utils" 13 | ) 14 | 15 | // GetTotalAccounts 获取系统中的总账户数 16 | // 参数: 17 | // - c: Echo 上下文 18 | // 19 | // 返回值: 20 | // - int64: 账户总数 21 | // - error: 操作过程中的错误 22 | func GetTotalAccounts(c echo.Context) (int64, error) { 23 | var count int64 24 | db := utils.GetDBFromContext(c) 25 | if err := db.Model(&model.Account{}).Where("deleted = ?", false).Count(&count).Error; err != nil { 26 | return 0, fmt.Errorf("获取用户总数失败: %w", err) 27 | } 28 | return count, nil 29 | } 30 | 31 | // GetAccountByEmail 根据邮箱获取用户账户信息 32 | // 参数: 33 | // - c: Echo 上下文 34 | // - email: 用户邮箱 35 | // 36 | // 返回值: 37 | // - *model.Account: 账户信息 38 | // - error: 操作过程中的错误 39 | func GetAccountByEmail(c echo.Context, email string) (*model.Account, error) { 40 | var user model.Account 41 | db := utils.GetDBFromContext(c) 42 | if err := db.Model(&model.Account{}).Where("email = ? AND deleted = ?", email, false).First(&user).Error; err != nil { 43 | return nil, fmt.Errorf("获取用户失败: %w", err) 44 | } 45 | return &user, nil 46 | } 47 | 48 | // GetAccountByAccountID 根据用户 ID 获取账户信息 49 | // 参数: 50 | // - c: Echo 上下文 51 | // - accountID: 账户 ID 52 | // 53 | // 返回值: 54 | // - *model.Account: 账户信息 55 | // - error: 操作过程中的错误 56 | func GetAccountByAccountID(c echo.Context, accountID int64) (*model.Account, error) { 57 | var user model.Account 58 | db := utils.GetDBFromContext(c) 59 | if err := db.Model(&model.Account{}).Where("id = ? AND deleted = ?", accountID, false).First(&user).Error; err != nil { 60 | return nil, fmt.Errorf("获取用户失败: %w", err) 61 | } 62 | return &user, nil 63 | } 64 | 65 | // CreateAccount 创建新用户 66 | // 参数: 67 | // - c: Echo 上下文 68 | // - acc: 账户信息 69 | // 70 | // 返回值: 71 | // - error: 操作过程中的错误 72 | func CreateAccount(c echo.Context, acc *model.Account) error { 73 | db := utils.GetDBFromContext(c) 74 | if err := db.Model(&model.Account{}).Create(acc).Error; err != nil { 75 | return fmt.Errorf("创建用户失败: %w", err) 76 | } 77 | return nil 78 | } 79 | 80 | // UpdateAccount 更新账户信息 81 | // 参数: 82 | // - c: Echo 上下文 83 | // - acc: 账户信息 84 | // 85 | // 返回值: 86 | // - error: 操作过程中的错误 87 | func UpdateAccount(c echo.Context, acc *model.Account) error { 88 | db := utils.GetDBFromContext(c) 89 | if err := db.Model(&model.Account{}).Save(acc).Error; err != nil { 90 | return fmt.Errorf("更新账户失败: %w", err) 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/serve/mapper/category.go: -------------------------------------------------------------------------------- 1 | // Package mapper 提供数据模型与数据库交互的映射层,处理类目相关数据操作 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package mapper 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | model "jank.com/jank_blog/internal/model/category" 12 | "jank.com/jank_blog/internal/utils" 13 | ) 14 | 15 | // GetCategoryByID 根据 ID 查找类目 16 | // 参数: 17 | // - c: Echo 上下文 18 | // - id: 类目 ID 19 | // 20 | // 返回值: 21 | // - *model.Category: 类目信息 22 | // - error: 操作过程中的错误 23 | func GetCategoryByID(c echo.Context, id int64) (*model.Category, error) { 24 | var cat model.Category 25 | db := utils.GetDBFromContext(c) 26 | if err := db.Model(&model.Category{}).Where("id = ? AND deleted = ?", id, false).First(&cat).Error; err != nil { 27 | return nil, fmt.Errorf("获取类目失败: %v", err) 28 | } 29 | return &cat, nil 30 | } 31 | 32 | // GetCategoriesByParentID 根据父类目 ID 查找直接子类目 33 | // 参数: 34 | // - c: Echo 上下文 35 | // - parentID: 父类目 ID 36 | // 37 | // 返回值: 38 | // - []*model.Category: 子类目列表 39 | // - error: 操作过程中的错误 40 | func GetCategoriesByParentID(c echo.Context, parentID int64) ([]*model.Category, error) { 41 | var categories []*model.Category 42 | db := utils.GetDBFromContext(c) 43 | if err := db.Model(&model.Category{}).Where("parent_id = ? AND deleted = ?", parentID, false).Find(&categories).Error; err != nil { 44 | return nil, fmt.Errorf("获取子类目失败: %v", err) 45 | } 46 | return categories, nil 47 | } 48 | 49 | // GetCategoriesByPath 根据路径获取所有子类目 50 | // 参数: 51 | // - c: Echo 上下文 52 | // - path: 类目路径 53 | // 54 | // 返回值: 55 | // - []*model.Category: 子类目列表 56 | // - error: 操作过程中的错误 57 | func GetCategoriesByPath(c echo.Context, path string) ([]*model.Category, error) { 58 | var categories []*model.Category 59 | db := utils.GetDBFromContext(c) 60 | 61 | // 如果路径为空,使用特殊查询条件只查询子类目 62 | if path == "" { 63 | if err := db.Model(&model.Category{}). 64 | Where("deleted = ?", false). 65 | Find(&categories).Error; err != nil { 66 | return nil, fmt.Errorf("获取路径下类目失败: %v", err) 67 | } 68 | } else { 69 | // 对于非空路径,确保只返回以该路径开头的类目 70 | if err := db.Model(&model.Category{}). 71 | Where("path LIKE ? AND deleted = ?", fmt.Sprintf("%s%%", path), false). 72 | Find(&categories).Error; err != nil { 73 | return nil, fmt.Errorf("获取路径下类目失败: %v", err) 74 | } 75 | } 76 | 77 | return categories, nil 78 | } 79 | 80 | // GetAllActivatedCategories 获取所有未删除的类目 81 | // 参数: 82 | // - c: Echo 上下文 83 | // 84 | // 返回值: 85 | // - []*model.Category: 类目列表 86 | // - error: 操作过程中的错误 87 | func GetAllActivatedCategories(c echo.Context) ([]*model.Category, error) { 88 | var categories []*model.Category 89 | db := utils.GetDBFromContext(c) 90 | if err := db.Model(&model.Category{}).Where("deleted = ?", false). 91 | Find(&categories).Error; err != nil { 92 | return nil, fmt.Errorf("获取所有类目失败: %v", err) 93 | } 94 | return categories, nil 95 | } 96 | 97 | // CreateCategory 将新类目保存到数据库 98 | // 参数: 99 | // - c: Echo 上下文 100 | // - newCategory: 类目信息 101 | // 102 | // 返回值: 103 | // - error: 操作过程中的错误 104 | func CreateCategory(c echo.Context, newCategory *model.Category) error { 105 | db := utils.GetDBFromContext(c) 106 | if err := db.Model(&model.Category{}).Create(newCategory).Error; err != nil { 107 | return fmt.Errorf("创建类目失败: %v", err) 108 | } 109 | return nil 110 | } 111 | 112 | // UpdateCategory 更新类目信息 113 | // 参数: 114 | // - c: Echo 上下文 115 | // - category: 类目信息 116 | // 117 | // 返回值: 118 | // - error: 操作过程中的错误 119 | func UpdateCategory(c echo.Context, category *model.Category) error { 120 | db := utils.GetDBFromContext(c) 121 | if err := db.Model(&model.Category{}).Save(category).Error; err != nil { 122 | return fmt.Errorf("更新类目失败: %v", err) 123 | } 124 | return nil 125 | } 126 | 127 | // DeleteCategoriesByPathSoftly 软删除类目及其子类目 128 | // 参数: 129 | // - c: Echo 上下文 130 | // - path: 类目路径 131 | // - id: 类目 ID 132 | // 133 | // 返回值: 134 | // - error: 操作过程中的错误 135 | func DeleteCategoriesByPathSoftly(c echo.Context, path string, id int64) error { 136 | db := utils.GetDBFromContext(c) 137 | if err := db.Model(&model.Category{}). 138 | Where("id = ? AND deleted = ?", id, false). 139 | Update("deleted", true).Error; err != nil { 140 | return fmt.Errorf("删除当前类目失败: %v", err) 141 | } 142 | 143 | if err := db.Model(&model.Category{}). 144 | Where("path LIKE ? AND deleted = ? AND path != ?", fmt.Sprintf("%s%%", path), false, path). 145 | Update("deleted", true).Error; err != nil { 146 | return fmt.Errorf("删除子类目失败: %v", err) 147 | } 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /pkg/serve/mapper/comment.go: -------------------------------------------------------------------------------- 1 | // Package mapper 提供数据模型与数据库交互的映射层,处理评论相关数据操作 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package mapper 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | model "jank.com/jank_blog/internal/model/comment" 12 | "jank.com/jank_blog/internal/utils" 13 | ) 14 | 15 | // CreateComment 保存评论到数据库 16 | // 参数: 17 | // - c: Echo 上下文 18 | // - comment: 评论信息 19 | // 20 | // 返回值: 21 | // - error: 操作过程中的错误 22 | func CreateComment(c echo.Context, comment *model.Comment) error { 23 | db := utils.GetDBFromContext(c) 24 | if err := db.Model(&model.Comment{}).Create(comment).Error; err != nil { 25 | return fmt.Errorf("创建评论失败: %w", err) 26 | } 27 | return nil 28 | } 29 | 30 | // GetCommentByID 根据 ID 查询评论 31 | // 参数: 32 | // - c: Echo 上下文 33 | // - id: 评论 ID 34 | // 35 | // 返回值: 36 | // - *model.Comment: 评论信息 37 | // - error: 操作过程中的错误 38 | func GetCommentByID(c echo.Context, id int64) (*model.Comment, error) { 39 | var comment model.Comment 40 | db := utils.GetDBFromContext(c) 41 | if err := db.Model(&model.Comment{}).Where("id = ? AND deleted = ?", id, false).First(&comment).Error; err != nil { 42 | return nil, fmt.Errorf("获取评论失败: %w", err) 43 | } 44 | return &comment, nil 45 | } 46 | 47 | // GetReplyByCommentID 获取评论的所有回复 48 | // 参数: 49 | // - c: Echo 上下文 50 | // - id: 评论 ID 51 | // 52 | // 返回值: 53 | // - []*model.Comment: 回复列表 54 | // - error: 操作过程中的错误 55 | func GetReplyByCommentID(c echo.Context, id int64) ([]*model.Comment, error) { 56 | var comments []*model.Comment 57 | db := utils.GetDBFromContext(c) 58 | if err := db.Model(&model.Comment{}).Where("reply_to_comment_id = ? AND deleted = ?", id, false).Find(&comments).Error; err != nil { 59 | return nil, fmt.Errorf("获取评论回复失败: %w", err) 60 | } 61 | return comments, nil 62 | } 63 | 64 | // GetCommentsByPostID 根据文章 ID 查询所有评论 65 | // 参数: 66 | // - c: Echo 上下文 67 | // - postID: 文章 ID 68 | // 69 | // 返回值: 70 | // - []*model.Comment: 评论列表 71 | // - error: 操作过程中的错误 72 | func GetCommentsByPostID(c echo.Context, postID int64) ([]*model.Comment, error) { 73 | var comments []*model.Comment 74 | db := utils.GetDBFromContext(c) 75 | if err := db.Model(&model.Comment{}).Where("post_id = ? AND deleted = ?", postID, false).Find(&comments).Error; err != nil { 76 | return nil, fmt.Errorf("获取文章评论失败: %w", err) 77 | } 78 | return comments, nil 79 | } 80 | 81 | // UpdateComment 更新评论 82 | // 参数: 83 | // - c: Echo 上下文 84 | // - comment: 评论信息 85 | // 86 | // 返回值: 87 | // - error: 操作过程中的错误 88 | func UpdateComment(c echo.Context, comment *model.Comment) error { 89 | db := utils.GetDBFromContext(c) 90 | if err := db.Model(&model.Comment{}).Save(comment).Error; err != nil { 91 | return fmt.Errorf("更新评论失败: %w", err) 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/serve/mapper/post.go: -------------------------------------------------------------------------------- 1 | // Package mapper 提供数据模型与数据库交互的映射层,处理文章相关数据操作 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package mapper 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | model "jank.com/jank_blog/internal/model/post" 12 | "jank.com/jank_blog/internal/utils" 13 | ) 14 | 15 | // CreatePost 将文章保存到数据库 16 | // 参数: 17 | // - c: Echo 上下文 18 | // - newPost: 文章信息 19 | // 20 | // 返回值: 21 | // - error: 操作过程中的错误 22 | func CreatePost(c echo.Context, newPost *model.Post) error { 23 | db := utils.GetDBFromContext(c) 24 | if err := db.Model(&model.Post{}).Create(newPost).Error; err != nil { 25 | return fmt.Errorf("创建文章失败: %w", err) 26 | } 27 | return nil 28 | } 29 | 30 | // GetPostByID 根据 ID 获取文章 31 | // 参数: 32 | // - c: Echo 上下文 33 | // - id: 文章 ID 34 | // 35 | // 返回值: 36 | // - *model.Post: 文章信息 37 | // - error: 操作过程中的错误 38 | func GetPostByID(c echo.Context, id int64) (*model.Post, error) { 39 | var pos model.Post 40 | db := utils.GetDBFromContext(c) 41 | if err := db.Model(&model.Post{}).Where("id = ? AND deleted = ?", id, false).First(&pos).Error; err != nil { 42 | return nil, fmt.Errorf("获取文章失败: %w", err) 43 | } 44 | return &pos, nil 45 | } 46 | 47 | // GetAllPostsWithPaging 获取分页后的文章列表和文章总数 48 | // 参数: 49 | // - c: Echo 上下文 50 | // - page: 页码 51 | // - pageSize: 每页大小 52 | // 53 | // 返回值: 54 | // - []*model.Post: 文章列表 55 | // - int64: 文章总数 56 | // - error: 操作过程中的错误 57 | func GetAllPostsWithPaging(c echo.Context, page, pageSize int) ([]*model.Post, int64, error) { 58 | var posts []*model.Post 59 | var total int64 60 | db := utils.GetDBFromContext(c) 61 | 62 | // 查询文章总数 63 | if err := db.Model(&model.Post{}).Where("deleted = ?", false).Count(&total).Error; err != nil { 64 | return nil, 0, fmt.Errorf("获取文章总数失败: %w", err) 65 | } 66 | 67 | // 使用雪花算法ID排序的分页查询 (雪花ID本身包含时间信息,降序排列即为最新内容) 68 | if err := db.Model(&model.Post{}).Where("deleted = ?", false). 69 | Order("id DESC"). 70 | Limit(pageSize).Offset((page - 1) * pageSize). 71 | Find(&posts).Error; err != nil { 72 | return nil, 0, fmt.Errorf("获取分页文章列表失败: %w", err) 73 | } 74 | return posts, total, nil 75 | } 76 | 77 | // UpdateOnePostByID 更新文章 78 | // 参数: 79 | // - c: Echo 上下文 80 | // - postID: 文章 ID 81 | // - newPost: 文章信息 82 | // 83 | // 返回值: 84 | // - error: 操作过程中的错误 85 | func UpdateOnePostByID(c echo.Context, postID int64, newPost *model.Post) error { 86 | db := utils.GetDBFromContext(c) 87 | result := db.Model(&model.Post{}).Where("id = ? AND deleted = ?", postID, false).Updates(newPost) 88 | 89 | if result.Error != nil { 90 | return fmt.Errorf("更新文章失败: %w", result.Error) 91 | } 92 | return nil 93 | } 94 | 95 | // DeleteOnePostByID 根据 ID 进行软删除操作 96 | // 参数: 97 | // - c: Echo 上下文 98 | // - postID: 文章 ID 99 | // 100 | // 返回值: 101 | // - error: 操作过程中的错误 102 | func DeleteOnePostByID(c echo.Context, postID int64) error { 103 | db := utils.GetDBFromContext(c) 104 | result := db.Model(&model.Post{}). 105 | Where("id = ? AND deleted = ?", postID, false). 106 | Update("deleted", true) 107 | 108 | if result.Error != nil { 109 | return fmt.Errorf("删除文章失败: %w", result.Error) 110 | } 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /pkg/serve/mapper/post_category.go: -------------------------------------------------------------------------------- 1 | // Package mapper 提供数据模型与数据库交互的映射层,处理文章与类目关联的数据操作 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package mapper 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | model "jank.com/jank_blog/internal/model/association" 12 | "jank.com/jank_blog/internal/utils" 13 | ) 14 | 15 | // CreatePostCategory 创建文章-类目关联 16 | // 参数: 17 | // - c: Echo 上下文 18 | // - postID: 文章 ID 19 | // - categoryID: 类目 ID 20 | // 21 | // 返回值: 22 | // - error: 操作过程中的错误 23 | func CreatePostCategory(c echo.Context, postID, categoryID int64) error { 24 | postCategory := &model.PostCategory{ 25 | PostID: postID, 26 | CategoryID: categoryID, 27 | } 28 | db := utils.GetDBFromContext(c) 29 | if err := db.Model(&model.PostCategory{}).Create(postCategory).Error; err != nil { 30 | return fmt.Errorf("创建文章-类目关联失败: %w", err) 31 | } 32 | return nil 33 | } 34 | 35 | // GetPostCategory 获取文章-类目关联 36 | // 参数: 37 | // - c: Echo 上下文 38 | // - postID: 文章 ID 39 | // 40 | // 返回值: 41 | // - *model.PostCategory: 文章-类目关联信息 42 | // - error: 操作过程中的错误 43 | func GetPostCategory(c echo.Context, postID int64) (*model.PostCategory, error) { 44 | var postCategory model.PostCategory 45 | db := utils.GetDBFromContext(c) 46 | err := db.Model(&model.PostCategory{}).Where("post_id = ? AND deleted = ?", postID, false).First(&postCategory).Error 47 | if err != nil { 48 | if err.Error() == "record not found" { 49 | return nil, fmt.Errorf("文章-类目关联不存在: %w", err) 50 | } 51 | return nil, fmt.Errorf("获取文章-类目关联失败: %w", err) 52 | } 53 | return &postCategory, nil 54 | } 55 | 56 | // UpdatePostCategory 更新文章-类目关联 57 | // 参数: 58 | // - c: Echo 上下文 59 | // - postID: 文章 ID 60 | // - categoryID: 类目 ID 61 | // 62 | // 返回值: 63 | // - error: 操作过程中的错误 64 | func UpdatePostCategory(c echo.Context, postID, categoryID int64) error { 65 | var exists int64 66 | db := utils.GetDBFromContext(c) 67 | if err := db.Model(&model.PostCategory{}). 68 | Where("post_id = ? AND deleted = ?", postID, false). 69 | Count(&exists).Error; err != nil { 70 | return fmt.Errorf("检查文章-类目关联失败: %w", err) 71 | } 72 | if exists > 0 { 73 | if err := db.Model(&model.PostCategory{}). 74 | Where("post_id = ? AND deleted = ?", postID, false). 75 | Update("category_id", categoryID).Error; err != nil { 76 | return fmt.Errorf("更新文章-类目关联失败: %w", err) 77 | } 78 | } else { 79 | return CreatePostCategory(c, postID, categoryID) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // DeletePostCategory 删除文章-类目关联 86 | // 参数: 87 | // - c: Echo 上下文 88 | // - postID: 文章 ID 89 | // 90 | // 返回值: 91 | // - error: 操作过程中的错误 92 | func DeletePostCategory(c echo.Context, postID int64) error { 93 | db := utils.GetDBFromContext(c) 94 | if err := db.Model(&model.PostCategory{}). 95 | Where("post_id = ? AND deleted = ?", postID, false). 96 | Update("deleted", true).Error; err != nil { 97 | return fmt.Errorf("删除文章-类目关联失败: %w", err) 98 | } 99 | return nil 100 | } 101 | 102 | // DeletePostCategoryByCategoryID 根据类目ID删除文章-类目关联 103 | // 参数: 104 | // - c: Echo 上下文 105 | // - categoryID: 类目 ID 106 | // 107 | // 返回值: 108 | // - error: 操作过程中的错误 109 | func DeletePostCategoryByCategoryID(c echo.Context, categoryID int64) error { 110 | db := utils.GetDBFromContext(c) 111 | if err := db.Model(&model.PostCategory{}). 112 | Where("category_id = ? AND deleted = ?", categoryID, false). 113 | Update("deleted", true).Error; err != nil { 114 | return fmt.Errorf("根据类目ID删除文章-类目关联失败: %w", err) 115 | } 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /pkg/serve/service/account/account.go: -------------------------------------------------------------------------------- 1 | // Package service 提供业务逻辑处理,处理账户相关业务 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package service 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | "github.com/labstack/echo/v4" 13 | "golang.org/x/crypto/bcrypt" 14 | 15 | "jank.com/jank_blog/internal/global" 16 | model "jank.com/jank_blog/internal/model/account" 17 | "jank.com/jank_blog/internal/utils" 18 | "jank.com/jank_blog/pkg/serve/controller/account/dto" 19 | "jank.com/jank_blog/pkg/serve/mapper" 20 | "jank.com/jank_blog/pkg/vo/account" 21 | ) 22 | 23 | var ( 24 | registerLock sync.Mutex // 用户注册锁,保护并发用户注册的操作 25 | passwordResetLock sync.Mutex // 修改密码锁,保护并发修改用户密码的操作 26 | logoutLock sync.Mutex // 用户登出锁,保护并发用户登出操作 27 | ) 28 | 29 | const ( 30 | USER_CACHE = "USER_CACHE" 31 | USER_CACHE_EXPIRE_TIME = time.Hour * 2 // Access Token 有效期 32 | ) 33 | 34 | // GetAccount 获取用户信息逻辑 35 | // 参数: 36 | // - c: Echo 上下文 37 | // - req: 获取账户请求 38 | // 39 | // 返回值: 40 | // - *account.GetAccountVO: 用户账户视图对象 41 | // - error: 操作过程中的错误 42 | func GetAccount(c echo.Context, req *dto.GetAccountRequest) (*account.GetAccountVO, error) { 43 | userInfo, err := mapper.GetAccountByEmail(c, req.Email) 44 | if err != nil { 45 | utils.BizLogger(c).Errorf("「%s」邮箱不存在", req.Email) 46 | return nil, fmt.Errorf("「%s」邮箱不存在", req.Email) 47 | } 48 | 49 | vo, err := utils.MapModelToVO(userInfo, &account.GetAccountVO{}) 50 | if err != nil { 51 | utils.BizLogger(c).Errorf("获取用户信息时映射 VO 失败: %v", err) 52 | return nil, fmt.Errorf("获取用户信息时映射 VO 失败: %w", err) 53 | } 54 | 55 | return vo.(*account.GetAccountVO), nil 56 | } 57 | 58 | // RegisterAcc 用户注册逻辑 59 | // 参数: 60 | // - c: Echo 上下文 61 | // - req: 注册账户请求 62 | // 63 | // 返回值: 64 | // - *account.RegisterAccountVO: 注册后的账户视图对象 65 | // - error: 操作过程中的错误 66 | func RegisterAcc(c echo.Context, req *dto.RegisterRequest) (*account.RegisterAccountVO, error) { 67 | registerLock.Lock() 68 | defer registerLock.Unlock() 69 | 70 | var registerVO *account.RegisterAccountVO 71 | 72 | err := utils.RunDBTransaction(c, func(tx error) error { 73 | totalAccounts, err := mapper.GetTotalAccounts(c) 74 | if err != nil { 75 | utils.BizLogger(c).Errorf("获取用户总数失败: %v", err) 76 | return fmt.Errorf("获取用户总数失败: %w", err) 77 | } 78 | 79 | if totalAccounts > 0 { 80 | utils.BizLogger(c).Error("系统限制: 当前为单用户独立部署版本,已达到账户数量上限 (1/1)") 81 | return fmt.Errorf("系统限制: 当前为单用户独立部署版本,已达到账户数量上限 (1/1)") 82 | } 83 | 84 | existingUser, _ := mapper.GetAccountByEmail(c, req.Email) 85 | if existingUser != nil { 86 | utils.BizLogger(c).Errorf("「%s」邮箱已被注册", req.Email) 87 | return fmt.Errorf("「%s」邮箱已被注册", req.Email) 88 | } 89 | 90 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) 91 | if err != nil { 92 | utils.BizLogger(c).Errorf("哈希加密失败: %v", err) 93 | return fmt.Errorf("哈希加密失败: %w", err) 94 | } 95 | 96 | acc := &model.Account{ 97 | Email: req.Email, 98 | Password: string(hashedPassword), 99 | Nickname: req.Nickname, 100 | Phone: req.Phone, 101 | } 102 | 103 | if err := mapper.CreateAccount(c, acc); err != nil { 104 | utils.BizLogger(c).Errorf("「%s」用户注册失败: %v", req.Email, err) 105 | return fmt.Errorf("「%s」用户注册失败: %w", req.Email, err) 106 | } 107 | 108 | vo, err := utils.MapModelToVO(acc, &account.RegisterAccountVO{}) 109 | if err != nil { 110 | utils.BizLogger(c).Errorf("用户注册时映射 VO 失败: %v", err) 111 | return fmt.Errorf("用户注册时映射 VO 失败: %w", err) 112 | } 113 | 114 | registerVO = vo.(*account.RegisterAccountVO) 115 | return nil 116 | }) 117 | 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return registerVO, nil 123 | } 124 | 125 | // LoginAcc 登录用户逻辑 126 | // 参数: 127 | // - c: Echo 上下文 128 | // - req: 登录请求 129 | // 130 | // 返回值: 131 | // - *account.LoginVO: 登录成功后的令牌视图对象 132 | // - error: 操作过程中的错误 133 | func LoginAcc(c echo.Context, req *dto.LoginRequest) (*account.LoginVO, error) { 134 | acc, err := mapper.GetAccountByEmail(c, req.Email) 135 | if err != nil { 136 | utils.BizLogger(c).Errorf("「%s」用户不存在: %v", req.Email, err) 137 | return nil, fmt.Errorf("「%s」用户不存在: %w", req.Email, err) 138 | } 139 | 140 | err = bcrypt.CompareHashAndPassword([]byte(acc.Password), []byte(req.Password)) 141 | if err != nil { 142 | utils.BizLogger(c).Errorf("密码输入错误: %v", err) 143 | return nil, fmt.Errorf("密码输入错误: %w", err) 144 | } 145 | 146 | accessTokenString, refreshTokenString, err := utils.GenerateJWT(acc.ID) 147 | if err != nil { 148 | utils.BizLogger(c).Errorf("token 生成失败: %v", err) 149 | return nil, fmt.Errorf("token 生成失败: %w", err) 150 | } 151 | 152 | cacheKey := fmt.Sprintf("%s:%d", USER_CACHE, acc.ID) 153 | 154 | err = global.RedisClient.Set(context.Background(), cacheKey, accessTokenString, USER_CACHE_EXPIRE_TIME).Err() 155 | if err != nil { 156 | utils.BizLogger(c).Errorf("登录时设置缓存失败: %v", err) 157 | return nil, fmt.Errorf("登录时设置缓存失败: %w", err) 158 | } 159 | 160 | token := &account.LoginVO{ 161 | AccessToken: accessTokenString, 162 | RefreshToken: refreshTokenString, 163 | } 164 | 165 | vo, err := utils.MapModelToVO(token, &account.LoginVO{}) 166 | if err != nil { 167 | utils.BizLogger(c).Errorf("用户登录时映射 VO 失败: %v", err) 168 | return nil, fmt.Errorf("用户登陆时映射 VO 失败: %v", err) 169 | } 170 | 171 | return vo.(*account.LoginVO), nil 172 | } 173 | 174 | // LogoutAcc 处理用户登出逻辑 175 | // 参数: 176 | // - c: Echo 上下文 177 | // 178 | // 返回值: 179 | // - error: 操作过程中的错误 180 | func LogoutAcc(c echo.Context) error { 181 | logoutLock.Lock() 182 | defer logoutLock.Unlock() 183 | 184 | accountID, err := utils.ParseAccountAndRoleIDFromJWT(c.Request().Header.Get("Authorization")) 185 | if err != nil { 186 | utils.BizLogger(c).Errorf("解析 access token 失败: %v", err) 187 | return fmt.Errorf("解析 access token 失败: %w", err) 188 | } 189 | 190 | cacheKey := fmt.Sprintf("%s:%d", USER_CACHE, accountID) 191 | err = global.RedisClient.Del(c.Request().Context(), cacheKey).Err() 192 | if err != nil { 193 | utils.BizLogger(c).Errorf("删除 Redis 缓存失败: %v", err) 194 | return fmt.Errorf("删除 Redis 缓存失败: %w", err) 195 | } 196 | 197 | return nil 198 | } 199 | 200 | // ResetPassword 重置密码逻辑 201 | // 参数: 202 | // - c: Echo 上下文 203 | // - req: 重置密码请求 204 | // 205 | // 返回值: 206 | // - error: 操作过程中的错误 207 | func ResetPassword(c echo.Context, req *dto.ResetPwdRequest) error { 208 | passwordResetLock.Lock() 209 | defer passwordResetLock.Unlock() 210 | 211 | return utils.RunDBTransaction(c, func(tx error) error { 212 | if req.NewPassword != req.AgainNewPassword { 213 | utils.BizLogger(c).Errorf("两次密码输入不一致") 214 | return fmt.Errorf("两次密码输入不一致") 215 | } 216 | 217 | accountID, err := utils.ParseAccountAndRoleIDFromJWT(c.Request().Header.Get("Authorization")) 218 | if err != nil { 219 | utils.BizLogger(c).Errorf("解析 token 失败: %v", err) 220 | return fmt.Errorf("解析 token 失败: %w", err) 221 | } 222 | 223 | acc, err := mapper.GetAccountByAccountID(c, accountID) 224 | if err != nil { 225 | utils.BizLogger(c).Errorf("「%s」用户不存在: %v", req.Email, err) 226 | return fmt.Errorf("「%s」用户不存在: %w", req.Email, err) 227 | } 228 | 229 | newPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) 230 | if err != nil { 231 | utils.BizLogger(c).Errorf("密码加密失败: %v", err) 232 | return fmt.Errorf("密码加密失败: %w", err) 233 | } 234 | acc.Password = string(newPassword) 235 | 236 | if err := mapper.UpdateAccount(c, acc); err != nil { 237 | utils.BizLogger(c).Errorf("密码修改失败: %v", err) 238 | return fmt.Errorf("密码修改失败: %w", err) 239 | } 240 | 241 | return nil 242 | }) 243 | } 244 | -------------------------------------------------------------------------------- /pkg/serve/service/comment/comment.go: -------------------------------------------------------------------------------- 1 | // Package service 提供业务逻辑处理,处理评论相关业务 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package service 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/labstack/echo/v4" 10 | 11 | model "jank.com/jank_blog/internal/model/comment" 12 | "jank.com/jank_blog/internal/utils" 13 | "jank.com/jank_blog/pkg/serve/controller/comment/dto" 14 | "jank.com/jank_blog/pkg/serve/mapper" 15 | "jank.com/jank_blog/pkg/vo/comment" 16 | ) 17 | 18 | // CreateComment 创建评论 19 | // 参数: 20 | // - c: Echo 上下文 21 | // - req: 创建评论请求 22 | // 23 | // 返回值: 24 | // - *comment.CommentsVO: 创建后的评论视图对象 25 | // - error: 操作过程中的错误 26 | func CreateComment(c echo.Context, req *dto.CreateCommentRequest) (*comment.CommentsVO, error) { 27 | var commentVO *comment.CommentsVO 28 | 29 | err := utils.RunDBTransaction(c, func(tx error) error { 30 | com := &model.Comment{ 31 | Content: req.Content, 32 | UserId: req.UserId, 33 | PostId: req.PostId, 34 | ReplyToCommentId: req.ReplyToCommentId, 35 | } 36 | 37 | if err := mapper.CreateComment(c, com); err != nil { 38 | utils.BizLogger(c).Errorf("创建评论失败:%v", err) 39 | return fmt.Errorf("创建评论失败:%w", err) 40 | } 41 | 42 | vo, err := utils.MapModelToVO(com, &comment.CommentsVO{}) 43 | if err != nil { 44 | utils.BizLogger(c).Errorf("创建评论时映射 VO 失败:%v", err) 45 | return fmt.Errorf("创建评论时映射 VO 失败:%w", err) 46 | } 47 | 48 | commentVO = vo.(*comment.CommentsVO) 49 | return nil 50 | }) 51 | 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return commentVO, nil 57 | } 58 | 59 | // GetCommentWithReplies 根据 ID 获取评论及其所有回复 60 | // 参数: 61 | // - c: Echo 上下文 62 | // - req: 获取评论请求 63 | // 64 | // 返回值: 65 | // - *comment.CommentsVO: 评论及其回复的视图对象 66 | // - error: 操作过程中的错误 67 | func GetCommentWithReplies(c echo.Context, req *dto.GetOneCommentRequest) (*comment.CommentsVO, error) { 68 | com, err := mapper.GetCommentByID(c, req.CommentID) 69 | if err != nil { 70 | utils.BizLogger(c).Errorf("获取评论失败:%v", err) 71 | return nil, fmt.Errorf("获取评论失败:%w", err) 72 | } 73 | 74 | replies, err := mapper.GetReplyByCommentID(c, req.CommentID) 75 | if err != nil { 76 | utils.BizLogger(c).Errorf("获取子评论失败:%v", err) 77 | return nil, fmt.Errorf("获取子评论失败:%w", err) 78 | } 79 | 80 | com.Replies = replies 81 | 82 | commentVO, err := utils.MapModelToVO(com, &comment.CommentsVO{}) 83 | if err != nil { 84 | utils.BizLogger(c).Errorf("获取评论时映射 VO 失败:%v", err) 85 | return nil, fmt.Errorf("获取评论时映射 VO 失败:%w", err) 86 | } 87 | 88 | return commentVO.(*comment.CommentsVO), nil 89 | } 90 | 91 | // GetCommentGraphByPostID 根据文章 ID 获取评论图结构 92 | // 参数: 93 | // - c: Echo 上下文 94 | // - req: 获取评论图请求 95 | // 96 | // 返回值: 97 | // - []*comment.CommentsVO: 评论图结构列表 98 | // - error: 操作过程中的错误 99 | func GetCommentGraphByPostID(c echo.Context, req *dto.GetCommentGraphRequest) ([]*comment.CommentsVO, error) { 100 | comments, err := mapper.GetCommentsByPostID(c, req.PostID) 101 | if err != nil { 102 | utils.BizLogger(c).Errorf("获取评论图失败:%v", err) 103 | return nil, fmt.Errorf("获取评论图失败:%w", err) 104 | } 105 | 106 | commentMap := make(map[int64]*comment.CommentsVO) 107 | var rootCommentsVO []*comment.CommentsVO 108 | 109 | for _, com := range comments { 110 | commentVO, err := utils.MapModelToVO(com, &comment.CommentsVO{}) 111 | if err != nil { 112 | utils.BizLogger(c).Errorf("获取评论图时映射 VO 失败:%v", err) 113 | return nil, fmt.Errorf("获取评论图时映射 VO 失败:%w", err) 114 | } 115 | vo := commentVO.(*comment.CommentsVO) 116 | vo.Replies = make([]*comment.CommentsVO, 0) 117 | commentMap[com.ID] = vo 118 | 119 | if com.ReplyToCommentId == 0 { 120 | rootCommentsVO = append(rootCommentsVO, vo) 121 | } 122 | } 123 | 124 | for _, com := range comments { 125 | if com.ReplyToCommentId != 0 { 126 | if parentVO, exists := commentMap[com.ReplyToCommentId]; exists { 127 | parentVO.Replies = append(parentVO.Replies, commentMap[com.ID]) 128 | } 129 | } 130 | } 131 | 132 | processed := make(map[int64]bool) 133 | var processComment func(*comment.CommentsVO) *comment.CommentsVO 134 | processComment = func(vo *comment.CommentsVO) *comment.CommentsVO { 135 | if processed[vo.ID] { 136 | newVO := *vo 137 | newVO.Replies = make([]*comment.CommentsVO, 0) 138 | return &newVO 139 | } 140 | processed[vo.ID] = true 141 | 142 | for i, reply := range vo.Replies { 143 | vo.Replies[i] = processComment(reply) 144 | } 145 | return vo 146 | } 147 | 148 | for i, rootVO := range rootCommentsVO { 149 | rootCommentsVO[i] = processComment(rootVO) 150 | } 151 | 152 | return rootCommentsVO, nil 153 | } 154 | 155 | // DeleteComment 软删除评论 156 | // 参数: 157 | // - c: Echo 上下文 158 | // - req: 删除评论请求 159 | // 160 | // 返回值: 161 | // - *comment.CommentsVO: 被删除的评论视图对象 162 | // - error: 操作过程中的错误 163 | func DeleteComment(c echo.Context, req *dto.DeleteCommentRequest) (*comment.CommentsVO, error) { 164 | var commentVO *comment.CommentsVO 165 | 166 | err := utils.RunDBTransaction(c, func(tx error) error { 167 | com, err := mapper.GetCommentByID(c, req.ID) 168 | if err != nil { 169 | utils.BizLogger(c).Errorf("获取评论失败:%v", err) 170 | return fmt.Errorf("评论不存在:%w", err) 171 | } 172 | 173 | com.Deleted = true 174 | if err := mapper.UpdateComment(c, com); err != nil { 175 | utils.BizLogger(c).Errorf("软删除评论失败:%v", err) 176 | return fmt.Errorf("软删除评论失败:%w", err) 177 | } 178 | 179 | vo, err := utils.MapModelToVO(com, &comment.CommentsVO{}) 180 | if err != nil { 181 | utils.BizLogger(c).Errorf("软删除评论时映射 VO 失败:%v", err) 182 | return fmt.Errorf("软删除评论时映射 VO 失败:%w", err) 183 | } 184 | 185 | commentVO = vo.(*comment.CommentsVO) 186 | return nil 187 | }) 188 | 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | return commentVO, nil 194 | } 195 | -------------------------------------------------------------------------------- /pkg/serve/service/post/post.go: -------------------------------------------------------------------------------- 1 | // Package service 提供业务逻辑处理,处理文章相关业务 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package service 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "math" 10 | "mime/multipart" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/labstack/echo/v4" 15 | 16 | model "jank.com/jank_blog/internal/model/post" 17 | "jank.com/jank_blog/internal/utils" 18 | "jank.com/jank_blog/pkg/serve/controller/post/dto" 19 | "jank.com/jank_blog/pkg/serve/mapper" 20 | "jank.com/jank_blog/pkg/vo/post" 21 | ) 22 | 23 | // CreateOnePost 创建文章 24 | // 参数: 25 | // - c: Echo 上下文 26 | // - req: 创建文章请求 27 | // 28 | // 返回值: 29 | // - *post.PostsVO: 创建后的文章视图对象 30 | // - error: 操作过程中的错误 31 | func CreateOnePost(c echo.Context, req *dto.CreateOnePostRequest) (*post.PostsVO, error) { 32 | var contentMarkdown string 33 | var categoryID int64 34 | 35 | contentType := c.Request().Header.Get("Content-Type") 36 | switch { 37 | case contentType == "application/json": 38 | contentMarkdown = req.ContentMarkdown 39 | categoryID = req.CategoryID 40 | case strings.HasPrefix(contentType, "multipart/form-data"): 41 | file, err := c.FormFile("content_markdown") 42 | if err != nil { 43 | return nil, fmt.Errorf("获取上传文件失败: %v", err) 44 | } 45 | src, err := file.Open() 46 | if err != nil { 47 | return nil, fmt.Errorf("打开上传文件失败: %v", err) 48 | } 49 | defer func(src multipart.File) { 50 | err := src.Close() 51 | if err != nil { 52 | utils.BizLogger(c).Errorf("关闭上传文件失败: %v", err) 53 | } 54 | }(src) 55 | content, err := io.ReadAll(src) 56 | if err != nil { 57 | return nil, fmt.Errorf("读取上传文件内容失败: %v", err) 58 | } 59 | contentMarkdown = string(content) 60 | categoryIDStr := c.FormValue("category_id") 61 | if categoryIDStr != "" { 62 | id, err := strconv.ParseInt(categoryIDStr, 10, 64) 63 | if err != nil { 64 | return nil, fmt.Errorf("类目ID格式错误: %v", err) 65 | } 66 | categoryID = id 67 | } 68 | default: 69 | return nil, fmt.Errorf("不支持的 Content-Type: %v", contentType) 70 | } 71 | 72 | if categoryID > 0 { 73 | _, err := mapper.GetCategoryByID(c, categoryID) 74 | if err != nil { 75 | utils.BizLogger(c).Errorf("类目ID「%d」不存在: %v", categoryID, err) 76 | return nil, fmt.Errorf("类目ID「%d」不存在: %w", categoryID, err) 77 | } 78 | } 79 | 80 | contentHTML, err := utils.RenderMarkdown([]byte(contentMarkdown)) 81 | if err != nil { 82 | utils.BizLogger(c).Errorf("渲染 Markdown 失败: %v", err) 83 | return nil, fmt.Errorf("渲染 Markdown 失败: %w", err) 84 | } 85 | 86 | var postsVO *post.PostsVO 87 | 88 | err = utils.RunDBTransaction(c, func(tx error) error { 89 | newPost := &model.Post{ 90 | Title: req.Title, 91 | Image: req.Image, 92 | Visibility: req.Visibility, 93 | ContentMarkdown: contentMarkdown, 94 | ContentHTML: contentHTML, 95 | } 96 | 97 | if err := mapper.CreatePost(c, newPost); err != nil { 98 | utils.BizLogger(c).Errorf("创建文章失败: %v", err) 99 | return fmt.Errorf("创建文章失败: %w", err) 100 | } 101 | 102 | if err := mapper.CreatePostCategory(c, newPost.ID, categoryID); err != nil { 103 | utils.BizLogger(c).Errorf("创建文章-类目关联失败: %v", err) 104 | return fmt.Errorf("创建文章-类目关联失败: %w", err) 105 | } 106 | 107 | vo, err := utils.MapModelToVO(newPost, &post.PostsVO{}) 108 | if err != nil { 109 | utils.BizLogger(c).Errorf("创建文章时映射 VO 失败: %v", err) 110 | return fmt.Errorf("创建文章时映射 VO 失败: %w", err) 111 | } 112 | 113 | postsVO = vo.(*post.PostsVO) 114 | postsVO.CategoryID = categoryID 115 | 116 | return nil 117 | }) 118 | 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | return postsVO, nil 124 | } 125 | 126 | // GetOnePostByID 根据 ID 获取文章 127 | // 参数: 128 | // - c: Echo 上下文 129 | // - req: 获取文章请求 130 | // 131 | // 返回值: 132 | // - interface{}: 获取到的文章视图对象 133 | // - error: 操作过程中的错误 134 | func GetOnePostByID(c echo.Context, req *dto.GetOnePostRequest) (*post.PostsVO, error) { 135 | pos, err := mapper.GetPostByID(c, req.ID) 136 | if err != nil { 137 | utils.BizLogger(c).Errorf("根据 ID 获取文章失败: %v", err) 138 | return nil, fmt.Errorf("根据 ID 获取文章失败: %w", err) 139 | } 140 | if pos == nil { 141 | utils.BizLogger(c).Errorf("文章不存在: %v", err) 142 | return nil, fmt.Errorf("文章不存在: %w", err) 143 | } 144 | 145 | vo, err := utils.MapModelToVO(pos, &post.PostsVO{}) 146 | if err != nil { 147 | utils.BizLogger(c).Errorf("获取文章时映射 VO 失败: %v", err) 148 | return nil, fmt.Errorf("获取文章时映射 VO 失败: %w", err) 149 | } 150 | 151 | postsVO := vo.(*post.PostsVO) 152 | 153 | postCategory, err := mapper.GetPostCategory(c, pos.ID) 154 | if err != nil { 155 | utils.BizLogger(c).Errorf("获取文章类目关联失败: %v", err) 156 | } 157 | 158 | if postCategory != nil { 159 | postsVO.CategoryID = postCategory.CategoryID 160 | } 161 | 162 | return postsVO, nil 163 | } 164 | 165 | // GetAllPostsWithPagingAndFormat 获取格式化后的分页文章列表、总页数和当前页数 166 | // 参数: 167 | // - c: Echo 上下文 168 | // - page: 页码 169 | // - pageSize: 每页文章数量 170 | // 171 | // 返回值: 172 | // - map[string]interface{}: 包含文章列表、总页数和当前页数的映射 173 | // - error: 操作过程中的错误 174 | func GetAllPostsWithPagingAndFormat(c echo.Context, page, pageSize int) (map[string]interface{}, error) { 175 | posts, total, err := mapper.GetAllPostsWithPaging(c, page, pageSize) 176 | if err != nil { 177 | utils.BizLogger(c).Errorf("获取文章列表失败: %v", err) 178 | return nil, fmt.Errorf("获取文章列表失败: %w", err) 179 | } 180 | 181 | postResponse := make([]*post.PostsVO, len(posts)) 182 | for i, pos := range posts { 183 | vo, err := utils.MapModelToVO(pos, &post.PostsVO{}) 184 | if err != nil { 185 | utils.BizLogger(c).Errorf("获取文章列表时映射 VO 失败: %v", err) 186 | return nil, fmt.Errorf("获取文章列表时映射 VO 失败: %w", err) 187 | } 188 | 189 | postVO := vo.(*post.PostsVO) 190 | 191 | postCategory, err := mapper.GetPostCategory(c, pos.ID) 192 | if err != nil { 193 | utils.BizLogger(c).Errorf("获取文章ID「%d」的类目关联失败: %v", pos.ID, err) 194 | } 195 | 196 | if postCategory != nil { 197 | postVO.CategoryID = postCategory.CategoryID 198 | } 199 | 200 | // 只保留 ContentHTML 的前 200 个字符 201 | if len(postVO.ContentHTML) > 200 { 202 | postVO.ContentHTML = postVO.ContentHTML[:200] 203 | } 204 | 205 | postResponse[i] = postVO 206 | } 207 | 208 | return map[string]interface{}{ 209 | "posts": &postResponse, 210 | "totalPages": int(math.Ceil(float64(total) / float64(pageSize))), 211 | "currentPage": page, 212 | }, nil 213 | } 214 | 215 | // UpdateOnePost 更新文章 216 | // 参数: 217 | // - c: Echo 上下文 218 | // - req: 更新文章请求 219 | // 220 | // 返回值: 221 | // - *post.PostsVO: 更新后的文章视图对象 222 | // - error: 操作过程中的错误 223 | func UpdateOnePost(c echo.Context, req *dto.UpdateOnePostRequest) (*post.PostsVO, error) { 224 | var contentMarkdown string 225 | var categoryID int64 226 | 227 | pos, err := mapper.GetPostByID(c, req.ID) 228 | if err != nil || pos == nil { 229 | utils.BizLogger(c).Errorf("获取文章失败: %v", err) 230 | return nil, fmt.Errorf("获取文章失败: %w", err) 231 | } 232 | 233 | contentType := c.Request().Header.Get("Content-Type") 234 | switch { 235 | case contentType == "application/json": 236 | if req.Title != "" { 237 | pos.Title = req.Title 238 | } 239 | if req.Image != "" { 240 | pos.Image = req.Image 241 | } 242 | pos.Visibility = req.Visibility 243 | if req.ContentMarkdown != "" { 244 | contentMarkdown = req.ContentMarkdown 245 | pos.ContentMarkdown = contentMarkdown 246 | pos.ContentHTML, err = utils.RenderMarkdown([]byte(contentMarkdown)) 247 | if err != nil { 248 | utils.BizLogger(c).Errorf("渲染 Markdown 失败: %v", err) 249 | return nil, fmt.Errorf("渲染 Markdown 失败: %w", err) 250 | } 251 | } 252 | categoryID = req.CategoryID 253 | 254 | case strings.HasPrefix(contentType, "multipart/form-data"): 255 | if file, err := c.FormFile("content_markdown"); err == nil { 256 | src, err := file.Open() 257 | if err != nil { 258 | return nil, fmt.Errorf("打开上传文件失败: %v", err) 259 | } 260 | defer func(src multipart.File) { 261 | err := src.Close() 262 | if err != nil { 263 | utils.BizLogger(c).Errorf("关闭上传文件失败: %v", err) 264 | } 265 | }(src) 266 | content, err := io.ReadAll(src) 267 | if err != nil { 268 | return nil, fmt.Errorf("读取上传文件内容失败: %v", err) 269 | } 270 | contentMarkdown = string(content) 271 | pos.ContentMarkdown = contentMarkdown 272 | pos.ContentHTML, err = utils.RenderMarkdown([]byte(contentMarkdown)) 273 | if err != nil { 274 | utils.BizLogger(c).Errorf("渲染 Markdown 失败: %v", err) 275 | return nil, fmt.Errorf("渲染 Markdown 失败: %w", err) 276 | } 277 | } 278 | 279 | if title := c.FormValue("title"); title != "" { 280 | pos.Title = title 281 | } 282 | if image := c.FormValue("image"); image != "" { 283 | pos.Image = image 284 | } 285 | if visibility := c.FormValue("visibility"); visibility != "" { 286 | pos.Visibility = visibility == "true" 287 | } 288 | if categoryIDStr := c.FormValue("category_id"); categoryIDStr != "" { 289 | id, err := strconv.ParseInt(categoryIDStr, 10, 64) 290 | if err != nil { 291 | return nil, fmt.Errorf("category_id 格式错误: %w", err) 292 | } 293 | categoryID = id 294 | } 295 | default: 296 | return nil, fmt.Errorf("不支持的 Content-Type: %v", contentType) 297 | } 298 | 299 | if categoryID > 0 { 300 | _, err := mapper.GetCategoryByID(c, categoryID) 301 | if err != nil { 302 | utils.BizLogger(c).Errorf("类目ID「%d」不存在: %v", categoryID, err) 303 | return nil, fmt.Errorf("类目ID「%d」不存在: %w", categoryID, err) 304 | } 305 | } 306 | 307 | var postsVO *post.PostsVO 308 | 309 | err = utils.RunDBTransaction(c, func(tx error) error { 310 | if err := mapper.UpdateOnePostByID(c, req.ID, pos); err != nil { 311 | utils.BizLogger(c).Errorf("更新文章失败: %v", err) 312 | return fmt.Errorf("更新文章失败: %w", err) 313 | } 314 | 315 | if err := mapper.UpdatePostCategory(c, req.ID, categoryID); err != nil { 316 | utils.BizLogger(c).Errorf("更新文章-类目关联失败: %v", err) 317 | return fmt.Errorf("更新文章-类目关联失败: %w", err) 318 | } 319 | 320 | vo, err := utils.MapModelToVO(pos, &post.PostsVO{}) 321 | if err != nil { 322 | utils.BizLogger(c).Errorf("更新文章时映射 VO 失败: %v", err) 323 | return fmt.Errorf("更新文章时映射 VO 失败: %w", err) 324 | } 325 | 326 | postsVO = vo.(*post.PostsVO) 327 | postsVO.CategoryID = categoryID 328 | 329 | return nil 330 | }) 331 | 332 | if err != nil { 333 | return nil, err 334 | } 335 | 336 | return postsVO, nil 337 | } 338 | 339 | // DeleteOnePost 删除文章 340 | // 参数: 341 | // - c: Echo 上下文 342 | // - req: 删除文章请求 343 | // 344 | // 返回值: 345 | // - error: 操作过程中的错误 346 | func DeleteOnePost(c echo.Context, req *dto.DeleteOnePostRequest) error { 347 | return utils.RunDBTransaction(c, func(tx error) error { 348 | if err := mapper.DeleteOnePostByID(c, req.ID); err != nil { 349 | utils.BizLogger(c).Errorf("删除文章失败: %v", err) 350 | return fmt.Errorf("删除文章失败: %w", err) 351 | } 352 | 353 | if err := mapper.DeletePostCategory(c, req.ID); err != nil { 354 | utils.BizLogger(c).Errorf("删除文章-类目关联失败: %v", err) 355 | return fmt.Errorf("删除文章-类目关联失败: %w", err) 356 | } 357 | 358 | return nil 359 | }) 360 | } 361 | -------------------------------------------------------------------------------- /pkg/vo/README.md: -------------------------------------------------------------------------------- 1 | VO 规范 2 | -------------------------------------------------------------------------------- /pkg/vo/account/get_acc_vo.go: -------------------------------------------------------------------------------- 1 | // Package account 提供账户相关的视图对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package account 5 | 6 | // GetAccountVO 获取账户信息请求体 7 | // @Description 请求获取账户信息时所需参数 8 | // @Property email body string true "用户邮箱" 9 | // @Property nickname body string true "用户昵称" 10 | // @Property phone body string true "用户手机号" 11 | type GetAccountVO struct { 12 | Nickname string `json:"nickname"` 13 | Email string `json:"email"` 14 | Phone string `json:"phone"` 15 | } 16 | -------------------------------------------------------------------------------- /pkg/vo/account/login_vo.go: -------------------------------------------------------------------------------- 1 | // Package account 提供账户相关的视图对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package account 5 | 6 | // LoginVO 返回给前端的登录信息 7 | // @Description 登录成功后返回的访问令牌和刷新令牌 8 | // @Property access_token body string true "访问令牌" 9 | // @Property refresh_token body string true "刷新令牌" 10 | type LoginVO struct { 11 | AccessToken string `json:"access_token"` 12 | RefreshToken string `json:"refresh_token"` 13 | } 14 | -------------------------------------------------------------------------------- /pkg/vo/account/register_acc_vo.go: -------------------------------------------------------------------------------- 1 | // Package account 提供账户相关的视图对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package account 5 | 6 | // RegisterAccountVO 获取账户信息请求体 7 | // @Description 请求获取账户信息时所需参数 8 | // @Property email body string true "用户邮箱" 9 | // @Property nickname body string true "用户昵称" 10 | // @Property role_code body string true "用户角色编码" 11 | type RegisterAccountVO struct { 12 | Nickname string `json:"nickname"` 13 | Email string `json:"email"` 14 | } 15 | -------------------------------------------------------------------------------- /pkg/vo/category/categories_vo.go: -------------------------------------------------------------------------------- 1 | // Package category 提供类目相关的视图对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package category 5 | 6 | // CategoriesVO 获取类目响应 7 | // @Description 获取类目响应 8 | // @Property id body int64 true "类目唯一标识" 9 | // @Property name body string true "类目名称" 10 | // @Property description body string true "类目描述" 11 | // @Property parent_id body int64 true "父类目ID" 12 | // @Property path body string true "类目路径" 13 | // @Property children body []*CategoriesVO true "子类目列表" 14 | type CategoriesVO struct { 15 | ID int64 `json:"id"` 16 | Name string `json:"name"` 17 | Description string `json:"description"` 18 | ParentID int64 `json:"parent_id"` 19 | Path string `json:"path"` 20 | Children []*CategoriesVO `json:"children"` 21 | } 22 | -------------------------------------------------------------------------------- /pkg/vo/comment/comments_vo.go: -------------------------------------------------------------------------------- 1 | // Package comment 提供评论相关的视图对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package comment 5 | 6 | // CommentsVO 获取评论响应 7 | // @Description 获取单个评论的响应 8 | // @Property id body int64 true "评论唯一标识" 9 | // @Property content body string true "评论内容" 10 | // @Property user_id body int64 true "评论所属用户ID" 11 | // @Property post_id body int64 true "评论所属文章ID" 12 | // @Property reply_to_comment_id body int64 false "回复的目标评论ID" 13 | // @Property replies body []*CommentsVO true "子评论列表" 14 | type CommentsVO struct { 15 | ID int64 `json:"id"` 16 | Content string `json:"content"` 17 | UserId int64 `json:"user_id"` 18 | PostId int64 `json:"post_id"` 19 | ReplyToCommentId int64 `json:"reply_to_comment_id"` 20 | Replies []*CommentsVO `json:"replies"` 21 | } 22 | -------------------------------------------------------------------------------- /pkg/vo/post/posts_vo.go: -------------------------------------------------------------------------------- 1 | // Package post 提供文章相关的视图对象定义 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package post 5 | 6 | // PostsVO 获取帖子的响应结构 7 | // @Description 获取帖子时返回的响应数据 8 | // @Property id body int64 true "帖子唯一标识" 9 | // @Property title body string true "帖子标题" 10 | // @Property image body string true "帖子封面图片 URL" 11 | // @Property visibility body bool true "帖子可见性状态" 12 | // @Property content_html body string true "帖子 HTML 格式内容" 13 | // @Property category_id body int64 true "帖子所属分类 ID" 14 | type PostsVO struct { 15 | ID int64 `json:"id"` 16 | Title string `json:"title"` 17 | Image string `json:"image"` 18 | Visibility bool `json:"visibility"` 19 | // ContentMarkdown string `json:"content_markdown"` 20 | ContentHTML string `json:"content_html"` 21 | CategoryID int64 `json:"category_id"` 22 | } 23 | -------------------------------------------------------------------------------- /pkg/vo/result.go: -------------------------------------------------------------------------------- 1 | // Package vo 提供视图对象定义和响应结果包装 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package vo 5 | 6 | import ( 7 | "errors" 8 | "time" 9 | 10 | "github.com/labstack/echo/v4" 11 | 12 | bizErr "jank.com/jank_blog/internal/error" 13 | ) 14 | 15 | // Result 通用 API 响应结果结构体 16 | type Result struct { 17 | *bizErr.Err // 错误信息 18 | Data interface{} `json:"data"` // 响应数据 19 | RequestId interface{} `json:"requestId"` // 请求ID 20 | TimeStamp interface{} `json:"timeStamp"` // 响应时间戳 21 | } 22 | 23 | // Success 成功返回 24 | // 参数: 25 | // - c: Echo 上下文 26 | // - data: 响应数据 27 | // 28 | // 返回值: 29 | // - Result: 成功响应结果 30 | func Success(c echo.Context, data interface{}) Result { 31 | return Result{ 32 | Err: nil, 33 | Data: data, 34 | RequestId: c.Response().Header().Get(echo.HeaderXRequestID), 35 | TimeStamp: time.Now().Unix(), 36 | } 37 | } 38 | 39 | // Fail 失败返回 40 | // 参数: 41 | // - c: Echo 上下文 42 | // - data: 错误相关数据 43 | // - err: 错误对象 44 | // 45 | // 返回值: 46 | // - Result: 失败响应结果 47 | func Fail(c echo.Context, data interface{}, err error) Result { 48 | var newBizErr *bizErr.Err 49 | if ok := errors.As(err, &newBizErr); ok { 50 | return Result{ 51 | Err: newBizErr, 52 | Data: data, 53 | RequestId: c.Response().Header().Get(echo.HeaderXRequestID), 54 | TimeStamp: time.Now().Unix(), 55 | } 56 | } 57 | 58 | return Result{ 59 | Err: bizErr.New(bizErr.SERVER_ERR), 60 | Data: data, 61 | RequestId: c.Response().Header().Get(echo.HeaderXRequestID), 62 | TimeStamp: time.Now().Unix(), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/vo/verification/img_verification_vo.go: -------------------------------------------------------------------------------- 1 | // Package verification 提供验证码相关的数据传输对象 2 | // 创建者:Done-0 3 | // 创建时间:2025-05-10 4 | package verification 5 | 6 | // ImgVerificationVO 图片验证码 7 | // @Description 图片验证码 8 | // @Property img body string true "图片的base64编码" 9 | type ImgVerificationVO struct { 10 | ImgBase64 string `json:"imgBase64"` 11 | } 12 | --------------------------------------------------------------------------------