Compare commits

..

No commits in common. "fb885e138dcf50b995f41d3b14151026fcbe256c" and "f177377fe3dcb686a420b40ecb7b49dcd36d9c87" have entirely different histories.

5 changed files with 32 additions and 80 deletions

View File

@ -42,31 +42,17 @@ public class ActivityService(
if (debugInclude.Contains("articles") || Random.Shared.NextDouble() < 0.2) if (debugInclude.Contains("articles") || Random.Shared.NextDouble() < 0.2)
{ {
var recentFeedIds = await db.WebArticles var recentArticlesQuery = db.WebArticles
.GroupBy(a => a.FeedId) .Include(a => a.Feed)
.OrderByDescending(g => g.Max(a => a.PublishedAt)) .Take(20); // Get a larger pool for randomization
.Take(10) // Get recent 10 distinct feeds
.Select(g => g.Key)
.ToListAsync();
// For each feed, get one random article // Apply random ordering 50% of the time
var recentArticles = new List<WebArticle>(); if (Random.Shared.NextDouble() < 0.5)
var random = new Random(); recentArticlesQuery = recentArticlesQuery.OrderBy(_ => EF.Functions.Random());
else
foreach (var feedId in recentFeedIds.OrderBy(_ => random.Next())) recentArticlesQuery = recentArticlesQuery.OrderByDescending(a => a.PublishedAt);
{
var article = await db.WebArticles var recentArticles = await recentArticlesQuery.Take(5).ToListAsync();
.Include(a => a.Feed)
.Where(a => a.FeedId == feedId)
.OrderBy(_ => EF.Functions.Random())
.FirstOrDefaultAsync();
if (article != null)
{
recentArticles.Add(article);
if (recentArticles.Count >= 5) break; // Limit to 5 articles
}
}
if (recentArticles.Count > 0) if (recentArticles.Count > 0)
{ {

View File

@ -93,13 +93,17 @@ public class WebFeedService(
{ {
var itemUrl = item.Links.FirstOrDefault()?.Uri.ToString(); var itemUrl = item.Links.FirstOrDefault()?.Uri.ToString();
if (string.IsNullOrEmpty(itemUrl)) if (string.IsNullOrEmpty(itemUrl))
{
continue; continue;
}
var articleExists = await database.Set<WebArticle>() var articleExists = await database.Set<WebArticle>()
.AnyAsync(a => a.FeedId == feed.Id && a.Url == itemUrl, cancellationToken); .AnyAsync(a => a.FeedId == feed.Id && a.Url == itemUrl, cancellationToken);
if (articleExists) if (articleExists)
{
continue; continue;
}
var content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text; var content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text;
LinkEmbed preview; LinkEmbed preview;
@ -123,11 +127,11 @@ public class WebFeedService(
Url = itemUrl, Url = itemUrl,
Author = item.Authors.FirstOrDefault()?.Name, Author = item.Authors.FirstOrDefault()?.Name,
Content = content, Content = content,
PublishedAt = item.LastUpdatedTime.UtcDateTime, PublishedAt = item.PublishDate.UtcDateTime,
Preview = preview, Preview = preview,
}; };
database.WebArticles.Add(newArticle); database.Set<WebArticle>().Add(newArticle);
} }
await database.SaveChangesAsync(cancellationToken); await database.SaveChangesAsync(cancellationToken);

View File

@ -7,43 +7,16 @@ namespace DysonNetwork.Sphere.Wallet;
public record class SubscriptionTypeData( public record class SubscriptionTypeData(
string Identifier, string Identifier,
string? GroupIdentifier, decimal BasePrice
string Currency,
decimal BasePrice,
int? RequiredLevel = null
) )
{ {
public static readonly Dictionary<string, SubscriptionTypeData> SubscriptionDict = public static readonly Dictionary<string, SubscriptionTypeData> SubscriptionDict =
new() new()
{ {
[SubscriptionType.Twinkle] = new SubscriptionTypeData( [SubscriptionType.Twinkle] = new SubscriptionTypeData(SubscriptionType.Twinkle, 0),
SubscriptionType.Twinkle, [SubscriptionType.Stellar] = new SubscriptionTypeData(SubscriptionType.Stellar, 10),
SubscriptionType.StellarProgram, [SubscriptionType.Nova] = new SubscriptionTypeData(SubscriptionType.Nova, 20),
WalletCurrency.SourcePoint, [SubscriptionType.Supernova] = new SubscriptionTypeData(SubscriptionType.Supernova, 30)
0,
1
),
[SubscriptionType.Stellar] = new SubscriptionTypeData(
SubscriptionType.Stellar,
SubscriptionType.StellarProgram,
WalletCurrency.SourcePoint,
1200,
3
),
[SubscriptionType.Nova] = new SubscriptionTypeData(
SubscriptionType.Nova,
SubscriptionType.StellarProgram,
WalletCurrency.SourcePoint,
2400,
6
),
[SubscriptionType.Supernova] = new SubscriptionTypeData(
SubscriptionType.Supernova,
SubscriptionType.StellarProgram,
WalletCurrency.SourcePoint,
3600,
9
)
}; };
public static readonly Dictionary<string, string> SubscriptionHumanReadable = public static readonly Dictionary<string, string> SubscriptionHumanReadable =

View File

@ -32,22 +32,16 @@ public class SubscriptionService(
bool noop = false bool noop = false
) )
{ {
var subscriptionInfo = SubscriptionTypeData var subscriptionTemplate = SubscriptionTypeData
.SubscriptionDict.TryGetValue(identifier, out var template) .SubscriptionDict.TryGetValue(identifier, out var template)
? template ? template
: null; : null;
if (subscriptionInfo is null) if (subscriptionTemplate is null)
throw new ArgumentOutOfRangeException(nameof(identifier), $@"Subscription {identifier} was not found."); throw new ArgumentOutOfRangeException(nameof(identifier), $@"Subscription {identifier} was not found.");
var subscriptionsInGroup = subscriptionInfo.GroupIdentifier is not null
? SubscriptionTypeData.SubscriptionDict
.Where(s => s.Value.GroupIdentifier == subscriptionInfo.GroupIdentifier)
.Select(s => s.Value.Identifier)
.ToArray()
: [identifier];
cycleDuration ??= Duration.FromDays(30); cycleDuration ??= Duration.FromDays(30);
var existingSubscription = await GetSubscriptionAsync(account.Id, subscriptionsInGroup); var existingSubscription = await GetSubscriptionAsync(account.Id, identifier);
if (existingSubscription is not null && !noop) if (existingSubscription is not null && !noop)
throw new InvalidOperationException($"Active subscription with identifier {identifier} already exists."); throw new InvalidOperationException($"Active subscription with identifier {identifier} already exists.");
if (existingSubscription is not null) if (existingSubscription is not null)
@ -83,7 +77,7 @@ public class SubscriptionService(
Status = SubscriptionStatus.Unpaid, Status = SubscriptionStatus.Unpaid,
PaymentMethod = paymentMethod, PaymentMethod = paymentMethod,
PaymentDetails = paymentDetails, PaymentDetails = paymentDetails,
BasePrice = subscriptionInfo.BasePrice, BasePrice = subscriptionTemplate.BasePrice,
CouponId = couponData?.Id, CouponId = couponData?.Id,
Coupon = couponData, Coupon = couponData,
RenewalAt = (isFreeTrial || !isAutoRenewal) ? null : now.Plus(cycleDuration.Value), RenewalAt = (isFreeTrial || !isAutoRenewal) ? null : now.Plus(cycleDuration.Value),
@ -226,16 +220,10 @@ public class SubscriptionService(
.OrderByDescending(s => s.BegunAt) .OrderByDescending(s => s.BegunAt)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (subscription is null) throw new InvalidOperationException("No matching subscription found."); if (subscription is null) throw new InvalidOperationException("No matching subscription found.");
var subscriptionInfo = SubscriptionTypeData.SubscriptionDict
.TryGetValue(subscription.Identifier, out var template)
? template
: null;
if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found.");
return await payment.CreateOrderAsync( return await payment.CreateOrderAsync(
null, null,
subscriptionInfo.Currency, WalletCurrency.GoldenPoint,
subscription.FinalPrice, subscription.FinalPrice,
appIdentifier: SubscriptionOrderIdentifier, appIdentifier: SubscriptionOrderIdentifier,
meta: new Dictionary<string, object>() meta: new Dictionary<string, object>()
@ -331,7 +319,7 @@ public class SubscriptionService(
{ {
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == subscription.AccountId); var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == subscription.AccountId);
if (account is null) return; if (account is null) return;
AccountService.SetCultureInfo(account); AccountService.SetCultureInfo(account);
var humanReadableName = var humanReadableName =
@ -357,10 +345,10 @@ public class SubscriptionService(
private const string SubscriptionCacheKeyPrefix = "subscription:"; private const string SubscriptionCacheKeyPrefix = "subscription:";
public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, params string[] identifiers) public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, string identifier)
{ {
// Create a unique cache key for this subscription // Create a unique cache key for this subscription
var cacheKey = $"{SubscriptionCacheKeyPrefix}{accountId}:{string.Join(",", identifiers)}"; var cacheKey = $"{SubscriptionCacheKeyPrefix}{accountId}:{identifier}";
// Try to get the subscription from cache first // Try to get the subscription from cache first
var (found, cachedSubscription) = await cache.GetAsyncWithStatus<Subscription>(cacheKey); var (found, cachedSubscription) = await cache.GetAsyncWithStatus<Subscription>(cacheKey);
@ -371,13 +359,15 @@ public class SubscriptionService(
// If not in cache, get from database // If not in cache, get from database
var subscription = await db.WalletSubscriptions var subscription = await db.WalletSubscriptions
.Where(s => s.AccountId == accountId && identifiers.Contains(s.Identifier)) .Where(s => s.AccountId == accountId && s.Identifier == identifier)
.OrderByDescending(s => s.BegunAt) .OrderByDescending(s => s.BegunAt)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
// Cache the result if found (with 30 minutes expiry) // Cache the result if found (with 30 minutes expiry)
if (subscription != null) if (subscription != null)
{
await cache.SetAsync(cacheKey, subscription, TimeSpan.FromMinutes(30)); await cache.SetAsync(cacheKey, subscription, TimeSpan.FromMinutes(30));
}
return subscription; return subscription;
} }

View File

@ -83,7 +83,6 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fdf_003F3fcdc4d2_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fdf_003F3fcdc4d2_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F7c_003F8b7572ae_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASyndicationFeed_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b43b9cf654743f8b9a2eee23c625dd21dd30_003Fad_003Fd26b4d73_003FSyndicationFeed_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASyndicationFeed_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b43b9cf654743f8b9a2eee23c625dd21dd30_003Fad_003Fd26b4d73_003FSyndicationFeed_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASyndicationItem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b43b9cf654743f8b9a2eee23c625dd21dd30_003Fe1_003Fb136d7be_003FSyndicationItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATagging_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36f4c2e6baa65ba603de42eedad12ea36845aa35a910a6a82d82baf688e3e1_003FTagging_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F12_003Fe0a28ad6_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003F12_003Fe0a28ad6_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATotp_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F48c9d2a1b3c84b32b36ebc6f20a927ea4600_003F7b_003Ff98e5727_003FTotp_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATotp_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F48c9d2a1b3c84b32b36ebc6f20a927ea4600_003F7b_003Ff98e5727_003FTotp_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>