193 lines
4.7 KiB
Go
193 lines
4.7 KiB
Go
package promo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
promomodel "github.com/perfect-panel/server/internal/model/promo"
|
|
"github.com/perfect-panel/server/internal/types"
|
|
"github.com/perfect-panel/server/pkg/xerr"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
ruleCacheKey = "promo:rules:enabled"
|
|
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 {
|
|
if priority < 0 {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "priority must be greater than or equal to 0")
|
|
}
|
|
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")
|
|
}
|
|
switch ruleType {
|
|
case "new_user":
|
|
windowHours, ok := numberParam(params, "window_hours")
|
|
if !ok || windowHours <= 0 {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "params.window_hours must be greater than 0")
|
|
}
|
|
case "inactive_user":
|
|
inactiveMonths, ok := numberParam(params, "inactive_months")
|
|
if !ok || inactiveMonths <= 0 {
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "params.inactive_months must be greater than 0")
|
|
}
|
|
case "campaign":
|
|
if params == nil {
|
|
return nil
|
|
}
|
|
default:
|
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "unsupported promo rule type")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func numberParam(params map[string]interface{}, key string) (int64, bool) {
|
|
if params == nil {
|
|
return 0, false
|
|
}
|
|
value, ok := params[key]
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
switch v := value.(type) {
|
|
case float64:
|
|
if math.Trunc(v) != v {
|
|
return 0, false
|
|
}
|
|
return int64(v), true
|
|
case int64:
|
|
return v, true
|
|
case int:
|
|
return int64(v), true
|
|
case json.Number:
|
|
n, err := v.Int64()
|
|
return n, err == nil
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func paramsToString(params map[string]interface{}) (string, error) {
|
|
if params == nil {
|
|
params = map[string]interface{}{}
|
|
}
|
|
b, err := json.Marshal(params)
|
|
if err != nil {
|
|
return "", errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid params")
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
func parseParams(data string) map[string]interface{} {
|
|
if data == "" {
|
|
return map[string]interface{}{}
|
|
}
|
|
var params map[string]interface{}
|
|
if err := json.Unmarshal([]byte(data), ¶ms); err != nil {
|
|
return map[string]interface{}{}
|
|
}
|
|
return params
|
|
}
|
|
|
|
func normalizeRuleTimestamp(ts *int64) (*time.Time, error) {
|
|
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 t
|
|
}
|
|
|
|
func timePtrToUnixPtr(t *time.Time) *int64 {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
ts := t.Unix()
|
|
return &ts
|
|
}
|
|
|
|
func convertRule(item *promomodel.Rule) types.PromoRule {
|
|
if item == nil {
|
|
return types.PromoRule{}
|
|
}
|
|
return types.PromoRule{
|
|
Id: item.Id,
|
|
Name: item.Name,
|
|
Type: item.Type,
|
|
Params: parseParams(item.Params),
|
|
Priority: item.Priority,
|
|
Enabled: item.Enabled,
|
|
StartTime: timePtrToUnixPtr(item.StartTime),
|
|
EndTime: timePtrToUnixPtr(item.EndTime),
|
|
CreatedAt: item.CreatedAt.Unix(),
|
|
UpdatedAt: item.UpdatedAt.Unix(),
|
|
}
|
|
}
|
|
|
|
func convertPrice(item *promomodel.SubscribePromo) types.PromoPrice {
|
|
if item == nil {
|
|
return types.PromoPrice{}
|
|
}
|
|
return types.PromoPrice{
|
|
Id: item.Id,
|
|
SubscribeId: item.SubscribeId,
|
|
PromoRuleId: item.PromoRuleId,
|
|
Quantity: item.Quantity,
|
|
PromoPrice: item.PromoPrice,
|
|
CreatedAt: item.CreatedAt.Unix(),
|
|
UpdatedAt: item.UpdatedAt.Unix(),
|
|
}
|
|
}
|
|
|
|
func convertUsage(item *promomodel.Usage) types.PromoUsage {
|
|
if item == nil {
|
|
return types.PromoUsage{}
|
|
}
|
|
return types.PromoUsage{
|
|
Id: item.Id,
|
|
UserId: item.UserId,
|
|
PromoRuleId: item.PromoRuleId,
|
|
SubscribeId: item.SubscribeId,
|
|
OrderNo: item.OrderNo,
|
|
PromoPrice: item.PromoPrice,
|
|
CreatedAt: item.CreatedAt.Unix(),
|
|
}
|
|
}
|
|
|
|
func subscribePromoCacheKey(subscribeId int64) string {
|
|
return fmt.Sprintf("%s%d", subscribeCachePref, subscribeId)
|
|
}
|