feat(server): implement chat file TTL cleanup and enhance JPush vendor channel support
Add FileCleanupWorker for automatic expiration of chat files with configurable retention period and batch processing. Files uploaded via `/api/v1/uploads/files` are tracked in `uploaded_files` table with expiration timestamps, then deleted from S3 and database upon expiry. Expired file URLs are injected as `"expired": true` in message responses. Extend JPush push notification configuration with vendor-specific channel_id support (xiaomi, huawei, oppo, vivo, meizu, honor, fcm) for differentiating system vs chat notifications. Add iOS APNs thread-id grouping configuration for notification categorization.
This commit is contained in:
@@ -90,20 +90,37 @@ type MessageHandler struct {
|
||||
messageService service.MessageService
|
||||
userService service.UserService
|
||||
groupService service.GroupService
|
||||
uploadService service.UploadService
|
||||
wsPublisher ws.MessagePublisher
|
||||
}
|
||||
|
||||
// NewMessageHandler 创建消息处理器
|
||||
func NewMessageHandler(chatService service.ChatService, messageService service.MessageService, userService service.UserService, groupService service.GroupService, wsPublisher ws.MessagePublisher) *MessageHandler {
|
||||
func NewMessageHandler(chatService service.ChatService, messageService service.MessageService, userService service.UserService, groupService service.GroupService, uploadService service.UploadService, wsPublisher ws.MessagePublisher) *MessageHandler {
|
||||
return &MessageHandler{
|
||||
chatService: chatService,
|
||||
messageService: messageService,
|
||||
userService: userService,
|
||||
groupService: groupService,
|
||||
uploadService: uploadService,
|
||||
wsPublisher: wsPublisher,
|
||||
}
|
||||
}
|
||||
|
||||
// convertMessagesWithExpiry 将消息列表转换为响应,并对已过期的文件 segment 注入 expired 标记。
|
||||
// 若 uploadService 不可用或查询失败,降级为不带过期标记的普通转换。
|
||||
func (h *MessageHandler) convertMessagesWithExpiry(ctx context.Context, messages []*model.Message) []*dto.MessageResponse {
|
||||
var expiredSet map[string]struct{}
|
||||
if h.uploadService != nil {
|
||||
if es, err := h.uploadService.GetExpiredURLSet(ctx); err == nil {
|
||||
expiredSet = es
|
||||
}
|
||||
}
|
||||
if len(expiredSet) == 0 {
|
||||
return dto.ConvertMessagesToResponse(messages)
|
||||
}
|
||||
return dto.ConvertMessagesToResponseWithExpiry(messages, expiredSet)
|
||||
}
|
||||
|
||||
// HandleTyping 输入状态上报
|
||||
// POST /api/v1/conversations/typing
|
||||
func (h *MessageHandler) HandleTyping(c *gin.Context) {
|
||||
@@ -287,8 +304,8 @@ func (h *MessageHandler) GetMessages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
result := dto.ConvertMessagesToResponse(messages)
|
||||
// 转换为响应格式(标记已过期文件)
|
||||
result := h.convertMessagesWithExpiry(c.Request.Context(), messages)
|
||||
|
||||
response.Success(c, &dto.MessageSyncResponse{
|
||||
Messages: result,
|
||||
@@ -315,8 +332,8 @@ func (h *MessageHandler) GetMessages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
result := dto.ConvertMessagesToResponse(messages)
|
||||
// 转换为响应格式(标记已过期文件)
|
||||
result := h.convertMessagesWithExpiry(c.Request.Context(), messages)
|
||||
|
||||
response.Success(c, &dto.MessageSyncResponse{
|
||||
Messages: result,
|
||||
@@ -335,8 +352,8 @@ func (h *MessageHandler) GetMessages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
result := dto.ConvertMessagesToResponse(messages)
|
||||
// 转换为响应格式(标记已过期文件)
|
||||
result := h.convertMessagesWithExpiry(c.Request.Context(), messages)
|
||||
|
||||
response.Paginated(c, result, total, page, pageSize)
|
||||
}
|
||||
@@ -857,8 +874,8 @@ func (h *MessageHandler) HandleGetMessages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
result := dto.ConvertMessagesToResponse(messages)
|
||||
// 转换为响应格式(标记已过期文件)
|
||||
result := h.convertMessagesWithExpiry(c.Request.Context(), messages)
|
||||
|
||||
response.Success(c, &dto.MessageSyncResponse{
|
||||
Messages: result,
|
||||
@@ -885,8 +902,8 @@ func (h *MessageHandler) HandleGetMessages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
result := dto.ConvertMessagesToResponse(messages)
|
||||
// 转换为响应格式(标记已过期文件)
|
||||
result := h.convertMessagesWithExpiry(c.Request.Context(), messages)
|
||||
|
||||
response.Success(c, &dto.MessageSyncResponse{
|
||||
Messages: result,
|
||||
@@ -905,8 +922,8 @@ func (h *MessageHandler) HandleGetMessages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
result := dto.ConvertMessagesToResponse(messages)
|
||||
// 转换为响应格式(标记已过期文件)
|
||||
result := h.convertMessagesWithExpiry(c.Request.Context(), messages)
|
||||
|
||||
response.Paginated(c, result, total, page, pageSize)
|
||||
}
|
||||
@@ -1067,8 +1084,8 @@ func (h *MessageHandler) GetMessagesByCursor(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
items := dto.ConvertMessagesToResponse(result.Items)
|
||||
// 转换为响应格式(标记已过期文件)
|
||||
items := h.convertMessagesWithExpiry(c.Request.Context(), result.Items)
|
||||
|
||||
response.Success(c, &dto.MessageCursorPageResponse{
|
||||
Items: items,
|
||||
|
||||
@@ -92,3 +92,39 @@ func (h *UploadHandler) UploadCover(c *gin.Context) {
|
||||
|
||||
response.Success(c, gin.H{"url": url})
|
||||
}
|
||||
|
||||
// UploadFile 上传聊天/通用文件(非图片类资源)
|
||||
// form 字段:file(必填)、folder(可选,默认 chat)
|
||||
func (h *UploadHandler) UploadFile(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "file is required")
|
||||
return
|
||||
}
|
||||
|
||||
folder := c.DefaultPostForm("folder", "chat")
|
||||
|
||||
result, err := h.uploadService.UploadFile(c.Request.Context(), file, folder, userID)
|
||||
if err != nil {
|
||||
// 参数类错误(大小/类型)返回 400,其余返回 500
|
||||
if file.Size > 0 && file.Size <= 100<<20 {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.InternalServerError(c, "failed to upload file")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"url": result.URL,
|
||||
"name": result.Name,
|
||||
"size": result.Size,
|
||||
"mime_type": result.MimeType,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user