using MiaoJiZhang.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace MiaoJiZhang.Infrastructure.Persistence; public class AppDbContext(DbContextOptions options) : DbContext(options) { public DbSet Users => Set(); public DbSet UserFeaturePermissions => Set(); public DbSet AiCompanionSettings => Set(); public DbSet Ledgers => Set(); public DbSet Categories => Set(); public DbSet Transactions => Set(); public DbSet Budgets => Set(); public DbSet ChatMessages => Set(); public DbSet AppConfigs => Set(); public DbSet AiPersonas => Set(); public DbSet AiAvatars => Set(); public DbSet Stickers => Set(); public DbSet PushDevices => Set(); public DbSet UserPushPreferences => Set(); public DbSet PushMessages => Set(); public DbSet PushDeliveries => Set(); public DbSet BudgetNotificationReceipts => Set(); public DbSet AdminUsers => Set(); public DbSet AdminSessions => Set(); public DbSet AdminAuditLogs => Set(); protected override void OnModelCreating(ModelBuilder b) { b.Entity(e => { e.HasIndex(x => x.Username).IsUnique(); e.Property(x => x.Username).HasMaxLength(32); e.Property(x => x.Nickname).HasMaxLength(32); // 预留字段索引(唯一但允许 null) e.HasIndex(x => x.Email).IsUnique(); e.HasIndex(x => x.Phone).IsUnique(); e.HasIndex(x => x.WeChatOpenId).IsUnique(); e.HasIndex(x => x.AccountClosureScheduledAt); e.HasOne(x => x.AiCompanion).WithOne(x => x.User) .HasForeignKey(x => x.UserId); }); b.Entity(e => { e.Property(x => x.PermissionKey).HasMaxLength(32); e.HasIndex(x => new { x.UserId, x.PermissionKey }).IsUnique(); e.HasOne(x => x.User).WithMany(x => x.FeaturePermissions) .HasForeignKey(x => x.UserId).OnDelete(DeleteBehavior.Cascade); }); b.Entity(e => { e.Property(x => x.Amount).HasPrecision(12, 2); e.HasIndex(x => new { x.LedgerId, x.OccurredAt }); e.HasIndex(x => new { x.UserId, x.IsDeleted }); e.Property(x => x.ClientRequestId).HasMaxLength(64); e.HasIndex(x => new { x.UserId, x.ClientRequestId }).IsUnique(); e.Property(x => x.Counterparty).HasMaxLength(100); e.Property(x => x.Provider).HasMaxLength(24); e.Property(x => x.ProviderTransactionId).HasMaxLength(128); e.Property(x => x.RecognitionOccurrenceId).HasMaxLength(64); e.Property(x => x.EvidenceFingerprint).HasMaxLength(64); e.Property(x => x.RecognitionConfidence).HasMaxLength(24); e.HasIndex(x => new { x.UserId, x.Provider, x.ProviderTransactionId }).IsUnique(); e.HasIndex(x => new { x.UserId, x.RecognitionOccurrenceId }).IsUnique(); e.HasQueryFilter(x => !x.IsDeleted); // 软删除全局过滤 }); b.Entity(e => e.Property(x => x.ColorKey).HasMaxLength(32)); b.Entity(e => { e.Property(x => x.Amount).HasPrecision(12, 2); e.HasIndex(x => new { x.LedgerId, x.Period, x.CategoryId }).IsUnique(); }); b.Entity(e => e.HasIndex(x => new { x.UserId, x.CreatedAt })); b.Entity(e => e.HasIndex(x => x.Key).IsUnique()); b.Entity(e => e.HasIndex(x => x.Key).IsUnique()); b.Entity(e => e.HasIndex(x => x.Key).IsUnique()); b.Entity(e => e.HasIndex(x => x.Key).IsUnique()); b.Entity(e => { e.Property(x => x.InstallationId).HasMaxLength(64); e.Property(x => x.Provider).HasMaxLength(16); e.Property(x => x.TokenCiphertext).HasMaxLength(6144); e.Property(x => x.TokenHash).HasMaxLength(64); e.Property(x => x.UnbindTokenHash).HasMaxLength(64); e.Property(x => x.PackageName).HasMaxLength(128); e.Property(x => x.Flavor).HasMaxLength(24); e.Property(x => x.AppVersion).HasMaxLength(32); e.Property(x => x.DisabledReason).HasMaxLength(64); e.HasIndex(x => new { x.PackageName, x.InstallationId }).IsUnique(); e.HasIndex(x => new { x.Provider, x.PackageName, x.TokenHash }).IsUnique(); e.HasIndex(x => new { x.UserId, x.IsActive }); e.HasIndex(x => x.LastSeenAt); e.HasOne(x => x.User).WithMany(x => x.PushDevices) .HasForeignKey(x => x.UserId).OnDelete(DeleteBehavior.Cascade); }); b.Entity(e => { e.Property(x => x.Category).HasMaxLength(24); e.HasIndex(x => new { x.UserId, x.Category }).IsUnique(); e.HasOne(x => x.User).WithMany(x => x.PushPreferences) .HasForeignKey(x => x.UserId).OnDelete(DeleteBehavior.Cascade); }); b.Entity(e => { e.Property(x => x.PublicId).HasMaxLength(36); e.Property(x => x.Source).HasMaxLength(24); e.Property(x => x.State).HasMaxLength(24); e.Property(x => x.Category).HasMaxLength(24); e.Property(x => x.Title).HasMaxLength(80); e.Property(x => x.Body).HasMaxLength(240); e.Property(x => x.Action).HasMaxLength(24); e.Property(x => x.EntityId).HasMaxLength(64); e.Property(x => x.Flavor).HasMaxLength(24); e.Property(x => x.ProviderFilter).HasMaxLength(16); e.HasIndex(x => x.PublicId).IsUnique(); e.HasIndex(x => new { x.State, x.ScheduledAt }); e.HasIndex(x => new { x.TargetUserId, x.CreatedAt }); }); b.Entity(e => { e.Property(x => x.Provider).HasMaxLength(16); e.Property(x => x.State).HasMaxLength(24); e.Property(x => x.LeaseId).HasMaxLength(36); e.Property(x => x.ProviderMessageId).HasMaxLength(128); e.Property(x => x.ErrorCode).HasMaxLength(64); e.Property(x => x.ErrorMessage).HasMaxLength(400); e.HasIndex(x => new { x.PushMessageId, x.PushDeviceId }).IsUnique(); e.HasIndex(x => new { x.State, x.NextAttemptAt, x.LeaseExpiresAt }); e.HasOne(x => x.PushMessage).WithMany(x => x.Deliveries) .HasForeignKey(x => x.PushMessageId).OnDelete(DeleteBehavior.Cascade); e.HasOne(x => x.PushDevice).WithMany(x => x.Deliveries) .HasForeignKey(x => x.PushDeviceId).OnDelete(DeleteBehavior.Cascade); }); b.Entity(e => { e.HasIndex(x => new { x.BudgetId, x.Period, x.Threshold }).IsUnique(); e.HasIndex(x => new { x.UserId, x.Period }); e.HasOne(x => x.User).WithMany() .HasForeignKey(x => x.UserId).OnDelete(DeleteBehavior.Cascade); e.HasOne(x => x.Budget).WithMany() .HasForeignKey(x => x.BudgetId).OnDelete(DeleteBehavior.Cascade); }); b.Entity(e => { e.Property(x => x.Username).HasMaxLength(64); e.Property(x => x.PasswordHash).HasMaxLength(128); e.Property(x => x.Role).HasMaxLength(24); e.HasIndex(x => x.Username).IsUnique(); e.HasIndex(x => new { x.IsActive, x.Role }); }); b.Entity(e => { e.Property(x => x.TokenHash).HasMaxLength(64); e.Property(x => x.CsrfTokenHash).HasMaxLength(64); e.Property(x => x.IpAddress).HasMaxLength(64); e.Property(x => x.UserAgent).HasMaxLength(300); e.HasIndex(x => x.TokenHash).IsUnique(); e.HasIndex(x => new { x.AdminUserId, x.RevokedAt, x.ExpiresAt }); e.HasOne(x => x.AdminUser).WithMany(x => x.Sessions) .HasForeignKey(x => x.AdminUserId).OnDelete(DeleteBehavior.Cascade); }); b.Entity(e => { e.Property(x => x.Username).HasMaxLength(64); e.Property(x => x.Action).HasMaxLength(80); e.Property(x => x.Resource).HasMaxLength(120); e.Property(x => x.HttpMethod).HasMaxLength(12); e.Property(x => x.Path).HasMaxLength(300); e.Property(x => x.Detail).HasMaxLength(1000); e.Property(x => x.IpAddress).HasMaxLength(64); e.HasIndex(x => x.CreatedAt); e.HasIndex(x => new { x.AdminUserId, x.CreatedAt }); }); // MySQL DATETIME has no timezone metadata. Preserve stored UTC wall-clock // values and restore DateTimeKind.Utc whenever EF materializes them. var utcDateTimeConverter = new ValueConverter( value => value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : DateTime.SpecifyKind(value, DateTimeKind.Utc), value => DateTime.SpecifyKind(value, DateTimeKind.Utc)); var nullableUtcDateTimeConverter = new ValueConverter( value => value.HasValue ? value.Value.Kind == DateTimeKind.Local ? value.Value.ToUniversalTime() : DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : value, value => value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : value); foreach (var property in b.Model.GetEntityTypes().SelectMany(entity => entity.GetProperties())) { if (property.ClrType == typeof(DateTime)) { property.SetValueConverter(utcDateTimeConverter); } else if (property.ClrType == typeof(DateTime?)) { property.SetValueConverter(nullableUtcDateTimeConverter); } } } }