266 lines
8.7 KiB
Go
266 lines
8.7 KiB
Go
package message
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"hyapp/pkg/xerr"
|
|
messagedomain "hyapp/services/activity-service/internal/domain/message"
|
|
)
|
|
|
|
const defaultNoticeProducer = "activity-service"
|
|
|
|
// NoticeCommand is the common producer-facing command for one user-visible system/activity message.
|
|
type NoticeCommand struct {
|
|
TargetUserID int64
|
|
Producer string
|
|
ProducerEventID string
|
|
ProducerEventType string
|
|
AggregateType string
|
|
AggregateID string
|
|
TemplateID string
|
|
TemplateVersion string
|
|
Title string
|
|
Summary string
|
|
Body string
|
|
IconURL string
|
|
ImageURL string
|
|
ActionType string
|
|
ActionParam string
|
|
Priority int32
|
|
SentAtMS int64
|
|
ExpireAtMS int64
|
|
Metadata map[string]any
|
|
MetadataJSON string
|
|
}
|
|
|
|
// NoticeResult keeps the inbox row and the idempotent create flag together.
|
|
type NoticeResult struct {
|
|
Message messagedomain.InboxMessage
|
|
Created bool
|
|
}
|
|
|
|
// NoticeFanoutCommand is the common command for background system/activity broadcasts.
|
|
type NoticeFanoutCommand struct {
|
|
CommandID string
|
|
MessageType string
|
|
TargetScope string
|
|
TargetUserID int64
|
|
UserIDs []int64
|
|
RegionID int64
|
|
Country string
|
|
ProducerEventType string
|
|
AggregateType string
|
|
AggregateID string
|
|
TemplateID string
|
|
TemplateVersion string
|
|
Title string
|
|
Summary string
|
|
Body string
|
|
IconURL string
|
|
ImageURL string
|
|
ActionType string
|
|
ActionParam string
|
|
Priority int32
|
|
SentAtMS int64
|
|
ExpireAtMS int64
|
|
Metadata map[string]any
|
|
MetadataJSON string
|
|
BatchSize int32
|
|
CreatedBy string
|
|
}
|
|
|
|
// CreateSystemNotice is the small public helper for account, wallet, room and application messages.
|
|
func (s *Service) CreateSystemNotice(ctx context.Context, cmd NoticeCommand) (NoticeResult, error) {
|
|
return s.CreateNotice(ctx, messagedomain.SectionSystem, cmd)
|
|
}
|
|
|
|
// CreateActivityNotice is the small public helper for activity, reward and campaign messages.
|
|
func (s *Service) CreateActivityNotice(ctx context.Context, cmd NoticeCommand) (NoticeResult, error) {
|
|
return s.CreateNotice(ctx, messagedomain.SectionActivity, cmd)
|
|
}
|
|
|
|
// CreateNotice normalizes producer defaults and metadata before delegating to the inbox owner.
|
|
func (s *Service) CreateNotice(ctx context.Context, messageType string, cmd NoticeCommand) (NoticeResult, error) {
|
|
metadataJSON, err := noticeMetadataJSON(cmd.Metadata, cmd.MetadataJSON)
|
|
if err != nil {
|
|
return NoticeResult{}, err
|
|
}
|
|
// The existing CreateInboxMessage method is still the only place that writes user_inbox_messages.
|
|
// This helper only narrows the public call shape so producers use the same idempotency and content rules.
|
|
message, created, err := s.CreateInboxMessage(ctx, messagedomain.CreateInput{
|
|
TargetUserID: cmd.TargetUserID,
|
|
Producer: firstNoticeText(cmd.Producer, defaultNoticeProducer),
|
|
ProducerEventID: cmd.ProducerEventID,
|
|
ProducerEventType: cmd.ProducerEventType,
|
|
MessageType: messageType,
|
|
AggregateType: cmd.AggregateType,
|
|
AggregateID: cmd.AggregateID,
|
|
TemplateID: cmd.TemplateID,
|
|
TemplateVersion: cmd.TemplateVersion,
|
|
Title: cmd.Title,
|
|
Summary: cmd.Summary,
|
|
Body: cmd.Body,
|
|
IconURL: cmd.IconURL,
|
|
ImageURL: cmd.ImageURL,
|
|
ActionType: cmd.ActionType,
|
|
ActionParam: cmd.ActionParam,
|
|
Priority: cmd.Priority,
|
|
SentAtMS: cmd.SentAtMS,
|
|
ExpireAtMS: cmd.ExpireAtMS,
|
|
MetadataJSON: metadataJSON,
|
|
})
|
|
if err != nil {
|
|
return NoticeResult{}, err
|
|
}
|
|
return NoticeResult{Message: message, Created: created}, nil
|
|
}
|
|
|
|
// CreateNoticeFanout records a broadcast command for the fanout worker instead of looping in the caller.
|
|
func (s *Service) CreateNoticeFanout(ctx context.Context, cmd NoticeFanoutCommand) (messagedomain.FanoutJob, bool, error) {
|
|
messageType := normalizeSection(cmd.MessageType)
|
|
if messageType == "" {
|
|
messageType = messagedomain.SectionSystem
|
|
}
|
|
targetFilterJSON, err := noticeTargetFilterJSON(cmd)
|
|
if err != nil {
|
|
return messagedomain.FanoutJob{}, false, err
|
|
}
|
|
templateSnapshotJSON, err := noticeTemplateSnapshotJSON(cmd)
|
|
if err != nil {
|
|
return messagedomain.FanoutJob{}, false, err
|
|
}
|
|
// command_id is the broadcast idempotency key. Retrying the same admin/activity command returns the
|
|
// existing job; changing payload under the same command_id is rejected by CreateFanoutJob.
|
|
return s.CreateFanoutJob(ctx, messagedomain.CreateFanoutInput{
|
|
CommandID: strings.TrimSpace(cmd.CommandID),
|
|
MessageType: messageType,
|
|
TargetScope: strings.TrimSpace(cmd.TargetScope),
|
|
TargetFilterJSON: targetFilterJSON,
|
|
TemplateSnapshotJSON: templateSnapshotJSON,
|
|
BatchSize: cmd.BatchSize,
|
|
CreatedBy: strings.TrimSpace(cmd.CreatedBy),
|
|
})
|
|
}
|
|
|
|
func noticeTargetFilterJSON(cmd NoticeFanoutCommand) (string, error) {
|
|
scope := strings.TrimSpace(cmd.TargetScope)
|
|
var payload map[string]any
|
|
switch scope {
|
|
case messagedomain.TargetScopeSingleUser:
|
|
if cmd.TargetUserID <= 0 {
|
|
return "", xerr.New(xerr.InvalidArgument, "target_user_id is required")
|
|
}
|
|
payload = map[string]any{"target_user_id": cmd.TargetUserID}
|
|
case messagedomain.TargetScopeUserIDs:
|
|
userIDs := positiveUniqueInt64s(cmd.UserIDs)
|
|
if len(userIDs) == 0 {
|
|
return "", xerr.New(xerr.InvalidArgument, "user_ids is required")
|
|
}
|
|
payload = map[string]any{"user_ids": userIDs}
|
|
case messagedomain.TargetScopeRegion:
|
|
if cmd.RegionID <= 0 {
|
|
return "", xerr.New(xerr.InvalidArgument, "region_id is required")
|
|
}
|
|
payload = map[string]any{"region_id": cmd.RegionID}
|
|
case messagedomain.TargetScopeCountry:
|
|
country := strings.TrimSpace(cmd.Country)
|
|
if country == "" {
|
|
return "", xerr.New(xerr.InvalidArgument, "country is required")
|
|
}
|
|
payload = map[string]any{"country": country}
|
|
case messagedomain.TargetScopeAllActiveUsers, messagedomain.TargetScopeAllRegistered:
|
|
payload = map[string]any{}
|
|
default:
|
|
return "", xerr.New(xerr.InvalidArgument, "target_scope is invalid")
|
|
}
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(encoded), nil
|
|
}
|
|
|
|
func noticeTemplateSnapshotJSON(cmd NoticeFanoutCommand) (string, error) {
|
|
metadataJSON, err := noticeMetadataJSON(cmd.Metadata, cmd.MetadataJSON)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
payload := map[string]any{
|
|
"producer_event_type": strings.TrimSpace(cmd.ProducerEventType),
|
|
"aggregate_type": strings.TrimSpace(cmd.AggregateType),
|
|
"aggregate_id": strings.TrimSpace(cmd.AggregateID),
|
|
"template_id": strings.TrimSpace(cmd.TemplateID),
|
|
"template_version": strings.TrimSpace(cmd.TemplateVersion),
|
|
"title": strings.TrimSpace(cmd.Title),
|
|
"summary": strings.TrimSpace(cmd.Summary),
|
|
"body": strings.TrimSpace(cmd.Body),
|
|
"icon_url": strings.TrimSpace(cmd.IconURL),
|
|
"image_url": strings.TrimSpace(cmd.ImageURL),
|
|
"action_type": strings.TrimSpace(cmd.ActionType),
|
|
"action_param": strings.TrimSpace(cmd.ActionParam),
|
|
"priority": cmd.Priority,
|
|
"sent_at_ms": cmd.SentAtMS,
|
|
"expire_at_ms": cmd.ExpireAtMS,
|
|
}
|
|
if metadataJSON != "" {
|
|
var parsed any
|
|
if err := json.Unmarshal([]byte(metadataJSON), &parsed); err != nil {
|
|
return "", xerr.New(xerr.InvalidArgument, "metadata_json is invalid")
|
|
}
|
|
payload["metadata_json"] = parsed
|
|
}
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(encoded), nil
|
|
}
|
|
|
|
func noticeMetadataJSON(metadata map[string]any, metadataJSON string) (string, error) {
|
|
metadataJSON = strings.TrimSpace(metadataJSON)
|
|
if len(metadata) > 0 && metadataJSON != "" {
|
|
return "", xerr.New(xerr.InvalidArgument, "metadata and metadata_json cannot both be set")
|
|
}
|
|
if metadataJSON != "" {
|
|
if !json.Valid([]byte(metadataJSON)) {
|
|
return "", xerr.New(xerr.InvalidArgument, "metadata_json is invalid")
|
|
}
|
|
return metadataJSON, nil
|
|
}
|
|
if len(metadata) == 0 {
|
|
return "", nil
|
|
}
|
|
encoded, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(encoded), nil
|
|
}
|
|
|
|
func positiveUniqueInt64s(values []int64) []int64 {
|
|
seen := make(map[int64]struct{}, len(values))
|
|
out := make([]int64, 0, len(values))
|
|
for _, value := range values {
|
|
if value <= 0 {
|
|
continue
|
|
}
|
|
if _, exists := seen[value]; exists {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
out = append(out, value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func firstNoticeText(values ...string) string {
|
|
for _, value := range values {
|
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|