修复(#128): 兼容促销规则毫秒时间戳
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -12,7 +12,11 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakePromoModel struct{}
|
type fakePromoModel struct {
|
||||||
|
insertRule func(context.Context, *promomodel.Rule) error
|
||||||
|
findRule func(context.Context, int64) (*promomodel.Rule, error)
|
||||||
|
updateRule func(context.Context, *promomodel.Rule) error
|
||||||
|
}
|
||||||
|
|
||||||
func (fakePromoModel) QueryEligibleRules(context.Context, int64, int64) ([]*promomodel.RuleWithPrice, error) {
|
func (fakePromoModel) QueryEligibleRules(context.Context, int64, int64) ([]*promomodel.RuleWithPrice, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -22,15 +26,24 @@ func (fakePromoModel) InsertUsage(context.Context, *promomodel.Usage, ...*gorm.D
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fakePromoModel) InsertRule(context.Context, *promomodel.Rule) error {
|
func (m fakePromoModel) InsertRule(ctx context.Context, rule *promomodel.Rule) error {
|
||||||
|
if m.insertRule != nil {
|
||||||
|
return m.insertRule(ctx, rule)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fakePromoModel) FindRule(context.Context, int64) (*promomodel.Rule, error) {
|
func (m fakePromoModel) FindRule(ctx context.Context, id int64) (*promomodel.Rule, error) {
|
||||||
|
if m.findRule != nil {
|
||||||
|
return m.findRule(ctx, id)
|
||||||
|
}
|
||||||
return nil, gorm.ErrRecordNotFound
|
return nil, gorm.ErrRecordNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fakePromoModel) UpdateRule(context.Context, *promomodel.Rule) error {
|
func (m fakePromoModel) UpdateRule(ctx context.Context, rule *promomodel.Rule) error {
|
||||||
|
if m.updateRule != nil {
|
||||||
|
return m.updateRule(ctx, rule)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package promo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
promomodel "github.com/perfect-panel/server/internal/model/promo"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
|
pkgerrors "github.com/pkg/errors"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateRuleAcceptsMillisecondTimestamps(t *testing.T) {
|
||||||
|
startTime := int64(1777618800000)
|
||||||
|
endTime := int64(1782802800000)
|
||||||
|
var inserted *promomodel.Rule
|
||||||
|
|
||||||
|
svcCtx := &svc.ServiceContext{
|
||||||
|
Redis: redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"}),
|
||||||
|
PromoModel: fakePromoModel{
|
||||||
|
insertRule: func(_ context.Context, rule *promomodel.Rule) error {
|
||||||
|
inserted = rule
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewCreateRuleLogic(context.Background(), svcCtx).CreateRule(&types.CreatePromoRuleRequest{
|
||||||
|
Name: "618活动",
|
||||||
|
Type: promomodel.RuleTypeInactiveUser,
|
||||||
|
Params: map[string]interface{}{"inactive_months": float64(1)},
|
||||||
|
Priority: 0,
|
||||||
|
StartTime: &startTime,
|
||||||
|
EndTime: &endTime,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateRule returned error: %v", err)
|
||||||
|
}
|
||||||
|
if inserted == nil {
|
||||||
|
t.Fatal("rule was not inserted")
|
||||||
|
}
|
||||||
|
assertPromoRuleTime(t, inserted.StartTime, time.UnixMilli(startTime))
|
||||||
|
assertPromoRuleTime(t, inserted.EndTime, time.UnixMilli(endTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRuleAcceptsMillisecondTimestamps(t *testing.T) {
|
||||||
|
startTime := int64(1777618800000)
|
||||||
|
endTime := int64(1782802800000)
|
||||||
|
existing := &promomodel.Rule{Id: 9, Enabled: true}
|
||||||
|
var updated *promomodel.Rule
|
||||||
|
|
||||||
|
svcCtx := &svc.ServiceContext{
|
||||||
|
Redis: redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"}),
|
||||||
|
PromoModel: fakePromoModel{
|
||||||
|
findRule: func(_ context.Context, id int64) (*promomodel.Rule, error) {
|
||||||
|
if id != existing.Id {
|
||||||
|
t.Fatalf("FindRule id = %d, want %d", id, existing.Id)
|
||||||
|
}
|
||||||
|
return existing, nil
|
||||||
|
},
|
||||||
|
updateRule: func(_ context.Context, rule *promomodel.Rule) error {
|
||||||
|
updated = rule
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewUpdateRuleLogic(context.Background(), svcCtx).UpdateRule(&types.UpdatePromoRuleRequest{
|
||||||
|
Id: existing.Id,
|
||||||
|
Name: "618活动",
|
||||||
|
Type: promomodel.RuleTypeInactiveUser,
|
||||||
|
Params: map[string]interface{}{"inactive_months": float64(1)},
|
||||||
|
Priority: 0,
|
||||||
|
StartTime: &startTime,
|
||||||
|
EndTime: &endTime,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateRule returned error: %v", err)
|
||||||
|
}
|
||||||
|
if updated == nil {
|
||||||
|
t.Fatal("rule was not updated")
|
||||||
|
}
|
||||||
|
assertPromoRuleTime(t, updated.StartTime, time.UnixMilli(startTime))
|
||||||
|
assertPromoRuleTime(t, updated.EndTime, time.UnixMilli(endTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuleRejectsOutOfRangeTimestamp(t *testing.T) {
|
||||||
|
startTime := int64(253402300800000)
|
||||||
|
endTime := int64(253402304400000)
|
||||||
|
svcCtx := &svc.ServiceContext{PromoModel: fakePromoModel{
|
||||||
|
insertRule: func(context.Context, *promomodel.Rule) error {
|
||||||
|
t.Fatal("InsertRule should not be called for invalid timestamp")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
_, err := NewCreateRuleLogic(context.Background(), svcCtx).CreateRule(&types.CreatePromoRuleRequest{
|
||||||
|
Name: "bad time",
|
||||||
|
Type: promomodel.RuleTypeInactiveUser,
|
||||||
|
Params: map[string]interface{}{"inactive_months": float64(1)},
|
||||||
|
Priority: 0,
|
||||||
|
StartTime: &startTime,
|
||||||
|
EndTime: &endTime,
|
||||||
|
})
|
||||||
|
assertInvalidParams(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertPromoRuleTime(t *testing.T, got *time.Time, want time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
if got == nil {
|
||||||
|
t.Fatalf("time is nil, want %v", want)
|
||||||
|
}
|
||||||
|
if !got.Equal(want) {
|
||||||
|
t.Fatalf("time = %v, want %v", *got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertInvalidParams(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
codeErr, ok := pkgerrors.Cause(err).(*xerr.CodeError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected CodeError, got %T", pkgerrors.Cause(err))
|
||||||
|
}
|
||||||
|
if got := codeErr.GetErrCode(); got != xerr.InvalidParams {
|
||||||
|
t.Fatalf("error code = %d, want %d", got, xerr.InvalidParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,11 +17,24 @@ const (
|
|||||||
subscribeCachePref = "promo:subscribe:"
|
subscribeCachePref = "promo:subscribe:"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
minPromoRuleTime = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
maxPromoRuleTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
|
||||||
|
)
|
||||||
|
|
||||||
func validateRuleInput(ruleType string, params map[string]interface{}, priority int64, startTime, endTime *int64) error {
|
func validateRuleInput(ruleType string, params map[string]interface{}, priority int64, startTime, endTime *int64) error {
|
||||||
if priority < 0 {
|
if priority < 0 {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "priority must be greater than or equal to 0")
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "priority must be greater than or equal to 0")
|
||||||
}
|
}
|
||||||
if startTime != nil && endTime != nil && *startTime >= *endTime {
|
startAt, err := normalizeRuleTimestamp(startTime)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid start_time")
|
||||||
|
}
|
||||||
|
endAt, err := normalizeRuleTimestamp(endTime)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid end_time")
|
||||||
|
}
|
||||||
|
if startAt != nil && endAt != nil && !startAt.Before(*endAt) {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "start_time must be less than end_time")
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "start_time must be less than end_time")
|
||||||
}
|
}
|
||||||
switch ruleType {
|
switch ruleType {
|
||||||
@@ -93,12 +106,29 @@ func parseParams(data string) map[string]interface{} {
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
func unixPtrToTimePtr(ts *int64) *time.Time {
|
func normalizeRuleTimestamp(ts *int64) (*time.Time, error) {
|
||||||
if ts == nil || *ts == 0 {
|
if ts == nil || *ts == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
value := *ts
|
||||||
|
var t time.Time
|
||||||
|
if value >= 1_000_000_000_000 || value <= -1_000_000_000_000 {
|
||||||
|
t = time.UnixMilli(value)
|
||||||
|
} else {
|
||||||
|
t = time.Unix(value, 0)
|
||||||
|
}
|
||||||
|
if t.Before(minPromoRuleTime) || t.After(maxPromoRuleTime) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "timestamp out of range")
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unixPtrToTimePtr(ts *int64) *time.Time {
|
||||||
|
t, err := normalizeRuleTimestamp(ts)
|
||||||
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
t := time.Unix(*ts, 0)
|
return t
|
||||||
return &t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func timePtrToUnixPtr(t *time.Time) *int64 {
|
func timePtrToUnixPtr(t *time.Time) *int64 {
|
||||||
|
|||||||
Reference in New Issue
Block a user