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 }