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:
@@ -66,6 +66,18 @@ type Notification struct {
|
||||
IOS *IOSNotif `json:"ios,omitempty"`
|
||||
}
|
||||
|
||||
// ThirdPartyChannel 厂商通道配置,置于 push payload 的 options.third_party_channel
|
||||
// 用于向各厂商(小米/华为/OPPO/VIVO/魅族/FCM/荣耀)下发 channel_id 等厂商私有参数
|
||||
type ThirdPartyChannel struct {
|
||||
Xiaomi map[string]any `json:"xiaomi,omitempty"`
|
||||
Huawei map[string]any `json:"huawei,omitempty"`
|
||||
OPPO map[string]any `json:"oppo,omitempty"`
|
||||
VIVO map[string]any `json:"vivo,omitempty"`
|
||||
Meizu map[string]any `json:"meizu,omitempty"`
|
||||
FCM map[string]any `json:"fcm,omitempty"`
|
||||
Honor map[string]any `json:"honor,omitempty"`
|
||||
}
|
||||
|
||||
type AndroidNotif struct {
|
||||
Alert string `json:"alert"`
|
||||
Title string `json:"title,omitempty"`
|
||||
@@ -244,7 +256,7 @@ func (c *Client) Push(payload map[string]any) (*PushResponse, *RateLimitInfo, er
|
||||
return result, rateLimit, parseErr
|
||||
}
|
||||
|
||||
func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *Notification) (*PushResponse, error) {
|
||||
func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *Notification, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
if len(registrationIDs) == 0 {
|
||||
return nil, fmt.Errorf("registration IDs cannot be empty")
|
||||
}
|
||||
@@ -262,10 +274,7 @@ func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *N
|
||||
"platform": "all",
|
||||
"audience": map[string]any{"registration_id": registrationIDs},
|
||||
"notification": notification,
|
||||
"options": map[string]any{
|
||||
"time_to_live": 86400,
|
||||
"apns_production": c.production,
|
||||
},
|
||||
"options": c.buildOptions(tpc),
|
||||
}
|
||||
|
||||
var result *PushResponse
|
||||
@@ -292,7 +301,7 @@ func (c *Client) PushByRegistrationIDs(registrationIDs []string, notification *N
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) PushByAliases(aliases []string, notification *Notification) (*PushResponse, error) {
|
||||
func (c *Client) PushByAliases(aliases []string, notification *Notification, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
if len(aliases) == 0 {
|
||||
return nil, fmt.Errorf("aliases cannot be empty")
|
||||
}
|
||||
@@ -310,10 +319,7 @@ func (c *Client) PushByAliases(aliases []string, notification *Notification) (*P
|
||||
"platform": "all",
|
||||
"audience": map[string]any{"alias": aliases},
|
||||
"notification": notification,
|
||||
"options": map[string]any{
|
||||
"time_to_live": 86400,
|
||||
"apns_production": c.production,
|
||||
},
|
||||
"options": c.buildOptions(tpc),
|
||||
}
|
||||
|
||||
result, _, err := c.Push(payload)
|
||||
@@ -332,7 +338,7 @@ func (c *Client) PushByAliases(aliases []string, notification *Notification) (*P
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) PushAll(notification *Notification) (*PushResponse, error) {
|
||||
func (c *Client) PushAll(notification *Notification, tpc *ThirdPartyChannel) (*PushResponse, error) {
|
||||
c.logger.Info("jpush push all",
|
||||
zap.String("title", notification.Android.Title),
|
||||
zap.String("alert", notification.Alert),
|
||||
@@ -342,10 +348,7 @@ func (c *Client) PushAll(notification *Notification) (*PushResponse, error) {
|
||||
"platform": "all",
|
||||
"audience": "all",
|
||||
"notification": notification,
|
||||
"options": map[string]any{
|
||||
"time_to_live": 86400,
|
||||
"apns_production": c.production,
|
||||
},
|
||||
"options": c.buildOptions(tpc),
|
||||
}
|
||||
|
||||
result, _, err := c.Push(payload)
|
||||
@@ -362,41 +365,24 @@ func (c *Client) PushAll(notification *Notification) (*PushResponse, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func BuildNotification(title, body string, notificationType string, extras map[string]any) *Notification {
|
||||
androidExtras := map[string]any{
|
||||
"notification_type": notificationType,
|
||||
// buildOptions 构造 push payload 的 options 部分
|
||||
// 当 tpc 非空时加入 third_party_channel 字段,用于向各厂商下发 channel_id
|
||||
func (c *Client) buildOptions(tpc *ThirdPartyChannel) map[string]any {
|
||||
opts := map[string]any{
|
||||
"time_to_live": 86400,
|
||||
"apns_production": c.production,
|
||||
}
|
||||
for k, v := range extras {
|
||||
androidExtras[k] = v
|
||||
}
|
||||
|
||||
iosExtras := map[string]any{
|
||||
"notification_type": notificationType,
|
||||
}
|
||||
for k, v := range extras {
|
||||
iosExtras[k] = v
|
||||
}
|
||||
|
||||
return &Notification{
|
||||
Alert: body,
|
||||
Android: &AndroidNotif{
|
||||
Alert: body,
|
||||
Title: title,
|
||||
Extras: androidExtras,
|
||||
Priority: 1,
|
||||
},
|
||||
IOS: &IOSNotif{
|
||||
Alert: map[string]any{
|
||||
"title": title,
|
||||
"body": body,
|
||||
},
|
||||
Sound: "default",
|
||||
Badge: "+1",
|
||||
Extras: iosExtras,
|
||||
},
|
||||
if tpc != nil {
|
||||
opts["third_party_channel"] = tpc
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// 通知与厂商通道的构建逻辑已迁移至 internal/pkg/jpush/vendor 子包:
|
||||
// - 通用通知(Android/iOS)由 vendor.TextRenderer 产出
|
||||
// - 各厂商私有字段(channel_id / 小米 mi_template / OPPO 私信模板)由 vendor.PrivateConverter 产出
|
||||
// - 组装入口为 vendor.Factory.Build,产物 *vendor.PushPayload 含 Notification 与 ThirdPartyChannel
|
||||
|
||||
type DeviceInfo struct {
|
||||
Tags []string `json:"tags"`
|
||||
Alias string `json:"alias"`
|
||||
|
||||
Reference in New Issue
Block a user