package app import ( "context" "encoding/json" "fmt" "strings" "time" "hyapp/pkg/appcode" "hyapp/pkg/walletmq" "hyapp/pkg/xerr" messageservice "hyapp/services/activity-service/internal/service/message" ) const vipDailyCoinRebateAvailableEventType = walletmq.EventTypeVIPDailyCoinRebateAvailable // vipRebateAvailableEvent 是 wallet outbox 的逐用户返现资格快照。 // activity-service 只把这个已提交事实投影为 inbox;资格、金额、过期和最终入账仍由 wallet owner 判定。 type vipRebateAvailableEvent struct { AppCode string EventID string RebateID string UserID int64 TaskDay string VIPLevel int32 VIPName string CoinAmount int64 AvailableAtMS int64 ExpiresAtMS int64 ConfigVersion int64 CreatedAtMS int64 } func vipRebateAvailableEventFromWalletMessage(body []byte) (vipRebateAvailableEvent, bool, error) { message, err := walletmq.DecodeWalletOutboxMessage(body) if err != nil { return vipRebateAvailableEvent{}, false, err } if message.EventType != vipDailyCoinRebateAvailableEventType { return vipRebateAvailableEvent{}, false, nil } var payload struct { RebateID string `json:"rebate_id"` UserID int64 `json:"user_id"` TaskDay string `json:"task_day"` VIPLevel int32 `json:"vip_level"` VIPName string `json:"vip_name"` CoinAmount int64 `json:"coin_amount"` AvailableAtMS int64 `json:"available_at_ms"` ExpiresAtMS int64 `json:"expires_at_ms"` ConfigVersion int64 `json:"config_version"` CreatedAtMS int64 `json:"created_at_ms"` } if err := json.Unmarshal([]byte(message.PayloadJSON), &payload); err != nil { // 目标事件 payload 损坏必须让 MQ 重投并进入告警,不能确认后永久丢失用户领取入口。 return vipRebateAvailableEvent{}, false, err } payload.RebateID = strings.TrimSpace(payload.RebateID) payload.TaskDay = strings.TrimSpace(payload.TaskDay) payload.VIPName = strings.TrimSpace(payload.VIPName) if payload.CreatedAtMS <= 0 { payload.CreatedAtMS = message.OccurredAtMS } if payload.RebateID == "" || payload.UserID <= 0 || payload.UserID != message.UserID || !validUTCTaskDay(payload.TaskDay) || payload.VIPLevel <= 0 || payload.CoinAmount <= 0 || payload.AvailableAtMS <= 0 || payload.ExpiresAtMS <= payload.AvailableAtMS || payload.ConfigVersion <= 0 { return vipRebateAvailableEvent{}, false, xerr.New(xerr.InvalidArgument, "VIP rebate wallet event is invalid") } return vipRebateAvailableEvent{ AppCode: appcode.Normalize(message.AppCode), EventID: message.EventID, RebateID: payload.RebateID, UserID: payload.UserID, TaskDay: payload.TaskDay, VIPLevel: payload.VIPLevel, VIPName: payload.VIPName, CoinAmount: payload.CoinAmount, AvailableAtMS: payload.AvailableAtMS, ExpiresAtMS: payload.ExpiresAtMS, ConfigVersion: payload.ConfigVersion, CreatedAtMS: payload.CreatedAtMS, }, true, nil } func createVIPRebateSystemNotice(ctx context.Context, service *messageservice.Service, event vipRebateAvailableEvent) error { if service == nil { return xerr.New(xerr.Unavailable, "message service is not configured") } actionParam, err := json.Marshal(map[string]any{ "rebate_id": event.RebateID, "task_day": event.TaskDay, "coin_amount": event.CoinAmount, "expires_at_ms": event.ExpiresAtMS, }) if err != nil { return err } summary := fmt.Sprintf("尊敬的VIP用户,平台今日返现%d金币,请及时领取。", event.CoinAmount) _, err = service.CreateSystemNotice(ctx, messageservice.NoticeCommand{ TargetUserID: event.UserID, Producer: "wallet-service", ProducerEventID: event.EventID, ProducerEventType: vipDailyCoinRebateAvailableEventType, AggregateType: "vip_daily_coin_rebate", AggregateID: event.RebateID, TemplateID: "vip_daily_coin_rebate_available", TemplateVersion: "v1", Title: "VIP金币返现", Summary: summary, Body: summary, ActionType: "vip_coin_rebate_claim", ActionParam: string(actionParam), Priority: 50, SentAtMS: event.AvailableAtMS, ExpireAtMS: event.ExpiresAtMS, Metadata: map[string]any{ "rebate_id": event.RebateID, "task_day": event.TaskDay, "vip_level": event.VIPLevel, "vip_name": event.VIPName, "coin_amount": event.CoinAmount, "available_at_ms": event.AvailableAtMS, "expires_at_ms": event.ExpiresAtMS, "config_version": event.ConfigVersion, "claim_status": "claimable", }, }) return err } func validUTCTaskDay(value string) bool { parsed, err := time.Parse("2006-01-02", value) return err == nil && parsed.Location() == time.UTC && parsed.Format("2006-01-02") == value }