diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.Designer.cs
new file mode 100644
index 0000000..a23014a
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.Designer.cs
@@ -0,0 +1,668 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Snap.Hutao.Model.Entity.Database;
+
+#nullable disable
+
+namespace Snap.Hutao.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260503131124_AddCultivateEntryRelatedEntryId")]
+ partial class AddCultivateEntryRelatedEntryId
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("ArchiveId")
+ .HasColumnType("TEXT");
+
+ b.Property("Current")
+ .HasColumnType("INTEGER");
+
+ b.Property("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("Time")
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.HasIndex("ArchiveId");
+
+ b.ToTable("achievements");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("IsSelected")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("achievement_archives");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Info2")
+ .HasColumnType("TEXT");
+
+ b.Property("RefreshTime")
+ .HasColumnType("TEXT");
+
+ b.Property("Uid")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("avatar_infos");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarStrategy", b =>
+ {
+ b.Property("AvatarId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChineseStrategyId")
+ .HasColumnType("INTEGER");
+
+ b.Property("OverseaStrategyId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("AvatarId");
+
+ b.ToTable("avatar_strategies");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property("ProjectId")
+ .HasColumnType("TEXT");
+
+ b.Property("RelatedEntryId")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("InnerId");
+
+ b.HasIndex("ProjectId");
+
+ b.HasIndex("RelatedEntryId");
+
+ b.ToTable("cultivate_entries");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AvatarIsPromoting")
+ .HasColumnType("INTEGER");
+
+ b.Property("AvatarLevelFrom")
+ .HasColumnType("INTEGER");
+
+ b.Property("AvatarLevelTo")
+ .HasColumnType("INTEGER");
+
+ b.Property("EntryId")
+ .HasColumnType("TEXT");
+
+ b.Property("SkillALevelFrom")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkillALevelTo")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkillELevelFrom")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkillELevelTo")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkillQLevelFrom")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkillQLevelTo")
+ .HasColumnType("INTEGER");
+
+ b.Property("WeaponIsPromoting")
+ .HasColumnType("INTEGER");
+
+ b.Property("WeaponLevelFrom")
+ .HasColumnType("INTEGER");
+
+ b.Property("WeaponLevelTo")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("InnerId");
+
+ b.HasIndex("EntryId")
+ .IsUnique();
+
+ b.ToTable("cultivate_entry_level_informations");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("EntryId")
+ .HasColumnType("TEXT");
+
+ b.Property("IsFinished")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("InnerId");
+
+ b.HasIndex("EntryId");
+
+ b.ToTable("cultivate_items");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("IsSelected")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ServerTimeZoneOffset")
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("cultivate_projects");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("DailyNote")
+ .HasColumnType("TEXT");
+
+ b.Property("DailyTaskDotVisible")
+ .HasColumnType("INTEGER");
+
+ b.Property("DailyTaskNotify")
+ .HasColumnType("INTEGER");
+
+ b.Property("DailyTaskNotifySuppressed")
+ .HasColumnType("INTEGER");
+
+ b.Property("ExpeditionDotVisible")
+ .HasColumnType("INTEGER");
+
+ b.Property("ExpeditionNotify")
+ .HasColumnType("INTEGER");
+
+ b.Property("ExpeditionNotifySuppressed")
+ .HasColumnType("INTEGER");
+
+ b.Property("HomeCoinDotVisible")
+ .HasColumnType("INTEGER");
+
+ b.Property("HomeCoinNotifySuppressed")
+ .HasColumnType("INTEGER");
+
+ b.Property("HomeCoinNotifyThreshold")
+ .HasColumnType("INTEGER");
+
+ b.Property("RefreshTime")
+ .HasColumnType("TEXT");
+
+ b.Property("ResinDotVisible")
+ .HasColumnType("INTEGER");
+
+ b.Property("ResinNotifySuppressed")
+ .HasColumnType("INTEGER");
+
+ b.Property("ResinNotifyThreshold")
+ .HasColumnType("INTEGER");
+
+ b.Property("TransformerDotVisible")
+ .HasColumnType("INTEGER");
+
+ b.Property("TransformerNotify")
+ .HasColumnType("INTEGER");
+
+ b.Property("TransformerNotifySuppressed")
+ .HasColumnType("INTEGER");
+
+ b.Property("Uid")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("daily_notes");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("IsSelected")
+ .HasColumnType("INTEGER");
+
+ b.Property("Uid")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("gacha_archives");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("ArchiveId")
+ .HasColumnType("TEXT");
+
+ b.Property("GachaType")
+ .HasColumnType("INTEGER");
+
+ b.Property("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("INTEGER");
+
+ b.Property("QueryType")
+ .HasColumnType("INTEGER");
+
+ b.Property("Time")
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.HasIndex("ArchiveId");
+
+ b.ToTable("gacha_items");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property("MacAddress")
+ .HasColumnType("TEXT");
+
+ b.Property("Mid")
+ .HasColumnType("TEXT");
+
+ b.Property("MihoyoSDK")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("game_accounts");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.HardChallengeEntry", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("HardChallengeData")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ScheduleId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Uid")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("hard_challenges");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ProjectId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.HasIndex("ProjectId");
+
+ b.ToTable("inventory_items");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("TEXT");
+
+ b.Property("ExpireTime")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Key");
+
+ b.ToTable("object_cache");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.RoleCombatEntry", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("RoleCombatData")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ScheduleId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Uid")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("role_combats");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Key");
+
+ b.ToTable("settings");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("ScheduleId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SpiralAbyss")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Uid")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("spiral_abysses");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.UidProfilePicture", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AvatarId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CostumeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ProfilePictureId")
+ .HasColumnType("INTEGER");
+
+ b.Property("RefreshTime")
+ .HasColumnType("TEXT");
+
+ b.Property("Uid")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("uid_profile_pictures");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
+ {
+ b.Property("InnerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Aid")
+ .HasColumnType("TEXT");
+
+ b.Property("CookieToken")
+ .HasColumnType("TEXT");
+
+ b.Property("CookieTokenLastUpdateTime")
+ .HasColumnType("TEXT");
+
+ b.Property("Fingerprint")
+ .HasColumnType("TEXT");
+
+ b.Property("FingerprintLastUpdateTime")
+ .HasColumnType("TEXT");
+
+ b.Property("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsOversea")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSelected")
+ .HasColumnType("INTEGER");
+
+ b.Property("LToken")
+ .HasColumnType("TEXT")
+ .HasColumnName("Ltoken");
+
+ b.Property("Mid")
+ .HasColumnType("TEXT");
+
+ b.Property("PreferredUid")
+ .HasColumnType("TEXT");
+
+ b.Property("SToken")
+ .HasColumnType("TEXT")
+ .HasColumnName("Stoken");
+
+ b.HasKey("InnerId");
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
+ {
+ b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
+ .WithMany()
+ .HasForeignKey("ArchiveId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Archive");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
+ {
+ b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "RelatedEntry")
+ .WithMany()
+ .HasForeignKey("RelatedEntryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("Project");
+
+ b.Navigation("RelatedEntry");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
+ {
+ b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
+ .WithOne("LevelInformation")
+ .HasForeignKey("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", "EntryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Entry");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
+ {
+ b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
+ .WithMany()
+ .HasForeignKey("EntryId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Entry");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
+ {
+ b.HasOne("Snap.Hutao.Model.Entity.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
+ {
+ b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
+ .WithMany()
+ .HasForeignKey("ArchiveId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Archive");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
+ {
+ b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
+ {
+ b.Navigation("LevelInformation");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.cs
new file mode 100644
index 0000000..2e9c2be
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20260503131124_AddCultivateEntryRelatedEntryId.cs
@@ -0,0 +1,50 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Snap.Hutao.Migrations
+{
+ ///
+ public partial class AddCultivateEntryRelatedEntryId : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "RelatedEntryId",
+ table: "cultivate_entries",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_cultivate_entries_RelatedEntryId",
+ table: "cultivate_entries",
+ column: "RelatedEntryId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
+ table: "cultivate_entries",
+ column: "RelatedEntryId",
+ principalTable: "cultivate_entries",
+ principalColumn: "InnerId",
+ onDelete: ReferentialAction.SetNull);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId",
+ table: "cultivate_entries");
+
+ migrationBuilder.DropIndex(
+ name: "IX_cultivate_entries_RelatedEntryId",
+ table: "cultivate_entries");
+
+ migrationBuilder.DropColumn(
+ name: "RelatedEntryId",
+ table: "cultivate_entries");
+ }
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs
index 17d1e22..58a702c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
@@ -113,6 +113,9 @@ namespace Snap.Hutao.Migrations
b.Property("ProjectId")
.HasColumnType("TEXT");
+ b.Property("RelatedEntryId")
+ .HasColumnType("TEXT");
+
b.Property("Type")
.HasColumnType("INTEGER");
@@ -120,6 +123,8 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId");
+ b.HasIndex("RelatedEntryId");
+
b.ToTable("cultivate_entries");
});
@@ -585,7 +590,14 @@ namespace Snap.Hutao.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
+ b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "RelatedEntry")
+ .WithMany()
+ .HasForeignKey("RelatedEntryId")
+ .OnDelete(DeleteBehavior.SetNull);
+
b.Navigation("Project");
+
+ b.Navigation("RelatedEntry");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs
index 3a00702..78c2219 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/CultivateEntry.cs
@@ -26,6 +26,11 @@ internal sealed class CultivateEntry : IAppDbEntity
public uint Id { get; set; }
+ public Guid? RelatedEntryId { get; set; }
+
+ [ForeignKey(nameof(RelatedEntryId))]
+ public CultivateEntry? RelatedEntry { get; set; }
+
public static CultivateEntry From(Guid projectId, CultivateType type, uint id)
{
return new()
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx
index cf80d67..ee96adc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.en.resx
@@ -1880,6 +1880,9 @@ Space Available: {2}
Ascending only
+
+ Linked character: {0}
+
Added successfully
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
index 5719d09..bffdee1 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
@@ -1931,6 +1931,9 @@
仅突破
+
+ 关联角色:{0}
+
添加成功
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/Consumption/ConsumptionSaveResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/Consumption/ConsumptionSaveResult.cs
new file mode 100644
index 0000000..c24a51d
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/Consumption/ConsumptionSaveResult.cs
@@ -0,0 +1,8 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.Cultivation.Consumption;
+
+internal readonly record struct ConsumptionSaveResult(
+ ConsumptionSaveResultKind Kind,
+ Guid? CreatedEntryInnerId = null);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs
index c1b01e9..b7eda10 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationRepository.cs
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
+using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Abstraction;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
@@ -51,6 +52,26 @@ internal sealed partial class CultivationRepository : ICultivationRepository
return this.ImmutableArray(e => e.ProjectId == projectId && e.Id == itemId);
}
+ public Guid? TryGetAvatarCultivateEntryInnerId(Guid projectId, uint avatarId)
+ {
+ ImmutableArray entries = GetCultivateEntryImmutableArrayByProjectIdAndItemId(projectId, avatarId);
+ Guid? last = null;
+ foreach (ref readonly CultivateEntry entry in entries.AsSpan())
+ {
+ if (entry.Type != CultivateType.AvatarAndSkill)
+ {
+ continue;
+ }
+
+ if (last is null || entry.InnerId.CompareTo(last.Value) > 0)
+ {
+ last = entry.InnerId;
+ }
+ }
+
+ return last;
+ }
+
public void AddCultivateEntry(CultivateEntry entry)
{
this.Add(entry);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs
index 75f0b82..73a19b3 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs
@@ -67,6 +67,12 @@ internal sealed partial class CultivationService : ICultivationService
{
ImmutableArray entries = cultivationRepository.GetCultivateEntryImmutableArrayIncludingLevelInformationByProjectId(cultivateProject.InnerId);
+ Dictionary entryByInnerId = new(entries.Length);
+ foreach (ref readonly CultivateEntry entry in entries.AsSpan())
+ {
+ entryByInnerId[entry.InnerId] = entry;
+ }
+
List resultEntries = new(entries.Length);
foreach (ref readonly CultivateEntry entry in entries.AsSpan())
{
@@ -87,7 +93,13 @@ internal sealed partial class CultivationService : ICultivationService
_ => default!,
};
- resultEntries.Add(CultivateEntryView.Create(entry, item, entryItems.ToImmutable()));
+ string? relatedAvatarName = null;
+ if (entry.Type is CultivateType.Weapon && entry.RelatedEntryId is Guid relatedId && entryByInnerId.TryGetValue(relatedId, out CultivateEntry? relatedEntry) && relatedEntry.Type is CultivateType.AvatarAndSkill)
+ {
+ relatedAvatarName = context.GetAvatarItem(relatedEntry.Id).Name;
+ }
+
+ resultEntries.Add(CultivateEntryView.Create(entry, item, entryItems.ToImmutable(), relatedAvatarName));
}
ObservableCollection result = resultEntries.SortByDescending(e => e.IsToday).ToObservableCollection();
@@ -156,13 +168,13 @@ internal sealed partial class CultivationService : ICultivationService
cultivationRepository.UpdateCultivateItem(item.Entity);
}
- public async ValueTask SaveConsumptionAsync(InputConsumption inputConsumption)
+ public async ValueTask SaveConsumptionAsync(InputConsumption inputConsumption)
{
// No selected project
IAdvancedDbCollectionView projects = await GetProjectCollectionAsync().ConfigureAwait(false);
if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
{
- return ConsumptionSaveResultKind.NoProject;
+ return new(ConsumptionSaveResultKind.NoProject);
}
ArgumentNullException.ThrowIfNull(projects.CurrentItem);
@@ -172,7 +184,7 @@ internal sealed partial class CultivationService : ICultivationService
// PreserveExisting or CreateNewEntry, but no item
if (inputConsumption is { Strategy: not ConsumptionSaveStrategyKind.OverwriteExisting, Items: [] })
{
- return ConsumptionSaveResultKind.NoItem;
+ return new(ConsumptionSaveResultKind.NoItem);
}
// PreserveExisting or OverwriteExisting
@@ -185,7 +197,7 @@ internal sealed partial class CultivationService : ICultivationService
{
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting)
{
- return ConsumptionSaveResultKind.Skipped;
+ return new(ConsumptionSaveResultKind.Skipped);
}
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.OverwriteExisting)
@@ -199,7 +211,7 @@ internal sealed partial class CultivationService : ICultivationService
if (inputConsumption.Items is [])
{
- return ConsumptionSaveResultKind.Removed;
+ return new(ConsumptionSaveResultKind.Removed);
}
}
}
@@ -207,13 +219,14 @@ internal sealed partial class CultivationService : ICultivationService
{
if (inputConsumption.Items is [])
{
- return ConsumptionSaveResultKind.NoItem;
+ return new(ConsumptionSaveResultKind.NoItem);
}
}
}
{
CultivateEntry entry = CultivateEntry.From(projects.CurrentItem.InnerId, inputConsumption.Type, inputConsumption.ItemId);
+ entry.RelatedEntryId = inputConsumption.RelatedEntryId;
cultivationRepository.AddCultivateEntry(entry);
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entry.InnerId, inputConsumption.Type, inputConsumption.LevelInformation);
@@ -225,9 +238,23 @@ internal sealed partial class CultivationService : ICultivationService
// The consumption save operation is always performed outside cultivation page
// and without touching the cache. So we have to invalidate the cache manually.
entryCollectionCache.TryRemove(projects.CurrentItem.InnerId, out _);
+
+ return new(ConsumptionSaveResultKind.Added, entry.InnerId);
+ }
+ }
+
+ public async ValueTask TryGetAvatarCultivateEntryInnerIdAsync(uint avatarId)
+ {
+ IAdvancedDbCollectionView projects = await GetProjectCollectionAsync().ConfigureAwait(false);
+ if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false))
+ {
+ return null;
}
- return ConsumptionSaveResultKind.Added;
+ ArgumentNullException.ThrowIfNull(projects.CurrentItem);
+
+ await taskContext.SwitchToBackgroundAsync();
+ return cultivationRepository.TryGetAvatarCultivateEntryInnerId(projects.CurrentItem.InnerId, avatarId);
}
public async ValueTask TryAddProjectAsync(CultivateProject project)
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs
index a6b7754..7a9454e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationRepository.cs
@@ -43,6 +43,12 @@ internal interface ICultivationRepository : IRepository GetCultivateEntryImmutableArrayByProjectIdAndItemId(Guid projectId, uint itemId);
+ ///
+ /// 解析当前计划中指定角色的养成条目(类型为 CultivateType.AvatarAndSkill)。
+ /// 若存在多条历史记录,取条目主键 InnerId(Guid)较大的一条。
+ ///
+ Guid? TryGetAvatarCultivateEntryInnerId(Guid projectId, uint avatarId);
+
Guid GetCultivateProjectIdByEntryId(Guid entryId);
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs
index 423f898..c3d2eea 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs
@@ -25,7 +25,9 @@ internal interface ICultivationService
ValueTask RemoveProjectAsync(CultivateProject project);
- ValueTask SaveConsumptionAsync(InputConsumption inputConsumption);
+ ValueTask SaveConsumptionAsync(InputConsumption inputConsumption);
+
+ ValueTask TryGetAvatarCultivateEntryInnerIdAsync(uint avatarId);
void SaveCultivateItem(CultivateItemView item);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/InputConsumption.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/InputConsumption.cs
index 65db8d5..dd8994b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/InputConsumption.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/InputConsumption.cs
@@ -19,4 +19,9 @@ internal sealed class InputConsumption
public required LevelInformation LevelInformation { get; init; }
public required ConsumptionSaveStrategyKind Strategy { get; init; }
+
+ ///
+ /// 武器条目关联的养成角色条目的主键(自引用外键,可为空)。
+ ///
+ public Guid? RelatedEntryId { get; init; }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml
index e69d456..403ff70 100644
--- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml
@@ -198,6 +198,11 @@
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Description}"/>
+
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs
index 713277b..fb69c22 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs
@@ -335,9 +335,9 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
Strategy = options.Strategy,
};
- ConsumptionSaveResultKind avatarSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false);
+ ConsumptionSaveResult avatarSave = await scopeContext.CultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false);
- InfoBarMessage? avatarMessage = avatarSaveKind switch
+ InfoBarMessage? avatarMessage = avatarSave.Kind switch
{
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
ConsumptionSaveResultKind.Skipped => isBatch ? default : InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
@@ -351,13 +351,19 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
scopeContext.Messenger.Send(avatarMessage);
}
- if (avatarSaveKind is ConsumptionSaveResultKind.NoProject)
+ if (avatarSave.Kind is ConsumptionSaveResultKind.NoProject)
{
return false;
}
ArgumentNullException.ThrowIfNull(options.Delta.Weapon);
+ Guid? relatedAvatarEntryId = avatarSave.CreatedEntryInnerId;
+ if (relatedAvatarEntryId is null && avatarSave.Kind is ConsumptionSaveResultKind.Skipped)
+ {
+ relatedAvatarEntryId = await scopeContext.CultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false);
+ }
+
InputConsumption weaponInput = new()
{
Type = CultivateType.Weapon,
@@ -365,10 +371,11 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
Items = consumption.WeaponConsume,
LevelInformation = levelInformation,
Strategy = options.Strategy,
+ RelatedEntryId = relatedAvatarEntryId,
};
- ConsumptionSaveResultKind weaponSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false);
- InfoBarMessage? weaponMessage = weaponSaveKind switch
+ ConsumptionSaveResult weaponSave = await scopeContext.CultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false);
+ InfoBarMessage? weaponMessage = weaponSave.Kind switch
{
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
ConsumptionSaveResultKind.Skipped => isBatch ? default : InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
@@ -382,7 +389,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
scopeContext.Messenger.Send(weaponMessage);
}
- return weaponSaveKind is not ConsumptionSaveResultKind.NoProject;
+ return weaponSave.Kind is not ConsumptionSaveResultKind.NoProject;
}
[Command("ExportToTextCommand")]
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs
index 162007d..b952d62 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/CultivateEntryView.cs
@@ -14,7 +14,7 @@ namespace Snap.Hutao.ViewModel.Cultivation;
internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider
{
- private CultivateEntryView(CultivateEntry entry, Item item, ImmutableArray items)
+ private CultivateEntryView(CultivateEntry entry, Item item, ImmutableArray items, string? relatedAvatarName)
{
Id = entry.Id;
EntryId = entry.InnerId;
@@ -25,6 +25,8 @@ internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider
Items = items;
Type = entry.Type;
+ RelatedAvatarLine = relatedAvatarName is { } name ? SH.FormatViewModelCultivationEntryRelatedAvatar(name) : null;
+
Description = ParseDescription(entry);
IsToday = items.Any(i => i.IsToday);
RotationalItemIds = [.. items.Where(i => i.DaysOfWeek is not DaysOfWeek.Any).Select(i => i.Inner.Id)];
@@ -110,12 +112,17 @@ internal sealed partial class CultivateEntryView : Item, IPropertyValuesProvider
public string Description { get; }
+ ///
+ /// 武器条目在养成计算中与角色条目关联时的展示文案(含本地化前缀)。
+ ///
+ public string? RelatedAvatarLine { get; }
+
internal Guid EntryId { get; }
internal CultivateType Type { get; }
- public static CultivateEntryView Create(CultivateEntry entry, Item item, ImmutableArray items)
+ public static CultivateEntryView Create(CultivateEntry entry, Item item, ImmutableArray items, string? relatedAvatarName = null)
{
- return new(entry, item, items);
+ return new(entry, item, items, relatedAvatarName);
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs
index 5417f05..e2abb06 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs
@@ -155,7 +155,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
Strategy = deltaOptions.Strategy,
};
- InfoBarMessage? message = await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false) switch
+ InfoBarMessage? message = (await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false)).Kind switch
{
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
ConsumptionSaveResultKind.Skipped => InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs
index d8f0575..1484d59 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs
@@ -146,7 +146,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
Strategy = deltaOptions.Strategy,
};
- InfoBarMessage? message = await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false) switch
+ InfoBarMessage? message = (await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false)).Kind switch
{
ConsumptionSaveResultKind.NoProject => InfoBarMessage.Warning(SH.ViewModelCultivationEntryAddWarning),
ConsumptionSaveResultKind.Skipped => InfoBarMessage.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint),