All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m36s
189 lines
5.9 KiB
Go
189 lines
5.9 KiB
Go
package common
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"time"
|
|
|
|
modelOrder "github.com/perfect-panel/server/internal/model/order"
|
|
modelUser "github.com/perfect-panel/server/internal/model/user"
|
|
"github.com/perfect-panel/server/pkg/xerr"
|
|
"github.com/pkg/errors"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
NewUserEligibilitySourceDeviceCreatedAt = "device_created_at"
|
|
NewUserEligibilitySourceUserCreatedAtFallback = "user_created_at_fallback"
|
|
NewUserEligibilityJoinSourceBindEmail = "bind_email_with_verification"
|
|
NewUserEligibilityWindow = 24 * time.Hour
|
|
)
|
|
|
|
type NewUserEligibilityContext struct {
|
|
ScopeUserIDs []int64
|
|
EligibilityStartAt time.Time
|
|
Source string
|
|
}
|
|
|
|
func (c *NewUserEligibilityContext) IsNewUserAt(now time.Time) bool {
|
|
if c == nil || c.EligibilityStartAt.IsZero() {
|
|
return false
|
|
}
|
|
return now.Sub(c.EligibilityStartAt) <= NewUserEligibilityWindow
|
|
}
|
|
|
|
type newUserEligibilityRelation struct {
|
|
FamilyID int64 `gorm:"column:family_id"`
|
|
Role uint8 `gorm:"column:role"`
|
|
JoinSource string `gorm:"column:join_source"`
|
|
OwnerUserID int64 `gorm:"column:owner_user_id"`
|
|
}
|
|
|
|
func ResolveNewUserEligibility(ctx context.Context, db *gorm.DB, currentUserID int64) (*NewUserEligibilityContext, error) {
|
|
if currentUserID <= 0 {
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "current user id is empty")
|
|
}
|
|
|
|
scopeUserIDs, err := resolveNewUserEligibilityScope(ctx, db, currentUserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
startAt, source, err := resolveNewUserEligibilityStartAt(ctx, db, scopeUserIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &NewUserEligibilityContext{
|
|
ScopeUserIDs: scopeUserIDs,
|
|
EligibilityStartAt: startAt,
|
|
Source: source,
|
|
}, nil
|
|
}
|
|
|
|
func CountScopedSubscribePurchaseOrders(
|
|
ctx context.Context,
|
|
db *gorm.DB,
|
|
scopeUserIDs []int64,
|
|
subscribeID int64,
|
|
statuses []int64,
|
|
excludeOrderNo string,
|
|
) (int64, error) {
|
|
if len(scopeUserIDs) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
var count int64
|
|
query := db.WithContext(ctx).
|
|
Model(&modelOrder.Order{}).
|
|
Where("user_id IN ? AND subscribe_id = ? AND type = 1", scopeUserIDs, subscribeID)
|
|
if len(statuses) > 0 {
|
|
query = query.Where("status IN ?", statuses)
|
|
}
|
|
if excludeOrderNo != "" {
|
|
query = query.Where("order_no != ?", excludeOrderNo)
|
|
}
|
|
if err := query.Count(&count).Error; err != nil {
|
|
return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count scoped subscribe purchase orders failed")
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func resolveNewUserEligibilityScope(ctx context.Context, db *gorm.DB, currentUserID int64) ([]int64, error) {
|
|
defaultScope := []int64{currentUserID}
|
|
|
|
var relation newUserEligibilityRelation
|
|
err := db.WithContext(ctx).
|
|
Table("user_family_member AS ufm").
|
|
Select("ufm.family_id, ufm.role, ufm.join_source, uf.owner_user_id").
|
|
Joins("JOIN user_family AS uf ON uf.id = ufm.family_id AND uf.deleted_at IS NULL AND uf.status = ?", modelUser.FamilyStatusActive).
|
|
Where("ufm.user_id = ? AND ufm.deleted_at IS NULL AND ufm.status = ?", currentUserID, modelUser.FamilyMemberActive).
|
|
Limit(1).
|
|
Take(&relation).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return defaultScope, nil
|
|
}
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query new user eligibility family relation failed")
|
|
}
|
|
|
|
if relation.Role != modelUser.FamilyRoleOwner && relation.JoinSource != NewUserEligibilityJoinSourceBindEmail {
|
|
return defaultScope, nil
|
|
}
|
|
|
|
var scopedUserIDs []int64
|
|
if err = db.WithContext(ctx).
|
|
Table("user_family_member AS ufm").
|
|
Select("ufm.user_id").
|
|
Joins("JOIN user_family AS uf ON uf.id = ufm.family_id AND uf.deleted_at IS NULL AND uf.status = ?", modelUser.FamilyStatusActive).
|
|
Where(
|
|
"ufm.family_id = ? AND ufm.deleted_at IS NULL AND ufm.status = ? AND (ufm.role = ? OR ufm.join_source = ?)",
|
|
relation.FamilyID,
|
|
modelUser.FamilyMemberActive,
|
|
modelUser.FamilyRoleOwner,
|
|
NewUserEligibilityJoinSourceBindEmail,
|
|
).
|
|
Pluck("ufm.user_id", &scopedUserIDs).Error; err != nil {
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query new user eligibility scope failed")
|
|
}
|
|
|
|
scopedUserIDs = append(scopedUserIDs, currentUserID)
|
|
return uniqueSortedInt64(scopedUserIDs), nil
|
|
}
|
|
|
|
func resolveNewUserEligibilityStartAt(ctx context.Context, db *gorm.DB, scopeUserIDs []int64) (time.Time, string, error) {
|
|
var earliestDevice modelUser.Device
|
|
err := db.WithContext(ctx).
|
|
Model(&modelUser.Device{}).
|
|
Where("user_id IN ?", scopeUserIDs).
|
|
Order("created_at ASC").
|
|
Limit(1).
|
|
Take(&earliestDevice).Error
|
|
switch {
|
|
case err == nil && !earliestDevice.CreatedAt.IsZero():
|
|
return earliestDevice.CreatedAt, NewUserEligibilitySourceDeviceCreatedAt, nil
|
|
case err != nil && !errors.Is(err, gorm.ErrRecordNotFound):
|
|
return time.Time{}, "", errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query new user eligibility device start failed")
|
|
}
|
|
|
|
var earliestUser modelUser.User
|
|
err = db.WithContext(ctx).
|
|
Model(&modelUser.User{}).
|
|
Where("id IN ?", scopeUserIDs).
|
|
Order("created_at ASC").
|
|
Limit(1).
|
|
Take(&earliestUser).Error
|
|
if err != nil {
|
|
return time.Time{}, "", errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query new user eligibility fallback start failed")
|
|
}
|
|
if earliestUser.CreatedAt.IsZero() {
|
|
return time.Time{}, "", errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "new user eligibility start time not found")
|
|
}
|
|
|
|
return earliestUser.CreatedAt, NewUserEligibilitySourceUserCreatedAtFallback, nil
|
|
}
|
|
|
|
func uniqueSortedInt64(values []int64) []int64 {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
|
|
seen := make(map[int64]struct{}, len(values))
|
|
result := make([]int64, 0, len(values))
|
|
for _, value := range values {
|
|
if value == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
result = append(result, value)
|
|
}
|
|
sort.Slice(result, func(i, j int) bool {
|
|
return result[i] < result[j]
|
|
})
|
|
return result
|
|
}
|