Refactor site configuration and color scheme management

- Replaced static config with dynamic site info context.
- Updated color scheme handling in various components to use site info.
- Removed deprecated config file and integrated site info fetching.
- Enhanced user preference page to allow color scheme selection.
- Adjusted blog and console components to reflect new site info structure.
- Improved error handling and fallback mechanisms for site info retrieval.
This commit is contained in:
2025-09-26 00:25:34 +08:00
parent 0812e334df
commit f501948f91
42 changed files with 770 additions and 199 deletions

View File

@ -1 +1,64 @@
package v1
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/common/utils"
"github.com/snowykami/neo-blog/internal/repo"
"github.com/snowykami/neo-blog/pkg/resps"
)
const (
KeySiteInfo = "site_info"
)
type MiscController struct{}
func NewMiscController() *MiscController {
return &MiscController{}
}
func (mc *MiscController) GetSiteInfo(ctx context.Context, c *app.RequestContext) {
value, err := repo.KV.GetKV(KeySiteInfo, utils.H{
"metadata": utils.H{
"name": "Neo Blog S",
"icon": "https://cdn.liteyuki.org/snowykami/avatar.jpg",
"description": "A neo blog system.",
},
"color_schemes": []string{"blue", "green", "orange", "red", "rose", "violet", "yellow"},
"default_cover": "https://cdn.liteyuki.org/blog/background.png",
"owner": utils.H{
"name": "SnowyKami",
"description": "A full-stack developer.",
"avatar": "https://cdn.liteyuki.org/snowykami/avatar.jpg",
},
"posts_per_page": 9,
"comments_per_page": 8,
"verify_code_cool_down": 60,
"animation_duration_second": 0.618,
"footer": utils.H{
"text": "Liteyuki ICP 114514",
"links": []string{"https://www.liteyuki.com/"},
},
})
if err != nil {
resps.InternalServerError(c, err.Error())
return
}
resps.Ok(c, "", value)
}
func (mc *MiscController) SetSiteInfo(ctx context.Context, c *app.RequestContext) {
data := make(map[string]interface{})
err := c.BindAndValidate(&data)
if err != nil {
resps.BadRequest(c, err.Error())
return
}
err = repo.KV.SetKV(KeySiteInfo, data)
if err != nil {
resps.InternalServerError(c, err.Error())
}
resps.Ok(c, "", nil)
}

View File

@ -1,102 +1,104 @@
package dto
type UserDto struct {
ID uint `json:"id"` // 用户ID
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"` // 头像URL
BackgroundUrl string `json:"background_url"`
Email string `json:"email"` // 邮箱
Gender string `json:"gender"`
Role string `json:"role"`
Language string `json:"language"` // 语言
ID uint `json:"id"` // 用户ID
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"` // 头像URL
BackgroundUrl string `json:"background_url"`
PreferredColor string `json:"preferred_color"` // 主题色
Email string `json:"email"` // 邮箱
Gender string `json:"gender"`
Role string `json:"role"`
Language string `json:"language"` // 语言
}
type UserOidcConfigDto struct {
Name string `json:"name"` // OIDC配置名称
DisplayName string `json:"display_name"` // OIDC配置显示名称
Icon string `json:"icon"` // OIDC配置图标URL
LoginUrl string `json:"login_url"` // OIDC登录URL
Name string `json:"name"` // OIDC配置名称
DisplayName string `json:"display_name"` // OIDC配置显示名称
Icon string `json:"icon"` // OIDC配置图标URL
LoginUrl string `json:"login_url"` // OIDC登录URL
}
type UserLoginReq struct {
Username string `json:"username"` // username or email
Password string `json:"password"`
Username string `json:"username"` // username or email
Password string `json:"password"`
}
type UserLoginResp struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User UserDto `json:"user"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User UserDto `json:"user"`
}
type UserRegisterReq struct {
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"` // 昵称
Password string `json:"password"` // 密码
Email string `json:"-" binding:"-"`
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"` // 昵称
Password string `json:"password"` // 密码
Email string `json:"-" binding:"-"`
}
type UserRegisterResp struct {
Token string `json:"token"` // 访问令牌
RefreshToken string `json:"refresh_token"` // 刷新令牌
User UserDto `json:"user"` // 用户信息
Token string `json:"token"` // 访问令牌
RefreshToken string `json:"refresh_token"` // 刷新令牌
User UserDto `json:"user"` // 用户信息
}
type VerifyEmailReq struct {
Email string `json:"email"` // 邮箱地址
Email string `json:"email"` // 邮箱地址
}
type VerifyEmailResp struct {
Success bool `json:"success"` // 验证码发送成功与否
Success bool `json:"success"` // 验证码发送成功与否
}
type OidcLoginReq struct {
Name string `json:"name"` // OIDC配置名称
Code string `json:"code"` // OIDC授权码
State string `json:"state"`
Name string `json:"name"` // OIDC配置名称
Code string `json:"code"` // OIDC授权码
State string `json:"state"`
}
type OidcLoginResp struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User UserDto `json:"user"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User UserDto `json:"user"`
}
type ListOidcConfigResp struct {
OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表
OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表
}
type GetUserReq struct {
UserID uint `json:"user_id"`
UserID uint `json:"user_id"`
}
type GetUserByUsernameReq struct {
Username string `json:"username"`
Username string `json:"username"`
}
type GetUserResp struct {
User UserDto `json:"user"` // 用户信息
User UserDto `json:"user"` // 用户信息
}
type UpdateUserReq struct {
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"`
BackgroundUrl string `json:"background_url"`
Gender string `json:"gender"`
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar_url"`
BackgroundUrl string `json:"background_url"`
PreferredColor string `json:"preferred_color"`
Gender string `json:"gender"`
}
type UpdateUserResp struct {
User *UserDto `json:"user"` // 更新后的用户信息
User *UserDto `json:"user"` // 更新后的用户信息
}
type UpdatePasswordReq struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
type ResetPasswordReq struct {
Email string `json:"-" binding:"-"`
NewPassword string `json:"new_password"`
Email string `json:"-" binding:"-"`
NewPassword string `json:"new_password"`
}

View File

@ -1 +1,84 @@
package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
// JSONMap 是一个通用的 JSON 类型map[string]any
type JSONMap map[string]any
func (JSONMap) GormDataType() string {
return "json"
}
func (JSONMap) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
switch db.Dialector.Name() {
case "mysql":
return "JSON"
case "postgres":
return "JSONB"
default: // sqlite 等
return "TEXT"
}
}
// Value 实现 driver.Valuer用于写入数据库
func (j JSONMap) Value() (driver.Value, error) {
if j == nil {
return nil, nil
}
b, err := json.Marshal(map[string]any(j))
if err != nil {
return nil, err
}
return string(b), nil
}
// Scan 实现 sql.Scanner用于从数据库读取并反序列化
func (j *JSONMap) Scan(src any) error {
if src == nil {
*j = nil
return nil
}
var data []byte
switch v := src.(type) {
case string:
data = []byte(v)
case []byte:
data = v
default:
return fmt.Errorf("cannot scan JSONMap from %T", src)
}
if len(data) == 0 {
*j = nil
return nil
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
return err
}
*j = JSONMap(m)
return nil
}
func (j JSONMap) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any(j))
}
func (j *JSONMap) UnmarshalJSON(b []byte) error {
if len(b) == 0 || string(b) == "null" {
*j = nil
return nil
}
var m map[string]any
if err := json.Unmarshal(b, &m); err != nil {
return err
}
*j = JSONMap(m)
return nil
}

View File

@ -1,5 +1,6 @@
package main
func main() {
package model
type KV struct {
Key string `gorm:"primaryKey;type:varchar(64);not null;comment:键"`
Value JSONMap
}

View File

@ -1,41 +1,43 @@
package model
import (
"github.com/snowykami/neo-blog/internal/dto"
"gorm.io/gorm"
"github.com/snowykami/neo-blog/internal/dto"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Username string `gorm:"uniqueIndex;not null"` // 用户名,唯一
Nickname string `gorm:"default:''"` // 昵称
AvatarUrl string
BackgroundUrl string
Email string `gorm:"uniqueIndex"`
Gender string `gorm:"default:''"`
Role string `gorm:"default:'user'"` // user editor admin
Language string `gorm:"default:'en'"`
Password string // 密码,存储加密后的值
gorm.Model
Username string `gorm:"uniqueIndex;not null"` // 用户名,唯一
Nickname string `gorm:"default:''"` // 昵称
AvatarUrl string
BackgroundUrl string
PreferredColor string `gorm:"default:'global'"` // 主题色
Email string `gorm:"uniqueIndex"`
Gender string `gorm:"default:''"`
Role string `gorm:"default:'user'"` // user editor admin
Language string `gorm:"default:'en'"`
Password string // 密码,存储加密后的值
}
type UserOpenID struct {
gorm.Model
UserID uint `gorm:"index"`
User User `gorm:"foreignKey:UserID;references:ID"`
Issuer string `gorm:"index"` // OIDC Issuer
Sub string `gorm:"index"` // OIDC Sub openid
gorm.Model
UserID uint `gorm:"index"`
User User `gorm:"foreignKey:UserID;references:ID"`
Issuer string `gorm:"index"` // OIDC Issuer
Sub string `gorm:"index"` // OIDC Sub openid
}
func (user *User) ToDto() dto.UserDto {
return dto.UserDto{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
AvatarUrl: user.AvatarUrl,
BackgroundUrl: user.BackgroundUrl,
Email: user.Email,
Gender: user.Gender,
Role: user.Role,
Language: user.Language,
}
return dto.UserDto{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
AvatarUrl: user.AvatarUrl,
BackgroundUrl: user.BackgroundUrl,
PreferredColor: user.PreferredColor,
Email: user.Email,
Gender: user.Gender,
Role: user.Role,
Language: user.Language,
}
}

View File

@ -129,6 +129,7 @@ func migrate() error {
&model.Label{},
&model.Like{},
&model.File{},
&model.KV{},
&model.OidcConfig{},
&model.Post{},
&model.Session{},

View File

@ -1,5 +1,50 @@
package main
package repo
func main() {
import (
"errors"
"github.com/snowykami/neo-blog/internal/model"
"gorm.io/gorm"
)
type kvRepo struct{}
var KV = &kvRepo{}
func (k *kvRepo) GetKV(key string, defaultValue ...any) (any, error) {
var kv = &model.KV{}
err := GetDB().First(kv, "key = ?", key).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
if len(defaultValue) > 0 {
return defaultValue[0], nil
}
return nil, nil
}
return nil, err
}
if kv.Value == nil {
if len(defaultValue) > 0 {
return defaultValue[0], nil
}
return nil, nil
}
if v, ok := kv.Value["value"]; ok {
return v, nil
}
if len(defaultValue) > 0 {
return defaultValue[0], nil
}
return nil, nil
}
func (k *kvRepo) SetKV(key string, value any) error {
kv := &model.KV{
Key: key,
Value: map[string]any{"value": value},
}
return GetDB().Save(kv).Error
}

View File

@ -1 +1,18 @@
package repo
import "testing"
func TestKvRepo_GetKV(t *testing.T) {
err := KV.SetKV("AAA", map[string]interface{}{"b": 1, "c": "2"})
if err != nil {
t.Fatal(err)
}
v, _ := KV.GetKV("AAA")
t.Log(v)
if v.(map[string]interface{})["b"] != float64(1) {
t.Fatal("b not equal")
}
if v.(map[string]interface{})["c"] != "2" {
t.Fatal("c not equal")
}
}

View File

@ -1 +1,18 @@
package apiv1
import (
"github.com/cloudwego/hertz/pkg/route"
v1 "github.com/snowykami/neo-blog/internal/controller/v1"
"github.com/snowykami/neo-blog/internal/middleware"
"github.com/snowykami/neo-blog/pkg/constant"
)
func registerMiscRoutes(group *route.RouterGroup) {
miscController := v1.NewMiscController()
miscGroupAdmin := group.Group("/misc").Use(middleware.UseAuth(true)).Use(middleware.UseRole(constant.RoleAdmin))
miscGroupWithoutAuth := group.Group("/misc").Use(middleware.UseAuth(false))
{
miscGroupWithoutAuth.GET("/site-info", miscController.GetSiteInfo)
miscGroupAdmin.PUT("/site-info", miscController.SetSiteInfo)
}
}

View File

@ -3,15 +3,16 @@ package apiv1
import "github.com/cloudwego/hertz/pkg/app/server"
func RegisterRoutes(h *server.Hertz) {
apiV1Group := h.Group("/api/v1")
{
registerCommentRoutes(apiV1Group)
registerAdminRoutes(apiV1Group)
registerFileRoutes(apiV1Group)
registerLabelRoutes(apiV1Group)
registerLikeRoutes(apiV1Group)
registerPageRoutes(apiV1Group)
registerPostRoutes(apiV1Group)
registerUserRoutes(apiV1Group)
}
apiV1Group := h.Group("/api/v1")
{
registerCommentRoutes(apiV1Group)
registerAdminRoutes(apiV1Group)
registerFileRoutes(apiV1Group)
registerLabelRoutes(apiV1Group)
registerLikeRoutes(apiV1Group)
registerMiscRoutes(apiV1Group)
registerPageRoutes(apiV1Group)
registerPostRoutes(apiV1Group)
registerUserRoutes(apiV1Group)
}
}

View File

@ -355,10 +355,12 @@ func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, e
Model: gorm.Model{
ID: req.ID,
},
Username: req.Username,
Nickname: req.Nickname,
Gender: req.Gender,
AvatarUrl: req.AvatarUrl,
Username: req.Username,
Nickname: req.Nickname,
Gender: req.Gender,
AvatarUrl: req.AvatarUrl,
BackgroundUrl: req.BackgroundUrl,
PreferredColor: req.PreferredColor,
}
err := repo.User.UpdateUser(user)
if err != nil {