优化页面、修复BUG

This commit is contained in:
李琦
2026-06-26 17:00:02 +08:00
parent 396272908b
commit e4ab62ab7e
44 changed files with 1058 additions and 688 deletions

View File

@@ -65,12 +65,7 @@ func Login(c *gin.Context) {
utils.Success(c, gin.H{
"token": tokenString,
"expire": expireUnix,
"user": gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
},
"user": repositories.BuildUserResponse(user),
})
}

View File

@@ -16,7 +16,7 @@ func GetCategories(c *gin.Context) {
utils.ServerError(c, err)
return
}
utils.Success(c, categories)
utils.Success(c, repositories.BuildCategoriesResponse(categories))
}
// GetCategoryByID 根据ID获取分类
@@ -38,7 +38,7 @@ func GetCategoryByID(c *gin.Context) {
return
}
utils.Success(c, category)
utils.Success(c, repositories.BuildCategoryResponse(category))
}
// AdminCreateCategory 创建分类
@@ -54,7 +54,7 @@ func AdminCreateCategory(c *gin.Context) {
return
}
utils.Success(c, category)
utils.Success(c, repositories.BuildCategoryResponse(&category))
}
// AdminUpdateCategory 更新分类
@@ -78,7 +78,7 @@ func AdminUpdateCategory(c *gin.Context) {
return
}
utils.Success(c, category)
utils.Success(c, repositories.BuildCategoryResponse(&category))
}
// AdminDeleteCategory 删除分类

View File

@@ -16,7 +16,7 @@ func GetColumns(c *gin.Context) {
utils.ServerError(c, err)
return
}
utils.Success(c, columns)
utils.Success(c, repositories.BuildColumnsResponse(columns))
}
// GetColumnByID 根据ID获取专栏

View File

@@ -20,7 +20,11 @@ func GetSettings(c *gin.Context) {
// 只返回前端需要的公开配置项
publicSettings := make(map[string]string)
publicKeys := []string{"site_title", "site_description", "site_author", "site_keywords", "visible_menus", "posts_per_page"}
publicKeys := []string{
"site_title", "site_description", "site_author", "site_keywords",
"visible_menus", "posts_per_page", "works_per_page", "snippets_per_page",
"footer_icp", "contact_email",
}
for _, key := range publicKeys {
if value, exists := settingsMap[key]; exists {

View File

@@ -26,6 +26,7 @@ func main() {
repositories.MigratePostSnippets()
repositories.MigratePostUserID()
repositories.MigrateUserAvatar()
repositories.MigrateUserProfileFields()
// 初始化ip2region (如果文件不存在将降级为普通IP记录)
// 函数会自动从环境变量或可执行文件目录查找 ip2region.xdb
@@ -101,6 +102,10 @@ func main() {
// 咨询相关路由
api.POST("/inquiries", handlers.SubmitInquiry)
api.GET("/email-suffixes", handlers.GetEmailSuffixes)
// 用户公开资料
api.GET("/users/:id/profile", handlers.GetUserProfile)
api.GET("/users/:id/posts", handlers.GetUserPosts)
}
// 管理员API路由组
@@ -113,6 +118,11 @@ func main() {
authAdmin := admin.Group("/")
authAdmin.Use(middleware.AuthMiddleware(), middleware.OperationLogMiddleware())
{
// 当前用户
authAdmin.GET("/me", handlers.GetCurrentUser)
authAdmin.PUT("/me", handlers.UpdateCurrentUser)
authAdmin.PUT("/me/password", handlers.UpdateCurrentUserPassword)
// 用户管理
authAdmin.GET("/users", middleware.PermissionMiddleware("users", "read"), handlers.AdminGetUsers)
authAdmin.GET("/users/:id", middleware.PermissionMiddleware("users", "read"), handlers.AdminGetUser)
@@ -198,6 +208,7 @@ func main() {
// 标签管理
authAdmin.GET("/tags", middleware.PermissionMiddleware("tags", "read"), handlers.AdminGetTags)
authAdmin.GET("/tags/:id", middleware.PermissionMiddleware("tags", "read"), handlers.GetTag)
authAdmin.POST("/tags", middleware.PermissionMiddleware("tags", "create"), handlers.AdminCreateTag)
authAdmin.PUT("/tags/:id", middleware.PermissionMiddleware("tags", "update"), handlers.AdminUpdateTag)
authAdmin.DELETE("/tags/:id", middleware.PermissionMiddleware("tags", "delete"), handlers.AdminDeleteTag)

View File

@@ -43,3 +43,14 @@ func (c *Category) BeforeUpdate(tx *gorm.DB) error {
c.UpdatedAt = time.Now().Unix()
return nil
}
// CategoryResponse 分类 API 响应
type CategoryResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
SortOrder uint `json:"sortOrder"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

View File

@@ -45,6 +45,20 @@ func (c *Column) BeforeUpdate(tx *gorm.DB) error {
return nil
}
// ColumnResponse 专栏 API 响应
type ColumnResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Cover string `json:"cover"`
IsActive int `json:"isActive"`
SortOrder uint `json:"sortOrder"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
PostCount int64 `json:"postCount,omitempty"`
LastUpdated string `json:"lastUpdated,omitempty"`
}
// ColumnPost 专栏文章关联模型
type ColumnPost struct {
ColumnID uint `json:"columnId" gorm:"primaryKey;column:column_id"`

View File

@@ -42,6 +42,15 @@ func (t *Tag) BeforeUpdate(tx *gorm.DB) error {
return nil
}
// TagResponse 标签 API 响应
type TagResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// PostTag 文章标签关联模型
type PostTag struct {
PostID uint `json:"postId" gorm:"primaryKey;column:post_id"`

View File

@@ -12,6 +12,10 @@ type User struct {
Username string `json:"username" gorm:"column:username;uniqueIndex;not null"`
Email string `json:"email" gorm:"column:email"`
Avatar string `json:"avatar" gorm:"column:avatar"`
Bio string `json:"bio" gorm:"column:bio;type:text"`
Phone string `json:"phone" gorm:"column:phone"`
Wechat string `json:"wechat" gorm:"column:wechat"`
WechatQrcode string `json:"wechatQrcode" gorm:"column:wechat_qrcode"`
Password string `json:"password,omitempty" gorm:"-"` // Virtual field for input
PasswordHash string `json:"-" gorm:"column:password_hash"`
RoleID uint `json:"roleId" gorm:"column:role_id"`
@@ -50,15 +54,47 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
// UserResponse 用户响应模型
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Avatar string `json:"avatar,omitempty"`
RoleID uint `json:"roleId"`
Role string `json:"role"`
IsActive int `json:"isActive"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Avatar string `json:"avatar,omitempty"`
Bio string `json:"bio,omitempty"`
Phone string `json:"phone,omitempty"`
Wechat string `json:"wechat,omitempty"`
WechatQrcode string `json:"wechatQrcode,omitempty"`
RoleID uint `json:"roleId"`
Role string `json:"role"`
IsActive int `json:"isActive"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// UserPublicProfile 公开用户资料(前台展示)
type UserPublicProfile struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Avatar string `json:"avatar,omitempty"`
Bio string `json:"bio,omitempty"`
Phone string `json:"phone,omitempty"`
Wechat string `json:"wechat,omitempty"`
WechatQrcode string `json:"wechatQrcode,omitempty"`
}
// UpdateProfileRequest 当前用户更新资料请求
type UpdateProfileRequest struct {
Email string `json:"email"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
Phone string `json:"phone"`
Wechat string `json:"wechat"`
WechatQrcode string `json:"wechatQrcode"`
}
// UpdatePasswordRequest 修改密码请求
type UpdatePasswordRequest struct {
OldPassword string `json:"oldPassword" binding:"required"`
NewPassword string `json:"newPassword" binding:"required"`
}
// LoginRequest 登录请求模型

View File

@@ -78,3 +78,25 @@ func DeleteCategory(id uint) error {
}
return nil
}
// BuildCategoryResponse 构建分类响应
func BuildCategoryResponse(category *models.Category) *models.CategoryResponse {
return &models.CategoryResponse{
ID: category.ID,
Name: category.Name,
Slug: category.Slug,
Description: category.Description,
SortOrder: category.SortOrder,
CreatedAt: formatTimestamp(category.CreatedAt),
UpdatedAt: formatTimestamp(category.UpdatedAt),
}
}
// BuildCategoriesResponse 构建分类列表响应
func BuildCategoriesResponse(categories []models.Category) []models.CategoryResponse {
responses := make([]models.CategoryResponse, 0, len(categories))
for _, category := range categories {
responses = append(responses, *BuildCategoryResponse(&category))
}
return responses
}

View File

@@ -87,24 +87,46 @@ func GetColumnStats(columnID uint) (int64, int64, error) {
}
// BuildColumnResponse 构建专栏响应(包含统计信息)
func BuildColumnResponse(col *models.Column) map[string]interface{} {
func BuildColumnResponse(col *models.Column) *models.ColumnResponse {
postCount, lastUpdated, _ := GetColumnStats(col.ID)
return map[string]interface{}{
"id": col.ID,
"name": col.Name,
"description": col.Description,
"cover": col.Cover,
"isActive": col.IsActive,
"sortOrder": col.SortOrder,
"createdAt": col.CreatedAt,
"updatedAt": col.UpdatedAt,
"deletedAt": col.DeletedAt,
"postCount": postCount,
"lastUpdated": lastUpdated,
return &models.ColumnResponse{
ID: col.ID,
Name: col.Name,
Description: col.Description,
Cover: col.Cover,
IsActive: col.IsActive,
SortOrder: col.SortOrder,
CreatedAt: formatTimestamp(col.CreatedAt),
UpdatedAt: formatTimestamp(col.UpdatedAt),
PostCount: postCount,
LastUpdated: formatTimestamp(lastUpdated),
}
}
// BuildColumnItemResponse 构建专栏列表项响应(不含统计)
func BuildColumnItemResponse(col *models.Column) *models.ColumnResponse {
return &models.ColumnResponse{
ID: col.ID,
Name: col.Name,
Description: col.Description,
Cover: col.Cover,
IsActive: col.IsActive,
SortOrder: col.SortOrder,
CreatedAt: formatTimestamp(col.CreatedAt),
UpdatedAt: formatTimestamp(col.UpdatedAt),
}
}
// BuildColumnsResponse 构建专栏列表响应
func BuildColumnsResponse(columns []models.Column) []models.ColumnResponse {
responses := make([]models.ColumnResponse, 0, len(columns))
for _, col := range columns {
responses = append(responses, *BuildColumnItemResponse(&col))
}
return responses
}
// CreateColumn 创建专栏
func CreateColumn(col *models.Column) error {
err := config.DB.Create(col).Error

View File

@@ -175,3 +175,23 @@ func MigrateUserAvatar() {
log.Printf("Adding users.avatar...")
execSQL("ALTER TABLE `users` ADD COLUMN `avatar` VARCHAR(500) NULL DEFAULT NULL COMMENT '头像URL' AFTER `email`")
}
// MigrateUserProfileFields adds profile fields to users if missing.
func MigrateUserProfileFields() {
if !columnExists("users", "bio") {
log.Printf("Adding users.bio...")
execSQL("ALTER TABLE `users` ADD COLUMN `bio` TEXT NULL COMMENT '个人介绍' AFTER `avatar`")
}
if !columnExists("users", "phone") {
log.Printf("Adding users.phone...")
execSQL("ALTER TABLE `users` ADD COLUMN `phone` VARCHAR(20) NULL DEFAULT NULL COMMENT '手机号' AFTER `bio`")
}
if !columnExists("users", "wechat") {
log.Printf("Adding users.wechat...")
execSQL("ALTER TABLE `users` ADD COLUMN `wechat` VARCHAR(100) NULL DEFAULT NULL COMMENT '微信号' AFTER `phone`")
}
if !columnExists("users", "wechat_qrcode") {
log.Printf("Adding users.wechat_qrcode...")
execSQL("ALTER TABLE `users` ADD COLUMN `wechat_qrcode` VARCHAR(500) NULL DEFAULT NULL COMMENT '微信二维码图片URL' AFTER `wechat`")
}
}

View File

@@ -76,6 +76,36 @@ func GetPosts(keyword string, categoryID uint, tagID uint, columnID uint, page i
return posts, total, nil
}
// GetPostsByUserID 获取用户已发布文章(用于公开资料页)
func GetPostsByUserID(userID uint, sort string, limit int) ([]models.Post, error) {
if limit <= 0 {
limit = 5
}
if limit > 20 {
limit = 20
}
orderClause := "created_at DESC"
if sort == "popular" {
orderClause = "read_count DESC, created_at DESC"
}
var posts []models.Post
err := config.DB.Model(&models.Post{}).
Where("user_id = ? AND is_published = ? AND deleted_at = ?", userID, 1, 0).
Preload("Category").
Preload("Column").
Preload("Tags").
Order(orderClause).
Limit(limit).
Find(&posts).Error
if err != nil {
log.Printf("Error querying posts by user ID: %v", err)
return nil, err
}
return posts, nil
}
// GetPostByID 根据ID获取博客文章
func GetPostByID(id uint) (*models.Post, error) {
var post models.Post
@@ -452,6 +482,8 @@ func buildPostResponse(post *models.Post, includeContent, includeSnippets bool,
if post.Category != nil {
catName = post.Category.Name
catSlug = post.Category.Slug
} else if post.CategoryID > 0 {
catName = lookupCategoryName(post.CategoryID)
}
colName := ""

View File

@@ -160,11 +160,21 @@ func GetPostsByTagID(tagID uint) ([]models.Post, error) {
}
// BuildTagsResponse 构建标签列表响应
func BuildTagsResponse(tags []models.Tag) []models.Tag {
return tags
func BuildTagsResponse(tags []models.Tag) []models.TagResponse {
responses := make([]models.TagResponse, 0, len(tags))
for _, tag := range tags {
responses = append(responses, *BuildTagResponse(&tag))
}
return responses
}
// BuildTagResponse 构建标签响应
func BuildTagResponse(tag *models.Tag) *models.Tag {
return tag
func BuildTagResponse(tag *models.Tag) *models.TagResponse {
return &models.TagResponse{
ID: tag.ID,
Name: tag.Name,
Slug: tag.Slug,
CreatedAt: formatTimestamp(tag.CreatedAt),
UpdatedAt: formatTimestamp(tag.UpdatedAt),
}
}

View File

@@ -134,12 +134,16 @@ func UpdateUser(user *models.User) error {
}
updates := map[string]interface{}{
"username": user.Username,
"email": user.Email,
"avatar": user.Avatar,
"role": user.Role,
"is_active": user.IsActive,
"updated_at": time.Now().Unix(),
"username": user.Username,
"email": user.Email,
"avatar": user.Avatar,
"bio": user.Bio,
"phone": user.Phone,
"wechat": user.Wechat,
"wechat_qrcode": user.WechatQrcode,
"role": user.Role,
"is_active": user.IsActive,
"updated_at": time.Now().Unix(),
}
if user.RoleID != 0 {
@@ -205,18 +209,56 @@ func GetUserCount() (int, error) {
// BuildUserResponse 构建用户响应
func BuildUserResponse(user *models.User) *models.UserResponse {
return &models.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Avatar: user.Avatar,
RoleID: user.RoleID,
Role: user.Role,
IsActive: user.IsActive,
CreatedAt: time.Unix(user.CreatedAt, 0).Format("2006-01-02 15:04:05"),
UpdatedAt: time.Unix(user.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
ID: user.ID,
Username: user.Username,
Email: user.Email,
Avatar: user.Avatar,
Bio: user.Bio,
Phone: user.Phone,
Wechat: user.Wechat,
WechatQrcode: user.WechatQrcode,
RoleID: user.RoleID,
Role: user.Role,
IsActive: user.IsActive,
CreatedAt: time.Unix(user.CreatedAt, 0).Format("2006-01-02 15:04:05"),
UpdatedAt: time.Unix(user.UpdatedAt, 0).Format("2006-01-02 15:04:05"),
}
}
// BuildUserPublicProfile 构建公开用户资料
func BuildUserPublicProfile(user *models.User) *models.UserPublicProfile {
return &models.UserPublicProfile{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Avatar: user.Avatar,
Bio: user.Bio,
Phone: user.Phone,
Wechat: user.Wechat,
WechatQrcode: user.WechatQrcode,
}
}
// UpdateCurrentUserProfile 更新当前用户资料(不含角色/状态)
func UpdateCurrentUserProfile(id uint, req *models.UpdateProfileRequest) error {
err := config.DB.Model(&models.User{}).
Where("id = ? AND deleted_at = ?", id, 0).
Updates(map[string]interface{}{
"email": req.Email,
"avatar": req.Avatar,
"bio": req.Bio,
"phone": req.Phone,
"wechat": req.Wechat,
"wechat_qrcode": req.WechatQrcode,
"updated_at": time.Now().Unix(),
}).Error
if err != nil {
log.Printf("Error updating user profile: %v", err)
return err
}
return nil
}
// BuildUsersResponse 构建用户列表响应
func BuildUsersResponse(users []models.User) []models.UserResponse {
var responses []models.UserResponse