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:
151
internal/pkg/jpush/vendor/params.go
vendored
Normal file
151
internal/pkg/jpush/vendor/params.go
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
// Package vendor 提供厂商无关的消息参数抽象与各厂商推送字段转换。
|
||||
//
|
||||
// 设计要点:
|
||||
// - 调用方只需提供厂商无关的 MessageParams,不感知厂商差异。
|
||||
// - 由于无法预知目标设备的具体厂商,JPush 要求所有厂商私有配置全数下发,
|
||||
// 由 JPush 服务端按目标设备厂商路由。
|
||||
// - 双层架构:TextRenderer(基础层,产出 Android/iOS 通用通知)+
|
||||
// PrivateConverter(附加层,产出各厂商 third_party_channel 私有字段)。
|
||||
package vendor
|
||||
|
||||
import (
|
||||
"with_you/internal/config"
|
||||
"with_you/internal/pkg/jpush"
|
||||
)
|
||||
|
||||
// 场景常量
|
||||
const (
|
||||
// SceneSystem 系统消息/通知场景
|
||||
SceneSystem = "system"
|
||||
// SceneChat 私聊/群聊消息场景
|
||||
SceneChat = "chat"
|
||||
)
|
||||
|
||||
// MessageParams 厂商无关的统一消息参数(超集,用不上的字段厂商可忽略)
|
||||
type MessageParams struct {
|
||||
// Scene 决定使用哪类 channel/模板:SceneSystem 或 SceneChat
|
||||
Scene string
|
||||
// Title 通知标题(如发送者昵称、群名:发送者)
|
||||
Title string
|
||||
// Body 通知正文(消息内容预览)
|
||||
Body string
|
||||
// SenderName 发送者昵称
|
||||
SenderName string
|
||||
// SenderID 发送者 ID
|
||||
SenderID string
|
||||
// ConversationName 会话名称(群聊为群名)
|
||||
ConversationName string
|
||||
// ConversationID 会话 ID
|
||||
ConversationID string
|
||||
// MessageType 消息类型标识(chat_message / 系统通知类型),填充到 extras.notification_type
|
||||
MessageType string
|
||||
// ActionTime 操作时间(RFC3339)
|
||||
ActionTime string
|
||||
// LargeIcon 大图标 URL(群头像/发送者头像)
|
||||
LargeIcon string
|
||||
// Extra 兜底扩展字段,合并进 android/ios 的 extras
|
||||
Extra map[string]any
|
||||
}
|
||||
|
||||
// PushPayload 工厂统一产物,client 据此组装最终推送 payload
|
||||
type PushPayload struct {
|
||||
// Notification 含 android + ios,由 TextRenderer 产出
|
||||
Notification *jpush.Notification
|
||||
// TPC 各厂商私有字段(options.third_party_channel),由 PrivateConverter 产出
|
||||
TPC *jpush.ThirdPartyChannel
|
||||
}
|
||||
|
||||
// TextRenderer 通用文本渲染器(基础层)
|
||||
// 产出所有厂商共享的通用通知:Android 通用通知 + iOS 通用通知
|
||||
type TextRenderer interface {
|
||||
// RenderAndroid 渲染 Android 通用通知(所有 Android 厂商共享)
|
||||
RenderAndroid(params MessageParams, cfg config.ChannelConfig) *jpush.AndroidNotif
|
||||
// RenderIOS 渲染 iOS 通用通知(按场景设置 thread-id 分组)
|
||||
RenderIOS(params MessageParams, cfg config.ChannelConfig) *jpush.IOSNotif
|
||||
}
|
||||
|
||||
// PrivateConverter 厂商私有转换器(附加层)
|
||||
// 产出 third_party_channel.{vendor} 的私有字段(channel_id、模板等)
|
||||
type PrivateConverter interface {
|
||||
// VendorName 返回厂商标识,对应 third_party_channel 的 key(xiaomi/oppo/huawei/...)
|
||||
VendorName() string
|
||||
// Convert 根据消息参数 + 全局配置产出该厂商私有字段 map
|
||||
// 厂商专属 channel_id 为空时回退到全局 cfg.System/Chat
|
||||
// 返回 nil 表示该厂商无需下发(如最终 channel_id 与模板均未配置)
|
||||
Convert(params MessageParams, cfg config.ChannelConfig) map[string]any
|
||||
}
|
||||
|
||||
// Factory 持有通用渲染器与全部厂商私有转换器
|
||||
type Factory struct {
|
||||
renderer TextRenderer
|
||||
privateConverters []PrivateConverter
|
||||
}
|
||||
|
||||
// NewFactory 创建工厂,注册默认渲染器与全部厂商转换器
|
||||
func NewFactory() *Factory {
|
||||
return &Factory{
|
||||
renderer: newDefaultRenderer(),
|
||||
privateConverters: []PrivateConverter{
|
||||
xiaomiConverter{},
|
||||
huaweiConverter{},
|
||||
oppoConverter{},
|
||||
vivoConverter{},
|
||||
meizuConverter{},
|
||||
honorConverter{},
|
||||
fcmConverter{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewFactoryWith 自定义渲染器与转换器创建工厂(便于测试)
|
||||
func NewFactoryWith(renderer TextRenderer, converters []PrivateConverter) *Factory {
|
||||
return &Factory{renderer: renderer, privateConverters: converters}
|
||||
}
|
||||
|
||||
// Build 根据统一参数与配置,组装完整的 PushPayload
|
||||
// 调用通用渲染器产出 Notification,调用各私有转换器产出 ThirdPartyChannel
|
||||
func (f *Factory) Build(params MessageParams, cfg config.ChannelConfig) *PushPayload {
|
||||
android := f.renderer.RenderAndroid(params, cfg)
|
||||
ios := f.renderer.RenderIOS(params, cfg)
|
||||
|
||||
notification := &jpush.Notification{
|
||||
Alert: params.Body,
|
||||
Android: android,
|
||||
IOS: ios,
|
||||
}
|
||||
|
||||
tpc := &jpush.ThirdPartyChannel{}
|
||||
for _, c := range f.privateConverters {
|
||||
m := c.Convert(params, cfg)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
switch c.VendorName() {
|
||||
case "xiaomi":
|
||||
tpc.Xiaomi = m
|
||||
case "huawei":
|
||||
tpc.Huawei = m
|
||||
case "oppo":
|
||||
tpc.OPPO = m
|
||||
case "vivo":
|
||||
tpc.VIVO = m
|
||||
case "meizu":
|
||||
tpc.Meizu = m
|
||||
case "honor":
|
||||
tpc.Honor = m
|
||||
case "fcm":
|
||||
tpc.FCM = m
|
||||
}
|
||||
}
|
||||
|
||||
// 全部厂商字段为空则置 nil,避免序列化出空的 third_party_channel
|
||||
if tpc.Xiaomi == nil && tpc.Huawei == nil && tpc.OPPO == nil &&
|
||||
tpc.VIVO == nil && tpc.Meizu == nil && tpc.Honor == nil && tpc.FCM == nil {
|
||||
tpc = nil
|
||||
}
|
||||
|
||||
return &PushPayload{
|
||||
Notification: notification,
|
||||
TPC: tpc,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user