feat(server): implement chat file TTL cleanup and enhance JPush vendor channel support
All checks were successful
Build Backend / build (push) Successful in 3m1s
Build Backend / build-docker (push) Successful in 2m10s

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:
lafay
2026-06-17 20:41:55 +08:00
parent d9aa4b46c3
commit a0e210feab
24 changed files with 1573 additions and 127 deletions

View File

@@ -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"`