├── .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 |
3 |
4 |
5 |
6 | Jank,一个轻量级的博客系统,基于 Go 语言和 Echo 框架开发,强调极简、低耦合和高扩展
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
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 | 
41 | 
42 | 
43 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------