134 lines
4.7 KiB
Go
134 lines
4.7 KiB
Go
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 = "VipDailyCoinRebateAvailable"
|
||
|
||
// 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
|
||
}
|