Initial project import

This commit is contained in:
2026-07-24 23:11:20 +08:00
commit 6396eabb87
372 changed files with 49682 additions and 0 deletions
@@ -0,0 +1,100 @@
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>();
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.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());
// 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);
}
}
}
}
@@ -0,0 +1,128 @@
using MiaoJiZhang.Domain.Entities;
using MiaoJiZhang.Domain.Enums;
using Microsoft.EntityFrameworkCore;
namespace MiaoJiZhang.Infrastructure.Persistence;
/// <summary>启动种子:默认分类、AI 形象/性格、品牌配置</summary>
public static class DbSeeder
{
public static async Task SeedAsync(AppDbContext db)
{
if (!await db.AiAvatars.AnyAsync())
{
db.AiAvatars.AddRange(
new AiAvatar { Key = "cat", DefaultName = "小账喵", SpeechTic = "喵" },
new AiAvatar { Key = "dog", DefaultName = "阿福汪", SpeechTic = "汪" },
new AiAvatar { Key = "robot", DefaultName = "账小智", SpeechTic = "" });
}
if (!await db.AiPersonas.AnyAsync())
{
db.AiPersonas.AddRange(
new AiPersona { Key = "sassy_cat", Name = "毒舌猫娘", Description = "嘴上嫌弃,心里关心,乱花钱会被吐槽", SampleLine = "又点外卖?你的钱包已经瘦成纸片了{tic}!", PromptTemplate = "你是用户的记账助手,性格毒舌但关心用户。回复末尾口癖:{tic}。", Version = 1 },
new AiPersona { Key = "gentle", Name = "温柔小暖", Description = "永远鼓励你,温柔提醒不施压", SampleLine = "记好啦,偶尔犒劳自己也是应该的呀", PromptTemplate = "你是用户的记账助手,性格温柔鼓励。口癖:{tic}。", Version = 1 },
new AiPersona { Key = "strict", Name = "严格管家", Description = "专业理性,盯紧预算,数据说话", SampleLine = "本笔支出使餐饮预算达到 87%,请注意。", PromptTemplate = "你是用户的记账管家,专业理性,用数据说话,不用口癖。", Version = 1 },
new AiPersona { Key = "meme", Name = "沙雕损友", Description = "玩梗高手,表情包狂魔,快乐记账", SampleLine = "家人们谁懂啊,他又双叒叕买奶茶了", PromptTemplate = "你是用户的记账损友,爱玩梗爱发表情包。口癖:{tic}。", Version = 1 });
}
if (!await db.Categories.AnyAsync(c => c.UserId == null))
{
string[,] expense =
{
{ "餐饮", "food", "coral" },
{ "饮品", "cup", "cyan" },
{ "购物", "cart", "blue" },
{ "交通", "metro", "teal" },
{ "住房", "house", "sand" },
{ "娱乐", "game", "violet" },
{ "医疗", "pill", "red" },
{ "学习", "book", "indigo" },
{ "服饰", "shirt", "plum" },
{ "人情", "gift", "rose" },
{ "旅行", "plane", "sky" },
{ "其他", "tag", "graphite" },
};
for (var i = 0; i < expense.GetLength(0); i++)
db.Categories.Add(new Category
{
Type = TransactionType.Expense,
Name = expense[i, 0],
IconKey = expense[i, 1],
ColorKey = expense[i, 2],
SortOrder = i,
});
string[,] income =
{
{ "工资", "money", "mint" },
{ "兼职", "briefcase", "forest" },
{ "理财", "chart", "navy" },
{ "红包", "gift", "orange" },
{ "报销", "card", "amber" },
{ "奖金", "sparkle", "aqua" },
{ "其他", "tag", "lime" },
};
for (var i = 0; i < income.GetLength(0); i++)
db.Categories.Add(new Category
{
Type = TransactionType.Income,
Name = income[i, 0],
IconKey = income[i, 1],
ColorKey = income[i, 2],
SortOrder = i,
});
}
if (!await db.Stickers.AnyAsync())
{
db.Stickers.AddRange(
new Sticker { Key = "salary", Label = "发工资啦", GroupKey = "ai_exclusive", TriggerTags = "salary" },
new Sticker { Key = "forgive", Label = "求原谅", GroupKey = "ai_exclusive", TriggerTags = "forgive" },
new Sticker { Key = "fire", Label = "剁手警告", GroupKey = "ai_exclusive", TriggerTags = "over_budget" },
new Sticker { Key = "empty_wallet", Label = "钱包空空", GroupKey = "ai_exclusive" },
new Sticker { Key = "angry", Label = "生气", GroupKey = "ai_exclusive" },
new Sticker { Key = "happy", Label = "开心", GroupKey = "ai_exclusive" },
new Sticker { Key = "treat", Label = "犒劳自己", GroupKey = "classic" },
new Sticker { Key = "shopping_joy", Label = "购物狂喜", GroupKey = "classic" });
}
if (!await db.AppConfigs.AnyAsync())
{
var now = DateTime.UtcNow;
db.AppConfigs.AddRange(
new AppConfig { Key = "brand.app_name", Value = "记之", Version = 1, UpdatedAt = now },
new AppConfig { Key = "brand.slogan", Value = "会聊天的记账本 · 让 AI 帮你管钱", Version = 1, UpdatedAt = now },
new AppConfig { Key = "brand.logo_url", Value = "", Version = 1, UpdatedAt = now },
new AppConfig { Key = "llm.protocol", Value = "chat_completions", Version = 1, UpdatedAt = now },
new AppConfig { Key = "llm.base_url", Value = "https://api.openai.com/v1", Version = 1, UpdatedAt = now },
new AppConfig { Key = "llm.model", Value = "gpt-4o-mini", Version = 1, UpdatedAt = now },
new AppConfig { Key = "llm.max_tokens", Value = "1024", Version = 1, UpdatedAt = now },
new AppConfig { Key = "llm.temperature", Value = "0.8", Version = 1, UpdatedAt = now },
new AppConfig { Key = "limit.daily_ai_messages", Value = "200", Version = 1, UpdatedAt = now },
new AppConfig { Key = "limit.daily_ai_messages_per_user", Value = "50", Version = 1, UpdatedAt = now },
new AppConfig { Key = "limit.max_monthly_budget", Value = "99999999", Version = 1, UpdatedAt = now },
new AppConfig { Key = "feature.ocr_enabled", Value = "true", Version = 1, UpdatedAt = now },
new AppConfig { Key = "feature.voice_enabled", Value = "true", Version = 1, UpdatedAt = now },
new AppConfig { Key = "feature.ai_auto_book", Value = "true", Version = 1, UpdatedAt = now },
new AppConfig { Key = "feature.sticker_enabled", Value = "true", Version = 1, UpdatedAt = now },
new AppConfig { Key = "system.default_ledger_name", Value = "日常账本", Version = 1, UpdatedAt = now },
new AppConfig { Key = "system.max_ledgers_per_user", Value = "10", Version = 1, UpdatedAt = now },
new AppConfig { Key = "feature.screenshot_bookkeeping_enabled", Value = "true", Version = 1, UpdatedAt = now },
new AppConfig { Key = "permission.default.ai_enabled", Value = "true", Version = 1, UpdatedAt = now },
new AppConfig { Key = "quota.default_ai_chat_limit", Value = "50", Version = 1, UpdatedAt = now },
new AppConfig { Key = "quota.default_ai_chat_period", Value = "day", Version = 1, UpdatedAt = now });
}
var appName = await db.AppConfigs.FirstOrDefaultAsync(config => config.Key == "brand.app_name");
if (appName?.Value is "喵记账" or "喵记")
{
appName.Value = "记之";
appName.Version++;
appName.UpdatedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync();
}
}
@@ -0,0 +1,548 @@
// <auto-generated />
using System;
using MiaoJiZhang.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260720063643_InitialBaseline")]
partial class InitialBaseline
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AiAvatar", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("DefaultName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("SpeechTic")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("AiAvatars");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AiCompanionSetting", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AvatarKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("CustomName")
.HasColumnType("longtext");
b.Property<string>("PersonaKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("ProactiveLevel")
.HasColumnType("int");
b.Property<int>("RoastLevel")
.HasColumnType("int");
b.Property<int>("StickerFrequency")
.HasColumnType("int");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("AiCompanionSettings");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AiPersona", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PromptTemplate")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("SampleLine")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("AiPersonas");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AppConfig", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("AppConfigs");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Budget", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(12, 2)
.HasColumnType("decimal(12,2)");
b.Property<long?>("CategoryId")
.HasColumnType("bigint");
b.Property<long>("LedgerId")
.HasColumnType("bigint");
b.Property<int>("Period")
.HasColumnType("int");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("LedgerId", "Period", "CategoryId")
.IsUnique();
b.ToTable("Budgets");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Category", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("IconKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<long?>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.ToTable("Categories");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.ChatMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("Role")
.HasColumnType("int");
b.Property<long?>("TransactionId")
.HasColumnType("bigint");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId", "CreatedAt");
b.ToTable("ChatMessages");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Ledger", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("IconKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsDefault")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<long>("OwnerId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Ledgers");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Sticker", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("GroupKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("Label")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("TriggerTags")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("Stickers");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Transaction", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(12, 2)
.HasColumnType("decimal(12,2)");
b.Property<long>("CategoryId")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<long>("LedgerId")
.HasColumnType("bigint");
b.Property<string>("Note")
.HasColumnType("longtext");
b.Property<DateTime>("OccurredAt")
.HasColumnType("datetime(6)");
b.Property<string>("PaymentMethod")
.HasColumnType("longtext");
b.Property<int>("Source")
.HasColumnType("int");
b.Property<long?>("SourceChatMessageId")
.HasColumnType("bigint");
b.Property<string>("SourceText")
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("LedgerId", "OccurredAt");
b.HasIndex("UserId", "IsDeleted");
b.ToTable("Transactions");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.User", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<int>("AppMode")
.HasColumnType("int");
b.Property<string>("AppleUserId")
.HasColumnType("longtext");
b.Property<string>("AvatarUrl")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.HasColumnType("varchar(255)");
b.Property<bool>("EmailVerified")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsBanned")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("datetime(6)");
b.Property<string>("Nickname")
.HasMaxLength(32)
.HasColumnType("varchar(32)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Phone")
.HasColumnType("varchar(255)");
b.Property<bool>("PhoneVerified")
.HasColumnType("tinyint(1)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)");
b.Property<string>("WeChatOpenId")
.HasColumnType("varchar(255)");
b.Property<string>("WeChatUnionId")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Phone")
.IsUnique();
b.HasIndex("Username")
.IsUnique();
b.HasIndex("WeChatOpenId")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AiCompanionSetting", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.User", "User")
.WithOne("AiCompanion")
.HasForeignKey("MiaoJiZhang.Domain.Entities.AiCompanionSetting", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Budget", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.Ledger", "Ledger")
.WithMany("Budgets")
.HasForeignKey("LedgerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ledger");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Ledger", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.User", "Owner")
.WithMany("Ledgers")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Transaction", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MiaoJiZhang.Domain.Entities.Ledger", "Ledger")
.WithMany("Transactions")
.HasForeignKey("LedgerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
b.Navigation("Ledger");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Ledger", b =>
{
b.Navigation("Budgets");
b.Navigation("Transactions");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.User", b =>
{
b.Navigation("AiCompanion");
b.Navigation("Ledgers");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,431 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class InitialBaseline : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "AiAvatars",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Key = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
DefaultName = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SpeechTic = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ImageUrl = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AiAvatars", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "AiPersonas",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Key = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SampleLine = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
PromptTemplate = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Version = table.Column<int>(type: "int", nullable: false),
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AiPersonas", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "AppConfigs",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Key = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Value = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Version = table.Column<int>(type: "int", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppConfigs", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<long>(type: "bigint", nullable: true),
Type = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IconKey = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SortOrder = table.Column<int>(type: "int", nullable: false),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "ChatMessages",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<long>(type: "bigint", nullable: false),
Role = table.Column<int>(type: "int", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
Content = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TransactionId = table.Column<long>(type: "bigint", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChatMessages", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Stickers",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Key = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Label = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ImageUrl = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
GroupKey = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TriggerTags = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Stickers", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Username = table.Column<string>(type: "varchar(32)", maxLength: 32, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
PasswordHash = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Nickname = table.Column<string>(type: "varchar(32)", maxLength: 32, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
AvatarUrl = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
EmailVerified = table.Column<bool>(type: "tinyint(1)", nullable: false),
Phone = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
PhoneVerified = table.Column<bool>(type: "tinyint(1)", nullable: false),
WeChatOpenId = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
WeChatUnionId = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
AppleUserId = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
AppMode = table.Column<int>(type: "int", nullable: false),
IsBanned = table.Column<bool>(type: "tinyint(1)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
LastLoginAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "AiCompanionSettings",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<long>(type: "bigint", nullable: false),
AvatarKey = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CustomName = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
PersonaKey = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
RoastLevel = table.Column<int>(type: "int", nullable: false),
StickerFrequency = table.Column<int>(type: "int", nullable: false),
ProactiveLevel = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AiCompanionSettings", x => x.Id);
table.ForeignKey(
name: "FK_AiCompanionSettings_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Ledgers",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
OwnerId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IconKey = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IsDefault = table.Column<bool>(type: "tinyint(1)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Ledgers", x => x.Id);
table.ForeignKey(
name: "FK_Ledgers_Users_OwnerId",
column: x => x.OwnerId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Budgets",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
LedgerId = table.Column<long>(type: "bigint", nullable: false),
UserId = table.Column<long>(type: "bigint", nullable: false),
CategoryId = table.Column<long>(type: "bigint", nullable: true),
Period = table.Column<int>(type: "int", nullable: false),
Amount = table.Column<decimal>(type: "decimal(12,2)", precision: 12, scale: 2, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Budgets", x => x.Id);
table.ForeignKey(
name: "FK_Budgets_Ledgers_LedgerId",
column: x => x.LedgerId,
principalTable: "Ledgers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Transactions",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
LedgerId = table.Column<long>(type: "bigint", nullable: false),
UserId = table.Column<long>(type: "bigint", nullable: false),
CategoryId = table.Column<long>(type: "bigint", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
Amount = table.Column<decimal>(type: "decimal(12,2)", precision: 12, scale: 2, nullable: false),
Note = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
PaymentMethod = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
OccurredAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Source = table.Column<int>(type: "int", nullable: false),
SourceText = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
SourceChatMessageId = table.Column<long>(type: "bigint", nullable: true),
IsDeleted = table.Column<bool>(type: "tinyint(1)", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Transactions", x => x.Id);
table.ForeignKey(
name: "FK_Transactions_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Transactions_Ledgers_LedgerId",
column: x => x.LedgerId,
principalTable: "Ledgers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_AiAvatars_Key",
table: "AiAvatars",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AiCompanionSettings_UserId",
table: "AiCompanionSettings",
column: "UserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AiPersonas_Key",
table: "AiPersonas",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AppConfigs_Key",
table: "AppConfigs",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Budgets_LedgerId_Period_CategoryId",
table: "Budgets",
columns: new[] { "LedgerId", "Period", "CategoryId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ChatMessages_UserId_CreatedAt",
table: "ChatMessages",
columns: new[] { "UserId", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_Ledgers_OwnerId",
table: "Ledgers",
column: "OwnerId");
migrationBuilder.CreateIndex(
name: "IX_Stickers_Key",
table: "Stickers",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Transactions_CategoryId",
table: "Transactions",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Transactions_LedgerId_OccurredAt",
table: "Transactions",
columns: new[] { "LedgerId", "OccurredAt" });
migrationBuilder.CreateIndex(
name: "IX_Transactions_UserId_IsDeleted",
table: "Transactions",
columns: new[] { "UserId", "IsDeleted" });
migrationBuilder.CreateIndex(
name: "IX_Users_Email",
table: "Users",
column: "Email",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_Phone",
table: "Users",
column: "Phone",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_Username",
table: "Users",
column: "Username",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_WeChatOpenId",
table: "Users",
column: "WeChatOpenId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AiAvatars");
migrationBuilder.DropTable(
name: "AiCompanionSettings");
migrationBuilder.DropTable(
name: "AiPersonas");
migrationBuilder.DropTable(
name: "AppConfigs");
migrationBuilder.DropTable(
name: "Budgets");
migrationBuilder.DropTable(
name: "ChatMessages");
migrationBuilder.DropTable(
name: "Stickers");
migrationBuilder.DropTable(
name: "Transactions");
migrationBuilder.DropTable(
name: "Categories");
migrationBuilder.DropTable(
name: "Ledgers");
migrationBuilder.DropTable(
name: "Users");
}
}
}
@@ -0,0 +1,68 @@
using MiaoJiZhang.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations;
[DbContext(typeof(AppDbContext))]
[Migration("20260721120000_AccountClosureAndCategoryIcons")]
public partial class AccountClosureAndCategoryIcons : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "AccountClosureRequestedAt",
table: "Users",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "AccountClosureScheduledAt",
table: "Users",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "AuthVersion",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_Users_AccountClosureScheduledAt",
table: "Users",
column: "AccountClosureScheduledAt");
migrationBuilder.Sql(
"UPDATE Categories SET IconKey = 'cart' WHERE IconKey = 'shopping';");
migrationBuilder.Sql(
"UPDATE Categories SET IconKey = 'metro' WHERE IconKey = 'transport';");
migrationBuilder.Sql(
"UPDATE Categories SET IconKey = 'house' WHERE IconKey = 'home';");
migrationBuilder.Sql(
"UPDATE Categories SET IconKey = 'money' WHERE IconKey = 'salary';");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Users_AccountClosureScheduledAt",
table: "Users");
migrationBuilder.DropColumn(
name: "AccountClosureRequestedAt",
table: "Users");
migrationBuilder.DropColumn(
name: "AccountClosureScheduledAt",
table: "Users");
migrationBuilder.DropColumn(
name: "AuthVersion",
table: "Users");
}
}
@@ -0,0 +1,50 @@
using MiaoJiZhang.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations;
[DbContext(typeof(AppDbContext))]
[Migration("20260721173000_CategoryColors")]
public partial class CategoryColors : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ColorKey",
table: "Categories",
type: "varchar(32)",
maxLength: 32,
nullable: false,
defaultValue: "mint");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'coral' WHERE UserId IS NULL AND Type = 0 AND Name = '餐饮';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'cyan' WHERE UserId IS NULL AND Type = 0 AND Name = '饮品';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'blue' WHERE UserId IS NULL AND Type = 0 AND Name = '购物';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'teal' WHERE UserId IS NULL AND Type = 0 AND Name = '交通';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'sand' WHERE UserId IS NULL AND Type = 0 AND Name = '住房';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'violet' WHERE UserId IS NULL AND Type = 0 AND Name = '娱乐';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'red' WHERE UserId IS NULL AND Type = 0 AND Name = '医疗';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'indigo' WHERE UserId IS NULL AND Type = 0 AND Name = '学习';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'plum' WHERE UserId IS NULL AND Type = 0 AND Name = '服饰';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'rose' WHERE UserId IS NULL AND Type = 0 AND Name = '人情';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'sky' WHERE UserId IS NULL AND Type = 0 AND Name = '旅行';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'graphite' WHERE UserId IS NULL AND Type = 0 AND Name = '其他';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'mint' WHERE UserId IS NULL AND Type = 1 AND Name = '工资';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'forest' WHERE UserId IS NULL AND Type = 1 AND Name = '兼职';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'navy' WHERE UserId IS NULL AND Type = 1 AND Name = '理财';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'orange' WHERE UserId IS NULL AND Type = 1 AND Name = '红包';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'amber' WHERE UserId IS NULL AND Type = 1 AND Name = '报销';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'aqua' WHERE UserId IS NULL AND Type = 1 AND Name = '奖金';");
migrationBuilder.Sql("UPDATE Categories SET ColorKey = 'lime' WHERE UserId IS NULL AND Type = 1 AND Name = '其他';");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ColorKey",
table: "Categories");
}
}
@@ -0,0 +1,90 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations;
[DbContext(typeof(AppDbContext))]
[Migration("20260721200000_AiPermissionsAndRecognitionIdempotency")]
public partial class AiPermissionsAndRecognitionIdempotency : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ClientRequestId",
table: "Transactions",
type: "varchar(64)",
maxLength: 64,
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "UserFeaturePermissions",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<long>(type: "bigint", nullable: false),
PermissionKey = table.Column<string>(type: "varchar(32)", maxLength: 32, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_UserFeaturePermissions", x => x.Id);
table.ForeignKey(
name: "FK_UserFeaturePermissions_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Transactions_UserId_ClientRequestId",
table: "Transactions",
columns: new[] { "UserId", "ClientRequestId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserFeaturePermissions_UserId_PermissionKey",
table: "UserFeaturePermissions",
columns: new[] { "UserId", "PermissionKey" },
unique: true);
migrationBuilder.Sql("""
INSERT INTO UserFeaturePermissions
(UserId, PermissionKey, IsEnabled, CreatedAt, UpdatedAt)
SELECT Id, 'ai', TRUE, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6)
FROM Users
WHERE NOT EXISTS (
SELECT 1 FROM UserFeaturePermissions p
WHERE p.UserId = Users.Id AND p.PermissionKey = 'ai'
);
""");
migrationBuilder.Sql("""
INSERT INTO AppConfigs (`Key`, Value, Version, UpdatedAt)
SELECT 'permission.default.ai_enabled', 'true', 1, UTC_TIMESTAMP(6)
WHERE NOT EXISTS (
SELECT 1 FROM AppConfigs
WHERE `Key` = 'permission.default.ai_enabled'
);
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "UserFeaturePermissions");
migrationBuilder.DropIndex(
name: "IX_Transactions_UserId_ClientRequestId",
table: "Transactions");
migrationBuilder.DropColumn(name: "ClientRequestId", table: "Transactions");
migrationBuilder.Sql(
"DELETE FROM AppConfigs WHERE `Key` = 'permission.default.ai_enabled';");
}
}
@@ -0,0 +1,75 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations;
[DbContext(typeof(AppDbContext))]
[Migration("20260722030000_AiChatQuotas")]
public partial class AiChatQuotas : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AiChatLimit",
table: "Users",
type: "int",
nullable: false,
defaultValue: 50);
migrationBuilder.AddColumn<int>(
name: "AiChatPeriod",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "AiChatUsed",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<DateTime>(
name: "AiChatWindowStartedAt",
table: "Users",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1970, 1, 1));
migrationBuilder.Sql("""
UPDATE Users
SET AiChatWindowStartedAt =
CONVERT_TZ(DATE(CONVERT_TZ(UTC_TIMESTAMP(6), '+00:00', '+08:00')), '+08:00', '+00:00');
""");
migrationBuilder.Sql("""
INSERT INTO AppConfigs (`Key`, Value, Version, UpdatedAt)
SELECT 'quota.default_ai_chat_limit', '50', 1, UTC_TIMESTAMP(6)
WHERE NOT EXISTS (
SELECT 1 FROM AppConfigs
WHERE `Key` = 'quota.default_ai_chat_limit'
);
""");
migrationBuilder.Sql("""
INSERT INTO AppConfigs (`Key`, Value, Version, UpdatedAt)
SELECT 'quota.default_ai_chat_period', 'day', 1, UTC_TIMESTAMP(6)
WHERE NOT EXISTS (
SELECT 1 FROM AppConfigs
WHERE `Key` = 'quota.default_ai_chat_period'
);
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "AiChatLimit", table: "Users");
migrationBuilder.DropColumn(name: "AiChatPeriod", table: "Users");
migrationBuilder.DropColumn(name: "AiChatUsed", table: "Users");
migrationBuilder.DropColumn(name: "AiChatWindowStartedAt", table: "Users");
migrationBuilder.Sql(
"DELETE FROM AppConfigs WHERE `Key` IN ('quota.default_ai_chat_limit', 'quota.default_ai_chat_period');");
}
}
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations;
[DbContext(typeof(AppDbContext))]
[Migration("20260722040000_RepairRecognitionTimestamps")]
public partial class RepairRecognitionTimestamps : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
UPDATE Transactions
SET OccurredAt = CreatedAt
WHERE Source IN (5, 6, 7)
AND OccurredAt < '2000-01-01 00:00:00';
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
@@ -0,0 +1,627 @@
// <auto-generated />
using System;
using MiaoJiZhang.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AiAvatar", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("DefaultName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("SpeechTic")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("AiAvatars");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AiCompanionSetting", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AvatarKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("CustomName")
.HasColumnType("longtext");
b.Property<string>("PersonaKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("ProactiveLevel")
.HasColumnType("int");
b.Property<int>("RoastLevel")
.HasColumnType("int");
b.Property<int>("StickerFrequency")
.HasColumnType("int");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("AiCompanionSettings");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AiPersona", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PromptTemplate")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("SampleLine")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("AiPersonas");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AppConfig", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Key")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("AppConfigs");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Budget", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(12, 2)
.HasColumnType("decimal(12,2)");
b.Property<long?>("CategoryId")
.HasColumnType("bigint");
b.Property<long>("LedgerId")
.HasColumnType("bigint");
b.Property<int>("Period")
.HasColumnType("int");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("LedgerId", "Period", "CategoryId")
.IsUnique();
b.ToTable("Budgets");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Category", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("ColorKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)");
b.Property<string>("IconKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<long?>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.ToTable("Categories");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.ChatMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Content")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("Role")
.HasColumnType("int");
b.Property<long?>("TransactionId")
.HasColumnType("bigint");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId", "CreatedAt");
b.ToTable("ChatMessages");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Ledger", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("IconKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsDefault")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<long>("OwnerId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Ledgers");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Sticker", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("GroupKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("Label")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("TriggerTags")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("Stickers");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Transaction", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(12, 2)
.HasColumnType("decimal(12,2)");
b.Property<long>("CategoryId")
.HasColumnType("bigint");
b.Property<string>("ClientRequestId")
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsDeleted")
.HasColumnType("tinyint(1)");
b.Property<long>("LedgerId")
.HasColumnType("bigint");
b.Property<string>("Note")
.HasColumnType("longtext");
b.Property<DateTime>("OccurredAt")
.HasColumnType("datetime(6)");
b.Property<string>("PaymentMethod")
.HasColumnType("longtext");
b.Property<int>("Source")
.HasColumnType("int");
b.Property<long?>("SourceChatMessageId")
.HasColumnType("bigint");
b.Property<string>("SourceText")
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("LedgerId", "OccurredAt");
b.HasIndex("UserId", "IsDeleted");
b.HasIndex("UserId", "ClientRequestId")
.IsUnique();
b.ToTable("Transactions");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.UserFeaturePermission", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("PermissionKey")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId", "PermissionKey")
.IsUnique();
b.ToTable("UserFeaturePermissions");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.User", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<int>("AppMode")
.HasColumnType("int");
b.Property<int>("AiChatLimit")
.HasColumnType("int");
b.Property<int>("AiChatPeriod")
.HasColumnType("int");
b.Property<int>("AiChatUsed")
.HasColumnType("int");
b.Property<DateTime>("AiChatWindowStartedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("AccountClosureRequestedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("AccountClosureScheduledAt")
.HasColumnType("datetime(6)");
b.Property<string>("AppleUserId")
.HasColumnType("longtext");
b.Property<string>("AvatarUrl")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.HasColumnType("varchar(255)");
b.Property<bool>("EmailVerified")
.HasColumnType("tinyint(1)");
b.Property<int>("AuthVersion")
.HasColumnType("int");
b.Property<bool>("IsBanned")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("datetime(6)");
b.Property<string>("Nickname")
.HasMaxLength(32)
.HasColumnType("varchar(32)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Phone")
.HasColumnType("varchar(255)");
b.Property<bool>("PhoneVerified")
.HasColumnType("tinyint(1)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)");
b.Property<string>("WeChatOpenId")
.HasColumnType("varchar(255)");
b.Property<string>("WeChatUnionId")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("AccountClosureScheduledAt");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Phone")
.IsUnique();
b.HasIndex("Username")
.IsUnique();
b.HasIndex("WeChatOpenId")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.AiCompanionSetting", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.User", "User")
.WithOne("AiCompanion")
.HasForeignKey("MiaoJiZhang.Domain.Entities.AiCompanionSetting", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Budget", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.Ledger", "Ledger")
.WithMany("Budgets")
.HasForeignKey("LedgerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ledger");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Ledger", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.User", "Owner")
.WithMany("Ledgers")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Transaction", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MiaoJiZhang.Domain.Entities.Ledger", "Ledger")
.WithMany("Transactions")
.HasForeignKey("LedgerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
b.Navigation("Ledger");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.UserFeaturePermission", b =>
{
b.HasOne("MiaoJiZhang.Domain.Entities.User", "User")
.WithMany("FeaturePermissions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.Ledger", b =>
{
b.Navigation("Budgets");
b.Navigation("Transactions");
});
modelBuilder.Entity("MiaoJiZhang.Domain.Entities.User", b =>
{
b.Navigation("AiCompanion");
b.Navigation("FeaturePermissions");
b.Navigation("Ledgers");
});
#pragma warning restore 612, 618
}
}
}