增加首冲

This commit is contained in:
hy001 2026-05-28 15:07:59 +08:00
parent e738fbc44a
commit b53313f62d
12 changed files with 346 additions and 92 deletions

View File

@ -4,11 +4,12 @@ import "time"
// FirstRechargeRewardConfig stores first recharge reward config per system.
type FirstRechargeRewardConfig struct {
ID int64 `gorm:"column:id;primaryKey"`
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_first_recharge_reward_sys_origin"`
Enabled bool `gorm:"column:enabled;index:idx_first_recharge_reward_enabled"`
CreateTime time.Time `gorm:"column:create_time"`
UpdateTime time.Time `gorm:"column:update_time"`
ID int64 `gorm:"column:id;primaryKey"`
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_first_recharge_reward_sys_origin"`
Enabled bool `gorm:"column:enabled;index:idx_first_recharge_reward_enabled"`
MinAppVersion string `gorm:"column:min_app_version;size:32"`
CreateTime time.Time `gorm:"column:create_time"`
UpdateTime time.Time `gorm:"column:update_time"`
}
func (FirstRechargeRewardConfig) TableName() string { return "first_recharge_reward_config" }
@ -19,6 +20,7 @@ type FirstRechargeRewardLevel struct {
ConfigID int64 `gorm:"column:config_id;uniqueIndex:uk_first_recharge_reward_level,priority:1;uniqueIndex:uk_first_recharge_reward_amount,priority:1;index:idx_first_recharge_reward_level_config"`
Level int `gorm:"column:level;uniqueIndex:uk_first_recharge_reward_level,priority:2"`
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents;uniqueIndex:uk_first_recharge_reward_amount,priority:2"`
GoogleProductID string `gorm:"column:google_product_id;size:128;index:idx_first_recharge_reward_level_google_product"`
RewardGroupID int64 `gorm:"column:reward_group_id"`
RewardGroupName string `gorm:"column:reward_group_name;size:255"`
Enabled bool `gorm:"column:enabled"`
@ -46,6 +48,7 @@ type FirstRechargeRewardGrantRecord struct {
Level int `gorm:"column:level"`
PayPlatform string `gorm:"column:pay_platform;size:64"`
PaymentMethod string `gorm:"column:payment_method;size:32"`
GoogleProductID string `gorm:"column:google_product_id;size:128;index:idx_first_recharge_reward_grant_google_product"`
SourceOrderID string `gorm:"column:source_order_id;size:128;index:idx_first_recharge_reward_order"`
RewardGroupID int64 `gorm:"column:reward_group_id"`
RewardGroupName string `gorm:"column:reward_group_name;size:255"`

View File

@ -2,6 +2,7 @@ package router
import (
"net/http"
"strings"
"chatapp3-golang/internal/service/firstrechargereward"
@ -60,7 +61,7 @@ func registerFirstRechargeRewardRoutes(engine *gin.Engine, javaClient authGatewa
func registerFirstRechargeRewardAppGroup(group *gin.RouterGroup, javaClient authGateway, service *firstrechargereward.Service) {
group.Use(authMiddleware(javaClient))
group.GET("/home", func(c *gin.Context) {
resp, err := service.GetHome(c.Request.Context(), mustAuthUser(c))
resp, err := service.GetHome(c.Request.Context(), mustAuthUser(c), firstRechargeRewardRequestVersion(c))
if err != nil {
writeError(c, err)
return
@ -68,3 +69,33 @@ func registerFirstRechargeRewardAppGroup(group *gin.RouterGroup, javaClient auth
writeOK(c, resp)
})
}
func firstRechargeRewardRequestVersion(c *gin.Context) string {
return firstNonEmptyString(
c.Query("appVersion"),
c.Query("version"),
c.GetHeader("req-version"),
c.GetHeader("Req-Version"),
versionFromReqAppIntel(c.GetHeader("req-app-intel")),
versionFromReqAppIntel(c.GetHeader("Req-App-Intel")),
)
}
func versionFromReqAppIntel(value string) string {
for _, item := range strings.Split(value, ";") {
parts := strings.SplitN(item, "=", 2)
if len(parts) == 2 && strings.EqualFold(strings.TrimSpace(parts[0]), "version") {
return strings.TrimSpace(parts[1])
}
}
return ""
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if text := strings.TrimSpace(value); text != "" {
return text
}
}
return ""
}

View File

@ -37,12 +37,13 @@ func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*ConfigRespo
}
rewardItems := s.loadRewardItemsMap(ctx, levels)
return &ConfigResponse{
Configured: true,
ID: configRow.ID,
SysOrigin: configRow.SysOrigin,
Enabled: configRow.Enabled,
UpdateTime: formatDateTime(configRow.UpdateTime),
LevelConfigs: buildLevelPayloads(levels, rewardItems, 0, 0, true),
Configured: true,
ID: configRow.ID,
SysOrigin: configRow.SysOrigin,
Enabled: configRow.Enabled,
MinAppVersion: strings.TrimSpace(configRow.MinAppVersion),
UpdateTime: formatDateTime(configRow.UpdateTime),
LevelConfigs: buildLevelPayloads(levels, rewardItems, 0, 0, true),
}, nil
}
@ -56,6 +57,10 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*Confi
if req.Enabled && !hasEnabledLevel(levels) {
return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "enabled config must contain at least one enabled level")
}
minAppVersion := strings.TrimSpace(req.MinAppVersion)
if req.Enabled && minAppVersion == "" {
return nil, NewAppError(http.StatusBadRequest, "invalid_min_app_version", "minAppVersion is required when enabled")
}
var savedConfigID int64
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@ -91,6 +96,7 @@ func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*Confi
configRow.SysOrigin = sysOrigin
configRow.Enabled = req.Enabled
configRow.MinAppVersion = minAppVersion
configRow.UpdateTime = now
if configRow.CreateTime.IsZero() {
configRow.CreateTime = now
@ -126,6 +132,7 @@ func (s *Service) normalizeLevelInputs(inputs []LevelInput) ([]model.FirstRechar
}
seenLevels := make(map[int]struct{}, len(inputs))
seenAmounts := make(map[int64]struct{}, len(inputs))
seenGoogleProductIDs := make(map[string]struct{}, len(inputs))
rows := make([]model.FirstRechargeRewardLevel, 0, len(inputs))
for _, item := range inputs {
if item.Level <= 0 {
@ -148,6 +155,13 @@ func (s *Service) normalizeLevelInputs(inputs []LevelInput) ([]model.FirstRechar
if _, exists := seenAmounts[amountCents]; exists {
return nil, NewAppError(http.StatusBadRequest, "duplicate_recharge_amount", "levelConfigs must not contain duplicate rechargeAmount")
}
googleProductID := normalizeGoogleProductID(item.GoogleProductID)
if googleProductID != "" {
if _, exists := seenGoogleProductIDs[googleProductID]; exists {
return nil, NewAppError(http.StatusBadRequest, "duplicate_google_product_id", "levelConfigs must not contain duplicate googleProductId")
}
seenGoogleProductIDs[googleProductID] = struct{}{}
}
seenLevels[item.Level] = struct{}{}
seenAmounts[amountCents] = struct{}{}
id, err := utils.NextID()
@ -158,6 +172,7 @@ func (s *Service) normalizeLevelInputs(inputs []LevelInput) ([]model.FirstRechar
ID: id,
Level: item.Level,
RechargeAmountCents: amountCents,
GoogleProductID: googleProductID,
RewardGroupID: rewardGroupID,
RewardGroupName: strings.TrimSpace(item.RewardGroupName),
Enabled: item.Enabled,

View File

@ -41,9 +41,6 @@ func (s *Service) ProcessRechargeEvent(ctx context.Context, event RechargeEvent)
}
return nil, err
}
if !isFirstRechargeRewardAppVersionSupported(normalized.AppVersion) {
return &ProcessRechargeResponse{Processed: false, Reason: "app_version_not_supported"}, nil
}
if !isAcceptedPaymentMethod(normalized.PaymentMethod, normalized.PayPlatform) {
return &ProcessRechargeResponse{Processed: false, Reason: "unsupported_payment_method"}, nil
}
@ -61,9 +58,12 @@ func (s *Service) ProcessRechargeEvent(ctx context.Context, event RechargeEvent)
if !bundle.Config.Enabled {
return &ProcessRechargeResponse{Processed: false, Reason: "config_disabled"}, nil
}
matched, ok := pickMatchedLevel(bundle.Levels, normalized.AmountCents)
if !isFirstRechargeRewardAppVersionSupported(normalized.AppVersion, bundle.Config.MinAppVersion) {
return &ProcessRechargeResponse{Processed: false, Reason: "app_version_not_supported"}, nil
}
matched, ok := pickMatchedLevel(bundle.Levels, normalized.AmountCents, normalized.GoogleProductID)
if !ok {
return &ProcessRechargeResponse{Processed: false, Reason: "amount_not_reached"}, nil
return &ProcessRechargeResponse{Processed: false, Reason: "level_not_matched"}, nil
}
record, terminal, err := s.getOrCreateGrantRecord(ctx, normalized, bundle.Config, matched)
@ -112,13 +112,14 @@ func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRecharg
if amountCents <= 0 {
amountCents = firstPositiveAmountCents(event.Amount.String(), event.AmountUSD.String(), event.USDAmount.String())
}
if amountCents <= 0 {
return normalizedRechargeEvent{}, NewAppError(http.StatusBadRequest, "invalid_amount", "amountCents or amount is required")
}
payPlatform := strings.ToUpper(strings.TrimSpace(event.PayPlatform))
paymentMethod := strings.ToUpper(strings.TrimSpace(event.PaymentMethod))
appVersion := normalizeRechargeAppVersion(event)
googleProductID := normalizeRechargeGoogleProductID(event)
if amountCents <= 0 && googleProductID == "" {
return normalizedRechargeEvent{}, NewAppError(http.StatusBadRequest, "invalid_recharge_identity", "amountCents, amount, or googleProductId is required")
}
sourceOrderID := firstNonEmpty(event.SourceOrderID.String(), event.OrderID.String())
eventID := strings.TrimSpace(event.EventID)
if eventID == "" && sourceOrderID != "" {
@ -132,16 +133,17 @@ func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRecharg
occurredAt = time.Now()
}
return normalizedRechargeEvent{
EventID: eventID,
SysOrigin: sysOrigin,
UserID: userID,
AmountCents: amountCents,
Currency: firstNonEmpty(strings.ToUpper(strings.TrimSpace(event.Currency)), "USD"),
PayPlatform: payPlatform,
PaymentMethod: paymentMethod,
AppVersion: appVersion,
SourceOrderID: sourceOrderID,
OccurredAt: occurredAt,
EventID: eventID,
SysOrigin: sysOrigin,
UserID: userID,
AmountCents: amountCents,
Currency: firstNonEmpty(strings.ToUpper(strings.TrimSpace(event.Currency)), "USD"),
PayPlatform: payPlatform,
PaymentMethod: paymentMethod,
AppVersion: appVersion,
GoogleProductID: googleProductID,
SourceOrderID: sourceOrderID,
OccurredAt: occurredAt,
}, nil
}
@ -187,6 +189,7 @@ func (s *Service) getOrCreateGrantRecord(
Level: level.Level,
PayPlatform: event.PayPlatform,
PaymentMethod: event.PaymentMethod,
GoogleProductID: event.GoogleProductID,
SourceOrderID: event.SourceOrderID,
RewardGroupID: level.RewardGroupID,
RewardGroupName: level.RewardGroupName,
@ -290,6 +293,7 @@ func (s *Service) pushRewardNotice(ctx context.Context, record *model.FirstRecha
"rewardGroupName": record.RewardGroupName,
"payPlatform": record.PayPlatform,
"paymentMethod": record.PaymentMethod,
"googleProductId": record.GoogleProductID,
"noticeType": firstRechargeRewardNoticeType,
}
return s.java.SendOfficialNoticeCustomize(ctx, integration.OfficialNoticeCustomizeRequest{
@ -372,6 +376,18 @@ func normalizeRechargeAppVersion(event RechargeEvent) string {
)
}
func normalizeRechargeGoogleProductID(event RechargeEvent) string {
return normalizeGoogleProductID(firstNonEmpty(
event.GoogleProductID,
event.ProductID.String(),
rechargePayloadString(event.Payload, "googleProductId", "productId", "product_id"),
))
}
func normalizeGoogleProductID(value string) string {
return strings.TrimSpace(value)
}
func rechargePayloadString(payload json.RawMessage, keys ...string) string {
if len(payload) == 0 {
return ""

View File

@ -20,7 +20,7 @@ const (
)
// GetHome returns app-facing first recharge reward config and the user's grant state.
func (s *Service) GetHome(ctx context.Context, user AuthUser) (*HomeResponse, error) {
func (s *Service) GetHome(ctx context.Context, user AuthUser, appVersion string) (*HomeResponse, error) {
sysOrigin, err := requireUser(user)
if err != nil {
return nil, err
@ -52,12 +52,17 @@ func (s *Service) GetHome(ctx context.Context, user AuthUser) (*HomeResponse, er
ActivityStatus: activityStatusOngoing,
UserID: user.UserID,
SysOrigin: bundle.Config.SysOrigin,
MinAppVersion: bundle.Config.MinAppVersion,
LevelConfigs: buildLevelPayloads(bundle.Levels, rewardItems, 0, 0, false),
HasRewardRecord: false,
}
if !bundle.Config.Enabled {
resp.ActivityStatus = activityStatusDisabled
}
if bundle.Config.Enabled && !isFirstRechargeRewardAppVersionSupported(appVersion, bundle.Config.MinAppVersion) {
resp.Enabled = false
resp.ActivityStatus = activityStatusDisabled
}
record, ok, err := s.loadUserGrantRecord(ctx, bundle.Config.SysOrigin, user.UserID)
if err != nil {

View File

@ -12,10 +12,11 @@ import (
func configSnapshotFromModel(row model.FirstRechargeRewardConfig) configSnapshot {
return configSnapshot{
ID: row.ID,
SysOrigin: strings.ToUpper(strings.TrimSpace(row.SysOrigin)),
Enabled: row.Enabled,
UpdateTime: row.UpdateTime,
ID: row.ID,
SysOrigin: strings.ToUpper(strings.TrimSpace(row.SysOrigin)),
Enabled: row.Enabled,
MinAppVersion: strings.TrimSpace(row.MinAppVersion),
UpdateTime: row.UpdateTime,
}
}
@ -24,6 +25,7 @@ func levelSnapshotFromModel(row model.FirstRechargeRewardLevel) levelSnapshot {
ID: row.ID,
Level: row.Level,
RechargeAmountCents: row.RechargeAmountCents,
GoogleProductID: normalizeGoogleProductID(row.GoogleProductID),
RewardGroupID: row.RewardGroupID,
RewardGroupName: strings.TrimSpace(row.RewardGroupName),
Enabled: row.Enabled,
@ -51,6 +53,7 @@ func buildLevelPayloads(
Level: level.Level,
RechargeAmount: formatAmountCents(level.RechargeAmountCents),
RechargeAmountCents: level.RechargeAmountCents,
GoogleProductID: level.GoogleProductID,
RewardGroupID: level.RewardGroupID,
RewardGroupName: level.RewardGroupName,
RewardItems: rewardItems[level.RewardGroupID],
@ -62,19 +65,27 @@ func buildLevelPayloads(
return payloads
}
func pickMatchedLevel(levels []levelSnapshot, rechargeAmountCents int64) (levelSnapshot, bool) {
var matched levelSnapshot
ok := false
func pickMatchedLevel(levels []levelSnapshot, rechargeAmountCents int64, googleProductID string) (levelSnapshot, bool) {
googleProductID = normalizeGoogleProductID(googleProductID)
if googleProductID != "" {
for _, level := range levels {
if !level.Enabled || level.RewardGroupID <= 0 || level.GoogleProductID == "" {
continue
}
if level.GoogleProductID == googleProductID {
return level, true
}
}
}
for _, level := range levels {
if !level.Enabled || level.RechargeAmountCents <= 0 || level.RewardGroupID <= 0 {
continue
}
if rechargeAmountCents >= level.RechargeAmountCents && (!ok || level.RechargeAmountCents > matched.RechargeAmountCents) {
matched = level
ok = true
if rechargeAmountCents == level.RechargeAmountCents {
return level, true
}
}
return matched, ok
return levelSnapshot{}, false
}
func sortLevels(levels []levelSnapshot) {

View File

@ -92,6 +92,7 @@ func grantRecordViewFromModel(row model.FirstRechargeRewardGrantRecord, rewardIt
Level: row.Level,
PayPlatform: row.PayPlatform,
PaymentMethod: row.PaymentMethod,
GoogleProductID: row.GoogleProductID,
SourceOrderID: row.SourceOrderID,
RewardGroupID: row.RewardGroupID,
RewardGroupName: row.RewardGroupName,

View File

@ -20,8 +20,9 @@ func TestSaveConfigReturnsRewardItems(t *testing.T) {
req := mustDecodeSaveConfigRequest(t, `{
"sysOrigin": "LIKEI",
"enabled": true,
"minAppVersion": "1.5.0",
"levelConfigs": [
{"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
{"level": 1, "rechargeAmount": "9.99", "googleProductId": "gold_999", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
]
}`)
resp, err := service.SaveConfig(context.Background(), req)
@ -38,6 +39,9 @@ func TestSaveConfigReturnsRewardItems(t *testing.T) {
if level.RechargeAmountCents != 999 || level.RewardGroupID != 1001 {
t.Fatalf("level payload = %+v", level)
}
if level.GoogleProductID != "gold_999" {
t.Fatalf("google product id = %q", level.GoogleProductID)
}
if len(level.RewardItems) != 1 || level.RewardItems[0].Name != "首冲礼包奖励" {
t.Fatalf("reward items = %+v", level.RewardItems)
}
@ -66,6 +70,7 @@ func TestSaveConfigReturnsVipCoverFromSourceURL(t *testing.T) {
resp, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{
"sysOrigin": "LIKEI",
"enabled": true,
"minAppVersion": "1.5.0",
"levelConfigs": [
{"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "2001", "rewardGroupName": "VIP礼包", "enabled": true}
]
@ -115,6 +120,7 @@ func TestRewardItemsReturnConcretePropsType(t *testing.T) {
resp, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{
"sysOrigin": "LIKEI",
"enabled": true,
"minAppVersion": "1.5.0",
"levelConfigs": [
{"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "3001", "rewardGroupName": "道具礼包", "enabled": true}
]
@ -147,8 +153,9 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
_, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{
"sysOrigin": "LIKEI",
"enabled": true,
"minAppVersion": "1.5.0",
"levelConfigs": [
{"level": 1, "rechargeAmount": "5.00", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
{"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
]
}`))
if err != nil {
@ -185,7 +192,7 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
if err != nil {
t.Fatalf("low amount ProcessRechargeEvent() error = %v", err)
}
if lowAmount.Processed || lowAmount.Reason != "amount_not_reached" {
if lowAmount.Processed || lowAmount.Reason != "level_not_matched" {
t.Fatalf("low amount response = %+v", lowAmount)
}
@ -292,8 +299,9 @@ func TestProcessRechargeEventStartsCountingAtSupportedAppVersion(t *testing.T) {
_, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{
"sysOrigin": "LIKEI",
"enabled": true,
"minAppVersion": "1.5.0",
"levelConfigs": [
{"level": 1, "rechargeAmount": "5.00", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
{"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
]
}`))
if err != nil {
@ -380,13 +388,86 @@ func TestFirstRechargeRewardAppVersionSupported(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isFirstRechargeRewardAppVersionSupported(tt.appVersion); got != tt.want {
if got := isFirstRechargeRewardAppVersionSupported(tt.appVersion, "1.5.0"); got != tt.want {
t.Fatalf("isFirstRechargeRewardAppVersionSupported(%q) = %v, want %v", tt.appVersion, got, tt.want)
}
})
}
}
func TestHomeRequiresConfiguredMinAppVersion(t *testing.T) {
service, gateway, _ := newTestService(t)
gateway.groups[1001] = testRewardGroup(1001, "首冲礼包")
_, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{
"sysOrigin": "LIKEI",
"enabled": true,
"minAppVersion": "2.0.0",
"levelConfigs": [
{"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
]
}`))
if err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
oldVersion, err := service.GetHome(context.Background(), AuthUser{UserID: 123, SysOrigin: "LIKEI"}, "1.9.9")
if err != nil {
t.Fatalf("old version GetHome() error = %v", err)
}
if oldVersion.Enabled || oldVersion.ActivityStatus != activityStatusDisabled {
t.Fatalf("old version home = %+v", oldVersion)
}
supported, err := service.GetHome(context.Background(), AuthUser{UserID: 123, SysOrigin: "LIKEI"}, "2.0.0")
if err != nil {
t.Fatalf("supported GetHome() error = %v", err)
}
if !supported.Enabled || supported.ActivityStatus != activityStatusOngoing {
t.Fatalf("supported home = %+v", supported)
}
}
func TestProcessRechargeEventMatchesGoogleProductIDBeforeAmount(t *testing.T) {
service, gateway, _ := newTestService(t)
gateway.groups[1001] = testRewardGroup(1001, "首冲礼包")
gateway.groups[1002] = testRewardGroup(1002, "Google 首冲礼包")
_, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{
"sysOrigin": "LIKEI",
"enabled": true,
"minAppVersion": "1.5.0",
"levelConfigs": [
{"level": 1, "rechargeAmount": "9.99", "googleProductId": "gold_999", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true},
{"level": 2, "rechargeAmount": "19.99", "googleProductId": "first_recharge_google_1999", "rewardGroupId": "1002", "rewardGroupName": "Google 首冲礼包", "enabled": true}
]
}`))
if err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
granted, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
"eventId": "RECHARGE_SUCCESS:GOOGLE:PRODUCT_ID",
"sysOrigin": "LIKEI",
"userId": "123",
"amountCents": "999",
"payPlatform": "GOOGLE",
"paymentMethod": "GOOGLE",
"googleProductId": "first_recharge_google_1999",
"appVersion": "1.5.0",
"sourceOrderId": "PRODUCT_ID"
}`))
if err != nil {
t.Fatalf("product id ProcessRechargeEvent() error = %v", err)
}
if !granted.Processed || granted.Status != statusSuccess || granted.Level != 2 {
t.Fatalf("product id response = %+v", granted)
}
if len(gateway.rewardRequests) != 1 || gateway.rewardRequests[0].SourceGroupID != 1002 {
t.Fatalf("reward requests = %+v", gateway.rewardRequests)
}
}
func newTestService(t *testing.T) (*Service, *fakeGateway, *gorm.DB) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})

View File

@ -65,6 +65,7 @@ type LevelPayload struct {
Level int `json:"level"`
RechargeAmount string `json:"rechargeAmount"`
RechargeAmountCents int64 `json:"rechargeAmountCents"`
GoogleProductID string `json:"googleProductId,omitempty"`
RewardGroupID int64 `json:"rewardGroupId,string"`
RewardGroupName string `json:"rewardGroupName,omitempty"`
RewardItems []RewardItem `json:"rewardItems"`
@ -78,25 +79,28 @@ type LevelInput struct {
Level int `json:"level"`
RechargeAmount flexibleAmount `json:"rechargeAmount"`
RechargeAmountCents int64 `json:"rechargeAmountCents"`
GoogleProductID string `json:"googleProductId"`
RewardGroupID flexibleInt64 `json:"rewardGroupId"`
RewardGroupName string `json:"rewardGroupName"`
Enabled bool `json:"enabled"`
}
type ConfigResponse struct {
Configured bool `json:"configured"`
ID int64 `json:"id,string"`
SysOrigin string `json:"sysOrigin"`
Enabled bool `json:"enabled"`
UpdateTime string `json:"updateTime,omitempty"`
LevelConfigs []LevelPayload `json:"levelConfigs"`
Configured bool `json:"configured"`
ID int64 `json:"id,string"`
SysOrigin string `json:"sysOrigin"`
Enabled bool `json:"enabled"`
MinAppVersion string `json:"minAppVersion,omitempty"`
UpdateTime string `json:"updateTime,omitempty"`
LevelConfigs []LevelPayload `json:"levelConfigs"`
}
type SaveConfigRequest struct {
ID flexibleInt64 `json:"id"`
SysOrigin string `json:"sysOrigin"`
Enabled bool `json:"enabled"`
LevelConfigs []LevelInput `json:"levelConfigs"`
ID flexibleInt64 `json:"id"`
SysOrigin string `json:"sysOrigin"`
Enabled bool `json:"enabled"`
MinAppVersion string `json:"minAppVersion"`
LevelConfigs []LevelInput `json:"levelConfigs"`
}
type HomeResponse struct {
@ -108,6 +112,7 @@ type HomeResponse struct {
HasRewardRecord bool `json:"hasRewardRecord"`
RewardStatus string `json:"rewardStatus,omitempty"`
Rewarded bool `json:"rewarded"`
MinAppVersion string `json:"minAppVersion,omitempty"`
MatchedLevel int `json:"matchedLevel,omitempty"`
RechargeAmount string `json:"rechargeAmount,omitempty"`
RechargeAmountCents int64 `json:"rechargeAmountCents,omitempty"`
@ -141,6 +146,7 @@ type GrantRecordView struct {
Level int `json:"level"`
PayPlatform string `json:"payPlatform"`
PaymentMethod string `json:"paymentMethod"`
GoogleProductID string `json:"googleProductId,omitempty"`
SourceOrderID string `json:"sourceOrderId,omitempty"`
RewardGroupID int64 `json:"rewardGroupId,string"`
RewardGroupName string `json:"rewardGroupName,omitempty"`
@ -166,24 +172,26 @@ type RewardItem struct {
}
type RechargeEvent struct {
EventID string `json:"eventId"`
SysOrigin string `json:"sysOrigin"`
UserID flexibleInt64 `json:"userId"`
AmountCents flexibleInt64 `json:"amountCents"`
Amount flexibleString `json:"amount"`
AmountUSD flexibleString `json:"amountUsd"`
USDAmount flexibleString `json:"usdAmount"`
Currency string `json:"currency"`
PayPlatform string `json:"payPlatform"`
PaymentMethod string `json:"paymentMethod"`
AppVersion flexibleString `json:"appVersion"`
ClientVersion flexibleString `json:"clientVersion"`
Version flexibleString `json:"version"`
ReqVersion flexibleString `json:"reqVersion"`
SourceOrderID flexibleString `json:"sourceOrderId"`
OrderID flexibleString `json:"orderId"`
OccurredAt flexibleString `json:"occurredAt"`
Payload json.RawMessage `json:"payload,omitempty"`
EventID string `json:"eventId"`
SysOrigin string `json:"sysOrigin"`
UserID flexibleInt64 `json:"userId"`
AmountCents flexibleInt64 `json:"amountCents"`
Amount flexibleString `json:"amount"`
AmountUSD flexibleString `json:"amountUsd"`
USDAmount flexibleString `json:"usdAmount"`
Currency string `json:"currency"`
PayPlatform string `json:"payPlatform"`
PaymentMethod string `json:"paymentMethod"`
GoogleProductID string `json:"googleProductId"`
ProductID flexibleString `json:"productId"`
AppVersion flexibleString `json:"appVersion"`
ClientVersion flexibleString `json:"clientVersion"`
Version flexibleString `json:"version"`
ReqVersion flexibleString `json:"reqVersion"`
SourceOrderID flexibleString `json:"sourceOrderId"`
OrderID flexibleString `json:"orderId"`
OccurredAt flexibleString `json:"occurredAt"`
Payload json.RawMessage `json:"payload,omitempty"`
}
type ProcessRechargeResponse struct {
@ -200,32 +208,35 @@ type configBundle struct {
}
type configSnapshot struct {
ID int64
SysOrigin string
Enabled bool
UpdateTime time.Time
ID int64
SysOrigin string
Enabled bool
MinAppVersion string
UpdateTime time.Time
}
type levelSnapshot struct {
ID int64
Level int
RechargeAmountCents int64
GoogleProductID string
RewardGroupID int64
RewardGroupName string
Enabled bool
}
type normalizedRechargeEvent struct {
EventID string
SysOrigin string
UserID int64
AmountCents int64
Currency string
PayPlatform string
PaymentMethod string
AppVersion string
SourceOrderID string
OccurredAt time.Time
EventID string
SysOrigin string
UserID int64
AmountCents int64
Currency string
PayPlatform string
PaymentMethod string
AppVersion string
GoogleProductID string
SourceOrderID string
OccurredAt time.Time
}
type flexibleInt64 int64

View File

@ -5,14 +5,18 @@ import (
"strings"
)
const firstRechargeRewardMinAppVersion = "1.5.0"
const defaultFirstRechargeRewardMinAppVersion = "1.5.0"
func isFirstRechargeRewardAppVersionSupported(appVersion string) bool {
func isFirstRechargeRewardAppVersionSupported(appVersion string, minAppVersion string) bool {
appVersion = strings.TrimSpace(appVersion)
minAppVersion = strings.TrimSpace(minAppVersion)
if minAppVersion == "" {
minAppVersion = defaultFirstRechargeRewardMinAppVersion
}
if appVersion == "" {
return false
}
return compareAppVersion(appVersion, firstRechargeRewardMinAppVersion) >= 0
return compareAppVersion(appVersion, minAppVersion) >= 0
}
func compareAppVersion(left, right string) int {

View File

@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_config` (
`id` bigint NOT NULL COMMENT '主键ID',
`sys_origin` varchar(32) NOT NULL COMMENT '来源系统',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用',
`min_app_version` varchar(32) NOT NULL DEFAULT '1.5.0' COMMENT '最低生效App版本',
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
@ -14,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_level` (
`config_id` bigint NOT NULL COMMENT '首冲奖励主配置ID',
`level` int NOT NULL COMMENT '档位',
`recharge_amount_cents` bigint NOT NULL DEFAULT '0' COMMENT '充值金额门槛,单位为分',
`google_product_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Google商品ID',
`reward_group_id` bigint NOT NULL DEFAULT '0' COMMENT '奖励资源组ID',
`reward_group_name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖励资源组名称快照',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
@ -22,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_level` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_first_recharge_reward_level` (`config_id`, `level`),
UNIQUE KEY `uk_first_recharge_reward_amount` (`config_id`, `recharge_amount_cents`),
KEY `idx_first_recharge_reward_level_google_product` (`google_product_id`),
KEY `idx_first_recharge_reward_level_config` (`config_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='首冲奖励充值档位';
@ -42,6 +45,7 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_grant_record` (
`level` int NOT NULL COMMENT '命中档位',
`pay_platform` varchar(64) NOT NULL DEFAULT '' COMMENT '支付平台GOOGLE/MIFA_PAY/PAYER_MAX等',
`payment_method` varchar(32) NOT NULL DEFAULT '' COMMENT '充值方式GOOGLE/THIRD_PARTY',
`google_product_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Google商品ID',
`source_order_id` varchar(128) NOT NULL DEFAULT '' COMMENT '来源订单ID',
`reward_group_id` bigint NOT NULL DEFAULT '0' COMMENT '奖励资源组ID',
`reward_group_name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖励资源组名称快照',
@ -58,5 +62,6 @@ CREATE TABLE IF NOT EXISTS `first_recharge_reward_grant_record` (
UNIQUE KEY `uk_first_recharge_reward_event` (`event_id`),
UNIQUE KEY `uk_first_recharge_reward_user` (`sys_origin`, `user_id`),
KEY `idx_first_recharge_reward_status` (`sys_origin`, `user_id`, `status`),
KEY `idx_first_recharge_reward_grant_google_product` (`google_product_id`),
KEY `idx_first_recharge_reward_order` (`source_order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='首冲奖励发放记录';

View File

@ -0,0 +1,71 @@
SET @current_schema := DATABASE();
SET @add_first_recharge_reward_min_app_version_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'first_recharge_reward_config'
AND column_name = 'min_app_version'
),
'ALTER TABLE `first_recharge_reward_config` ADD COLUMN `min_app_version` varchar(32) NOT NULL DEFAULT ''1.5.0'' COMMENT ''最低生效App版本'' AFTER `enabled`',
'SELECT 1'
);
PREPARE add_first_recharge_reward_min_app_version_stmt FROM @add_first_recharge_reward_min_app_version_sql;
EXECUTE add_first_recharge_reward_min_app_version_stmt;
DEALLOCATE PREPARE add_first_recharge_reward_min_app_version_stmt;
SET @add_first_recharge_reward_level_google_product_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'first_recharge_reward_level'
AND column_name = 'google_product_id'
),
'ALTER TABLE `first_recharge_reward_level` ADD COLUMN `google_product_id` varchar(128) NOT NULL DEFAULT '''' COMMENT ''Google商品ID'' AFTER `recharge_amount_cents`',
'SELECT 1'
);
PREPARE add_first_recharge_reward_level_google_product_stmt FROM @add_first_recharge_reward_level_google_product_sql;
EXECUTE add_first_recharge_reward_level_google_product_stmt;
DEALLOCATE PREPARE add_first_recharge_reward_level_google_product_stmt;
SET @add_first_recharge_reward_record_google_product_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'first_recharge_reward_grant_record'
AND column_name = 'google_product_id'
),
'ALTER TABLE `first_recharge_reward_grant_record` ADD COLUMN `google_product_id` varchar(128) NOT NULL DEFAULT '''' COMMENT ''Google商品ID'' AFTER `payment_method`',
'SELECT 1'
);
PREPARE add_first_recharge_reward_record_google_product_stmt FROM @add_first_recharge_reward_record_google_product_sql;
EXECUTE add_first_recharge_reward_record_google_product_stmt;
DEALLOCATE PREPARE add_first_recharge_reward_record_google_product_stmt;
SET @add_first_recharge_reward_level_google_product_idx_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'first_recharge_reward_level'
AND index_name = 'idx_first_recharge_reward_level_google_product'
),
'ALTER TABLE `first_recharge_reward_level` ADD INDEX `idx_first_recharge_reward_level_google_product` (`google_product_id`)',
'SELECT 1'
);
PREPARE add_first_recharge_reward_level_google_product_idx_stmt FROM @add_first_recharge_reward_level_google_product_idx_sql;
EXECUTE add_first_recharge_reward_level_google_product_idx_stmt;
DEALLOCATE PREPARE add_first_recharge_reward_level_google_product_idx_stmt;
SET @add_first_recharge_reward_record_google_product_idx_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'first_recharge_reward_grant_record'
AND index_name = 'idx_first_recharge_reward_grant_google_product'
),
'ALTER TABLE `first_recharge_reward_grant_record` ADD INDEX `idx_first_recharge_reward_grant_google_product` (`google_product_id`)',
'SELECT 1'
);
PREPARE add_first_recharge_reward_record_google_product_idx_stmt FROM @add_first_recharge_reward_record_google_product_idx_sql;
EXECUTE add_first_recharge_reward_record_google_product_idx_stmt;
DEALLOCATE PREPARE add_first_recharge_reward_record_google_product_idx_stmt;