feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等

新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
This commit is contained in:
2026-10-01 03:06:05 +08:00
parent 7fbd6ba699
commit ff2ab286fb
141 changed files with 7527 additions and 1407 deletions

View File

@@ -75,8 +75,8 @@ type RoomView struct {
Joined bool `json:"joined"`
MyRole string `json:"my_role"`
UnreadCount int64 `json:"unread_count"`
Pinned bool `json:"pinned"` // 对当前用户是否置顶(含大厅强制)
PinForced bool `json:"pin_forced"` // 大厅强制置顶,不可取消
Pinned bool `json:"pinned"` // 对当前用户是否置顶(含大厅强制)
PinForced bool `json:"pin_forced"` // 大厅强制置顶,不可取消
Peer *model.User `json:"peer,omitempty"` // 私聊对方(仅 direct)
}
@@ -114,6 +114,15 @@ func (s *ChatService) membership(tx *gorm.DB, roomID, userID uint) (*model.ChatR
if err != nil {
return nil, err
}
// 过期禁言自动解除:惰性清理,不依赖定时任务
if m.Muted && m.MutedUntil != nil && m.MutedUntil.Before(time.Now()) {
m.Muted = false
m.MutedUntil = nil
_ = tx.Model(&model.ChatRoomMember{}).
Where("room_id = ? AND user_id = ?", roomID, userID).
Select("muted", "muted_until").
Updates(map[string]interface{}{"muted": false, "muted_until": nil}).Error
}
return &m, nil
}
@@ -758,44 +767,85 @@ func (s *ChatService) Kick(operatorID, roomID, targetID uint, oversee bool) erro
return s.removeMember(roomID, targetID)
}
// SetMemberMute 禁言/解禁:群主、群管、全站监管;不可禁言群主或站点站长账号
func (s *ChatService) SetMemberMute(operatorID, roomID, targetID uint, muted, oversee bool) error {
// SendSystemMessage 向房间落库一条系统提示消息(sender_id=0,is_system=true),
// 并推进房间 last_message_id 与 updated_at。失败返回 nil + error。
func (s *ChatService) SendSystemMessage(roomID uint, content string) (*model.ChatMessage, error) {
msg := &model.ChatMessage{
RoomID: roomID,
SenderID: 0,
Content: content,
IsSystem: true,
CreatedAt: time.Now(),
}
err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(msg).Error; err != nil {
return err
}
return tx.Model(&model.ChatRoom{}).Where("id = ?", roomID).
Updates(map[string]interface{}{
"last_message_id": msg.ID,
"updated_at": time.Now(),
}).Error
})
if err != nil {
return nil, err
}
return msg, nil
}
// SetMemberMute 禁言/解禁:群主、群管、全站监管;不可禁言群主或站点站长账号。
// duration 为禁言时长;0 表示永久禁言;muted=false 时忽略 duration 并清空 muted_until。
// 返回目标用户昵称(供广播系统提示使用)。
func (s *ChatService) SetMemberMute(operatorID, roomID, targetID uint, muted bool, duration time.Duration, oversee bool) (string, error) {
room, err := s.getRoom(roomID)
if err != nil {
return err
return "", err
}
if room.RoomType == model.ChatRoomTypeDirect {
return ErrChatOwnerOnly
return "", ErrChatOwnerOnly
}
target, err := s.membership(s.db, roomID, targetID)
if err != nil {
return err
return "", err
}
if target.Role == model.ChatRoleOwner {
return ErrChatCannotMuteOwner
return "", ErrChatCannotMuteOwner
}
var targetUser model.User
if err := s.db.Select("role").First(&targetUser, targetID).Error; err != nil {
return ErrChatUserGone
if err := s.db.Select("role", "nickname", "username").First(&targetUser, targetID).Error; err != nil {
return "", ErrChatUserGone
}
if targetUser.Role == model.RoleOwner {
return ErrChatCannotMuteOwner
return "", ErrChatCannotMuteOwner
}
if !oversee {
op, err := s.membership(s.db, roomID, operatorID)
if err != nil {
return err
return "", err
}
if op.Role != model.ChatRoleOwner && op.Role != model.ChatRoleAdmin {
return ErrChatMuteDenied
return "", ErrChatMuteDenied
}
if target.Role == model.ChatRoleAdmin && op.Role != model.ChatRoleOwner {
return ErrChatMuteDenied
return "", ErrChatMuteDenied
}
}
return s.db.Model(&model.ChatRoomMember{}).
var mutedUntil *time.Time
if muted && duration > 0 {
t := time.Now().Add(duration)
mutedUntil = &t
}
if err := s.db.Model(&model.ChatRoomMember{}).
Where("room_id = ? AND user_id = ?", roomID, targetID).
Select("muted").Update("muted", muted).Error
Select("muted", "muted_until").
Updates(map[string]interface{}{"muted": muted, "muted_until": mutedUntil}).Error; err != nil {
return "", err
}
targetName := targetUser.Nickname
if targetName == "" {
targetName = targetUser.Username
}
return targetName, nil
}
// SetMemberRole 仅站点站长可任命/撤销群管理员(admin <-> member);不可改群主