user viper

This commit is contained in:
张超
2025-04-15 16:47:00 +08:00
parent 33cb413a12
commit f52d5a698b
12 changed files with 329 additions and 214 deletions

View File

@@ -1,23 +0,0 @@
[mysql]
Type = mysql
Host = 47.93.160.42
Port = 3630
User = blog
Password = qI7=bL4@iJ
DBName = blog
Charset = utf8mb4
Prefix = gin_
;Prefix = gin_
[jwt]
SecretKey = \x13\xbf\xd2 1\xce\x8b\xc1\t\xc1=\xec\x07\x93\xd4\x9e\xbco\xb0Z
[project]
StaticUrlMapPath = {"assets/static/": "static/", "assets/docs/": "docs/", "media/": "media/"}
TemplateGlob = templates/**/*
MediaFilePath = "media/upload/"
[server]
Port = 7890
ReadTimeout = 60
WriteTimeout = 60

View File

@@ -8,16 +8,13 @@ package config
import (
"fmt"
"log"
"time"
"github.com/fsnotify/fsnotify"
"github.com/go-ini/ini"
"github.com/spf13/viper"
)
type SqlDataBase struct {
Type string
type DataBaseConfig struct {
Host string
Port string
User string
@@ -25,6 +22,9 @@ type SqlDataBase struct {
DBName string
Charset string
Prefix string
Driver string
DSN string
MaxConns int `mapstructure:"max_conns"`
}
type Jwt struct {
@@ -32,75 +32,95 @@ type Jwt struct {
}
type Project struct {
StaticUrlMapPath string
StaticUrlMapPath []interface{}
TemplateGlob string
MediaFilePath string
CurrentTheme string
}
type Server struct {
type ServerConfig struct {
Port string
EnableGzip bool `mapstructure:"enable_gzip"`
CSRFSecret string `mapstructure:"csrf_secret"`
ReadTimeout time.Duration
WriteTimeout time.Duration
}
var (
DataBase = &SqlDataBase{}
JwtSecretKey = &Jwt{}
ProjectCfg = &Project{}
HttpServer = &Server{}
)
func SetUp() {
cfg, err := ini.Load("config/conf.ini")
if err != nil {
panic(err)
}
if err := cfg.Section("mysql").MapTo(DataBase); err != nil {
panic(err)
}
if err := cfg.Section("jwt").MapTo(JwtSecretKey); err != nil {
panic(err)
}
if err := cfg.Section("project").MapTo(ProjectCfg); err != nil {
panic(err)
}
if err := cfg.Section("server").MapTo(HttpServer); err != nil {
panic(err)
}
type ThemeConfig struct {
Current string
AllowUpload bool `mapstructure:"allow_upload"`
}
type SecurityConfig struct {
JWTSecret string `mapstructure:"jwt_secret"`
CORSAllowedOrigins []string `mapstructure:"cors_allowed_origins"`
}
type Config struct {
Env string
Server ServerConfig
DataBase DataBaseConfig
Theme ThemeConfig
JwtSecretKey Jwt
Project Project
Security SecurityConfig
}
func SetUp1() {
func LoadConfig(configPath string) (*Config, error) {
// 1. 基础配置
viper.SetConfigFile(configPath)
// 设置配置文件的名称(不包括扩展名)
viper.SetConfigName("config.yml")
viper.SetConfigName("config")
// 设置配置文件所在的目录
viper.AddConfigPath("./conf")
viper.AddConfigPath("./config")
// 设置配置文件的类型为YAML
viper.SetConfigType("yaml")
// 3. 自动读取环境变量(自动转换大写和下划线)
viper.AutomaticEnv()
viper.SetEnvPrefix("BLOG") // 环境变量前缀 BLOG_xxx
// 2. 设置默认值
viper.SetDefault("server.port", 3000)
viper.SetDefault("database.max_conns", 10)
viper.SetDefault("theme.allow_upload", false)
// 读取配置文件
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("Error reading config file, %s", err)
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return nil, fmt.Errorf("配置文件未找到: %s", configPath)
}
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
//监控并重新读取配置文件
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
// 配置文件发生变更之后会调用的回调函数
fmt.Println("Config file changed:", e.Name)
})
// 5. 反序列化到结构体
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("配置解析失败: %w", err)
}
// 6. 校验必要配置项
// if cfg.Security.JWTSecret == "" {
// return nil, fmt.Errorf("安全配置错误: jwt_secret 必须设置")
// }
// 获取配置值
mysqlHost := viper.GetString("mysql.Host")
mysqlHost := viper.GetString("database.Host")
jwtSecretKey := viper.GetString("jwt.SecretKey")
serverPort := viper.GetInt("server.Port")
templateGlob := viper.GetString("project.TemplateGlob")
// 打印获取到的配置值
fmt.Printf("MySQL Host: %s\n", mysqlHost)
fmt.Printf("JWT Secret Key: %s\n", jwtSecretKey)
fmt.Printf("Server Port: %d\n", serverPort)
fmt.Printf("TemplateGlob: %s\n", templateGlob)
return &cfg, nil
}
func GetCurrentTheme() string {
return "default"

View File

@@ -1,5 +1,5 @@
database:
Type: mysql
Driver: mysql
Host: 47.93.160.42
Port: 3630
User: blog
@@ -7,10 +7,13 @@ database:
DBName: blog
Charset: utf8mb4
Prefix: gin_
DSN: "mysql:mysql@tcp(47.93.160.42:3630)/gin_blog?charset=utf8mb4&parseTime=True&loc=Local"
# Prefix: gin_ # This line is commented out in the original .ini file
jwt:
SecretKey: "\x13\xbf\xd2 1\xce\x8b\xc1\t\xc1=\xec\x07\x93\xd4\x9e\xbco\xb0Z"
security:
JWTSecret: "jwt_secret"
project:
StaticUrlMapPath:
@@ -21,7 +24,7 @@ project:
MediaFilePath: "media/upload/"
server:
Port: 7890
Port: 8090
ReadTimeout: 60
WriteTimeout: 60

View File

@@ -1,13 +1,17 @@
package controllers
import (
"go_blog/models"
"net/http"
"github.com/gin-gonic/gin"
)
func Home(c *gin.Context) {
c.HTML(http.StatusOK, "home.html", gin.H{
"title": "首页",
var items []models.Content
models.DB.Select("*").Limit(5).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
})
}

View File

@@ -1,13 +1,20 @@
package controllers
import (
"go_blog/models"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func ShowPost(c *gin.Context) {
c.HTML(http.StatusOK, "home.html", gin.H{
"title": "首页",
})
id, err := strconv.ParseInt(c.Param("id"), 10, 32)
if err != nil {
return
}
var content = models.Content{Cid: int32(id)}
models.DB.First(&content)
c.HTML(http.StatusOK, "post.tmpl", content)
}

146
main.go
View File

@@ -1,47 +1,34 @@
package main
import (
"fmt"
conf "go_blog/config"
"go_blog/controllers"
"go_blog/config"
"go_blog/models"
"go_blog/routers"
"go_blog/serializers"
"log"
"net/http"
"strconv"
"go_blog/themes"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"honnef.co/go/tools/config"
"github.com/gin-gonic/gin"
)
const templatePath = "./templates/*"
func init() {
conf.SetUp()
models.SetUp()
}
func main() {
conf, err := config.LoadConfig("config.yml")
if err != nil {
panic("配置文件加载失败: " + err.Error())
}
models.InitDatabase(conf)
// 4. 初始化主题管理器
themeManager := themes.NewManager()
if err := themeManager.LoadTheme(conf.GetCurrentTheme()); err != nil {
panic("主题加载失败: " + err.Error())
}
// if err := themeManager.LoadTheme(conf.GetCurrentTheme()); err != nil {
// panic("主题加载失败: " + err.Error())
// }
// 5. 创建Gin实例
router := gin.Default()
router.LoadHTMLGlob(templatePath)
// 注册WebSocket路由
registerRoutes(router)
router.GET("/events", esSSE)
// 6. 注册中间件
router.Use(
@@ -58,120 +45,7 @@ func main() {
// 8. 注册路由
routers.RegisterRoutes(router)
// 9. 启动服务
router.Run(":8910") //router.Run(":" + cfg.Server.Port)
}
func esSSE(c *gin.Context) {
w := c.Writer
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
_, ok := w.(http.Flusher)
if !ok {
log.Panic("server not support") //浏览器不兼容
}
_, err := fmt.Fprintf(w, "id: aaa\ndata: %s\n\n", "dsdf")
_, er1r := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime")
if err != nil || er1r != nil {
print("error", err)
return
}
}
func registerRoutes(r *gin.Engine) {
r.GET("/", func(c *gin.Context) {
var items []models.Content
models.DB.Select("*").Limit(5).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
})
})
r.GET("/page", func(c *gin.Context) {
var items []models.Content
var pager serializers.Pager
pager.InitPager(c)
offset := (pager.Page - 1) * pager.PageSize
models.DB.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
"Pager": pager,
"Title": "文章列表",
})
})
r.GET("/createcontent", func(c *gin.Context) {
c.HTML(http.StatusOK, "content.tmpl", nil)
})
r.GET("/post/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 32)
if err != nil {
return
}
var content = models.Content{Cid: int32(id)}
models.DB.First(&content)
c.HTML(http.StatusOK, "post.tmpl", content)
})
user := getUserInfo()
r.GET("/login", func(c *gin.Context) {
c.HTML(200, "login.tmpl", map[string]interface{}{
"title": "这个是titile,传入templates中的",
"user": user,
})
})
r.GET("/register", func(c *gin.Context) {
c.HTML(200, "register.tmpl", map[string]interface{}{
"title": "这个是titile,传入templates中的",
"user": user,
})
})
r.GET("/ws", controllers.WebSocketHandler)
r.POST("/content", controllers.CreateContentHandler)
r.POST("/login", controllers.UsersLoginHandler)
r.POST("/register", controllers.UsersRegisterHandler)
r.POST("/setinfo", controllers.UsersSetInfoHandler)
r.POST("/setpwd", controllers.UsersSetPwdHandler)
}
func getUserInfo() models.User {
user := models.User{
Name: "user",
Gender: "male",
Age: 18,
Password: "nothings",
PasswordHash: []byte("nothings"),
}
return user
}
func initDatabase() {
// 1. 初始化配置
cfg, err := config.Load("config.yaml")
if err != nil {
panic("加载配置失败: " + err.Error())
}
// 2. 初始化数据库
db, err := gorm.Open(mysql.Open(cfg.String()), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: conf.DataBase.Prefix,
},
})
if err != nil {
panic("数据库连接失败: " + err.Error())
}
// 3. 自动迁移数据模型
if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil {
panic("数据库迁移失败: " + err.Error())
}
router.Run(":" + conf.Server.Port) //router.Run()
}
// 自定义模板渲染器

View File

@@ -8,7 +8,7 @@ package models
import (
"fmt"
conf "go_blog/config"
"go_blog/config"
"go_blog/pkg/util"
"time"
@@ -19,7 +19,8 @@ import (
var DB *gorm.DB
func SetUp() {
func InitDatabase(conf *config.Config) {
conUri := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
conf.DataBase.User,
conf.DataBase.Password,
@@ -27,20 +28,23 @@ func SetUp() {
conf.DataBase.Port,
conf.DataBase.DBName,
conf.DataBase.Charset)
// 2. 初始化数据库
db, err := gorm.Open(mysql.Open(conUri), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: conf.DataBase.Prefix,
},
})
if err != nil {
panic(err)
panic("数据库连接失败: " + err.Error())
}
DB = db
DB = db
// 3. 自动迁移数据模型
DB.AutoMigrate(&Account{})
DB.AutoMigrate(&Content{})
// if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil {
// panic("数据库迁移失败: " + err.Error())
// }
}
type BaseModel struct {

View File

@@ -7,12 +7,12 @@
package jwt
import (
conf "go_blog/config"
"go_blog/models"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/spf13/viper"
)
// 定义jwt载荷
@@ -44,14 +44,14 @@ func GenToken(id uint64, username string) (string, error) {
username,
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString([]byte(conf.JwtSecretKey.SecretKey))
token, err := tokenClaims.SignedString([]byte(viper.GetString("config.JwtSecretKey.secretKey")))
return token, err
}
// 验证token合法性
func ValidateJwtToken(token string) (*UserClaims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(conf.JwtSecretKey.SecretKey), nil
return []byte(viper.GetString("config.JwtSecretKey.secretKey")), nil
})
if tokenClaims != nil {

View File

@@ -1,13 +1,43 @@
package routers // Add the package declaration at the top
import (
"fmt"
"go_blog/controllers"
"go_blog/models"
"go_blog/serializers"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(r *gin.Engine) {
// 注册WebSocket路由
r.GET("/events", esSSE)
r.GET("/page", func(c *gin.Context) {
var items []models.Content
var pager serializers.Pager
pager.InitPager(c)
offset := (pager.Page - 1) * pager.PageSize
models.DB.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
"Pager": pager,
"Title": "文章列表",
})
})
r.GET("/createcontent", func(c *gin.Context) {
c.HTML(http.StatusOK, "content.tmpl", nil)
})
r.GET("/ws", controllers.WebSocketHandler)
r.POST("/content", controllers.CreateContentHandler)
r.POST("/login", controllers.UsersLoginHandler)
r.POST("/register", controllers.UsersRegisterHandler)
r.POST("/setinfo", controllers.UsersSetInfoHandler)
r.POST("/setpwd", controllers.UsersSetPwdHandler)
// Frontend routes (dynamic themes)
r.GET("/", controllers.Home)
r.GET("/post/:id", controllers.ShowPost)
@@ -22,3 +52,35 @@ func RegisterRoutes(r *gin.Engine) {
// Static files for themes
r.StaticFS("/themes", http.Dir("web/themes"))
}
func getUserInfo() models.User {
user := models.User{
Name: "user",
Gender: "male",
Age: 18,
Password: "nothings",
PasswordHash: []byte("nothings"),
}
return user
}
func esSSE(c *gin.Context) {
w := c.Writer
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
_, ok := w.(http.Flusher)
if !ok {
log.Panic("server not support") //浏览器不兼容
}
_, err := fmt.Fprintf(w, "id: aaa\ndata: %s\n\n", "dsdf")
_, er1r := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime")
if err != nil || er1r != nil {
print("error", err)
return
}
}

View File

@@ -22,19 +22,19 @@ func (tm *ThemeManager) LoadTheme(themeName string) error {
tm.CurrentTheme = themeName
// Step 1: Validate the theme by reading theme.yaml
themeConfigPath := fmt.Sprintf("h:/code/go_blog/themes/%s/theme.yaml", themeName)
themeConfigPath := fmt.Sprintf("h:/code/go_blog/web/themes/%s/theme.yaml", themeName)
if _, err := os.Stat(themeConfigPath); os.IsNotExist(err) {
return fmt.Errorf("theme %s does not exist or is invalid", themeName)
}
// Step 2: Precompile all HTML templates and cache them
tm.Templates = make(map[string]*template.Template)
templateDir := fmt.Sprintf("h:/code/go_blog/themes/%s/templates", themeName)
templateDir := fmt.Sprintf("h:/code/go_blog/web/themes/%s/tmplates/", themeName)
err := filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".html") {
if !info.IsDir() && strings.HasSuffix(info.Name(), ".tmpl") {
tmpl, err := template.ParseFiles(path)
if err != nil {
return err

View File

@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ .Title}}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
body{
background: #f8f8f8;
}
.layui-main {
width: 1140px;
margin: 0 auto;
}
.container {
width: 1170px;
margin: 0 auto;
}
.header .layui-nav {
position: absolute;
right: 0;
top: 0;
padding: 0;
background: none;
}
.post-list {
width: 75%;
float: left;
display: block;
}
.list-card {
background: #fff;
overflow: hidden;
padding: 20px 20px 10px 20px;
position: relative;
border-radius: 10px;
margin-bottom: 10px;
height: 200px;
}
.sidebar {
float: left;
width: 25%;
}
.header {}
</style>
</head>
<body>
<div class="header">
<div class="layui-main">
<h1><a class="logo" href="https://www.hanxiaonuan.cn/">韩小暖的博客</a></h1>
</div>
<ul class="layui-nav">
<li class="layui-nav-item layui-hide-xs layui-this">
<a href="https://www.hanxiaonuan.cn/">首页</a>
</li>
<li class="layui-nav-item layui-hide-xs ">
<a href="https://www.hanxiaonuan.cn/start-page.html" title="关于">关于</a>
</li>
<span class="layui-nav-bar"></span></ul>
</div>
<div class="container">
<div class="post-list">
{{ range .Items }}
<div class="list-card">
<div><a href="/post/{{ .Cid }}">
<h1>Title: {{ .Title }}</h1>
</a>
<p> Slug: {{ .Slug }}</p>
<p>Text: {{ .Text }}</p>
<div>Created: {{ .Created }},</div>
</div>
</div>
{{ end }}
<div class="page-navigator">
共 {{ .Total }} 条,每页 {{ .PageSize }} 条,当前第 {{ .Page }} 页</div>
<div class="pagination">
{{ if .PrevPage }}
<a href="/page/{{ .PrevPage }}">上一页</a>
{{ end }}
{{ if .NextPage }}
<a href="/page/{{ .NextPage }}">下一页</a>
{{ end }}
<a href="/">首页</a>
<a href="/page/{{ .Total }}">尾页</a>
</div>
</div>
<div class="sidebar">
<div class="column">
<h3 class="title-sidebar"><i class="layui-icon"></i> 博客信息</h3>
<div class="personal-information">
<div class="user">
<img src="https://www.hanxiaonuan.cn/usr/uploads/2021/07/3991382612.jpg" alt="韩小暖的博客的头像"
class="rounded-circle avatar">
<div class="p-2">
<a class="user-name" target="_blank" href="https://www.hanxiaonuan.cn/">
韩小暖的博客</a>
<p class="introduction mt-1">这里是小暖的日常记录,欢迎!</p>
</div>
</div>
</div>
</div>
<div class="component">
<form class="layui-form" id="search" method="post" action="https://www.hanxiaonuan.cn/"
role="search">
<div class="layui-inline input">
<input type="text" id="s" name="s" class="layui-input" required="" lay-verify="required"
placeholder="输入关键字搜索">
</div>
<div class="layui-inline">
<button class="layui-btn layui-btn-sm layui-btn-primary"><i
class="layui-icon"></i></button>
</div>
</form>
</div>
<div class="column">
<h3 class="title-sidebar"><i class="layui-icon"></i> 栏目分类</h3>
<ul class="layui-row layui-col-space5">
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/default/"><i class="layui-icon"></i>
默认分类<span class="layui-badge layui-bg-gray">12</span></a></li>
<li class="layui-col-md12 layui-col-xs6"><a href="https://www.hanxiaonuan.cn/category/zc/"><i
class="layui-icon"></i> 日常随想<span class="layui-badge layui-bg-gray">2</span></a>
</li>
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/%E5%85%B3%E4%BA%8E%E6%88%BF%E5%AD%90/"><i
class="layui-icon"></i> 关于房子<span class="layui-badge layui-bg-gray">1</span></a>
</li>
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/%E5%B7%A5%E4%BD%9C%E6%97%A5%E5%BF%97/"><i
class="layui-icon"></i> 工作日志<span class="layui-badge layui-bg-gray">0</span></a>
</li>
</ul>
</div>
<div class="tags">
<h3 class="title-sidebar"><i class="layui-icon"></i>标签云</h3>
<div>
<a class="layui-btn layui-btn-xs layui-btn-primary" style="color: rgb(231, 229, 26)"
href="https://www.hanxiaonuan.cn/tag/%E5%B0%8F%E6%9A%96/" title="小暖">小暖</a>
<a class="layui-btn layui-btn-xs layui-btn-primary" style="color: rgb(125, 196, 207)"
href="https://www.hanxiaonuan.cn/tag/%E6%84%9F%E6%82%9F/" title="感悟">感悟</a>
</div>
</div>
<div class="tags">
<h3 class="title-sidebar"><i class="layui-icon"></i>系统</h3>
<div>
<li class="last"><a href="https://www.hanxiaonuan.cn/admin/">进入后台 (admin)</a></li>
<li><a href="https://www.hanxiaonuan.cn/action/logout?_=5b09b0e1048bbd45bdddc56fc43667b2">退出</a>
</li>
</div>
</div>
</div>
</div>
</body>
</html>

View File