Files
jizhi/backend/MiaoJiZhang.Infrastructure/Persistence/AppDbContext.cs
T

221 lines
10 KiB
C#

using MiaoJiZhang.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace MiaoJiZhang.Infrastructure.Persistence;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
public DbSet<UserFeaturePermission> UserFeaturePermissions => Set<UserFeaturePermission>();
public DbSet<AiCompanionSetting> AiCompanionSettings => Set<AiCompanionSetting>();
public DbSet<Ledger> Ledgers => Set<Ledger>();
public DbSet<Category> Categories => Set<Category>();
public DbSet<Transaction> Transactions => Set<Transaction>();
public DbSet<Budget> Budgets => Set<Budget>();
public DbSet<ChatMessage> ChatMessages => Set<ChatMessage>();
public DbSet<AppConfig> AppConfigs => Set<AppConfig>();
public DbSet<AiPersona> AiPersonas => Set<AiPersona>();
public DbSet<AiAvatar> AiAvatars => Set<AiAvatar>();
public DbSet<Sticker> Stickers => Set<Sticker>();
public DbSet<PushDevice> PushDevices => Set<PushDevice>();
public DbSet<UserPushPreference> UserPushPreferences => Set<UserPushPreference>();
public DbSet<PushMessage> PushMessages => Set<PushMessage>();
public DbSet<PushDelivery> PushDeliveries => Set<PushDelivery>();
public DbSet<BudgetNotificationReceipt> BudgetNotificationReceipts => Set<BudgetNotificationReceipt>();
public DbSet<AdminUser> AdminUsers => Set<AdminUser>();
public DbSet<AdminSession> AdminSessions => Set<AdminSession>();
public DbSet<AdminAuditLog> AdminAuditLogs => Set<AdminAuditLog>();
protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<User>(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<AiCompanionSetting>(x => x.UserId);
});
b.Entity<UserFeaturePermission>(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<Transaction>(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<Category>(e => e.Property(x => x.ColorKey).HasMaxLength(32));
b.Entity<Budget>(e =>
{
e.Property(x => x.Amount).HasPrecision(12, 2);
e.HasIndex(x => new { x.LedgerId, x.Period, x.CategoryId }).IsUnique();
});
b.Entity<ChatMessage>(e => e.HasIndex(x => new { x.UserId, x.CreatedAt }));
b.Entity<AppConfig>(e => e.HasIndex(x => x.Key).IsUnique());
b.Entity<AiPersona>(e => e.HasIndex(x => x.Key).IsUnique());
b.Entity<AiAvatar>(e => e.HasIndex(x => x.Key).IsUnique());
b.Entity<Sticker>(e => e.HasIndex(x => x.Key).IsUnique());
b.Entity<PushDevice>(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<UserPushPreference>(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<PushMessage>(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<PushDelivery>(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<BudgetNotificationReceipt>(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<AdminUser>(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<AdminSession>(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<AdminAuditLog>(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<DateTime, DateTime>(
value => value.Kind == DateTimeKind.Local
? value.ToUniversalTime()
: DateTime.SpecifyKind(value, DateTimeKind.Utc),
value => DateTime.SpecifyKind(value, DateTimeKind.Utc));
var nullableUtcDateTimeConverter = new ValueConverter<DateTime?, DateTime?>(
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);
}
}
}
}