using System.Security.Cryptography; using EARSystem.Core; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace EARSystem.Infrastructure; public record SeedCredentials(string Email, string Password); public class DemoSeeder(AppDbContext db, UserManager users, RoleManager roles, TimeProvider clock) { public async Task InitializeAsync(bool demo, string? credentialsPath = null, string? adminEmail = null, string? adminPassword = null, double centerLng = 120.155, double centerLat = 30.274) { foreach (var role in new[] { "Admin", "User" }) if (!await roles.RoleExistsAsync(role)) await roles.CreateAsync(new(role)); if (!await db.Rules.AnyAsync()) db.Rules.Add(new RuleConfiguration()); await db.SaveChangesAsync(); var credentials = new List(); if (!string.IsNullOrWhiteSpace(adminEmail) && !string.IsNullOrWhiteSpace(adminPassword)) await CreateUser(adminEmail, "管理员", false, "Admin", adminPassword, credentials); else if (demo) await CreateUser("admin@ear.local", "演示管理员", true, "Admin", Password(), credentials); if (!demo) return; var rule = await db.Rules.SingleAsync(); // A completed seed is never replayed: deleted or edited demo records stay that way. if (rule.DemoSeededAt.HasValue) { await SaveCredentials(); return; } var members = new List(); var demoPassword = Password(); for (var i = 1; i <= 20; i++) members.Add(await CreateUser($"demo{i:00}@ear.local", i == 1 ? "小耳同学" : $"示例用户 {i:00}", true, "User", demoPassword, credentials)); // Persist generated credentials before the larger data generation operation. await SaveCredentials(); if (!await db.Places.AnyAsync(x => x.Source == PlaceSource.Demo)) { var random = new Random(42); string[] names = ["一碗好心情", "慢慢咖啡", "绿意小径", "纸间书屋", "留白展厅", "周末游戏屋"]; string[] descriptions = ["用一顿喜欢的饭,为日常留一点满足。", "找个舒服的角落,让节奏慢下来。", "走进绿意里,留一点时间给自己。", "翻几页书,安静地专注一会儿。", "看看展览,让新的灵感自然发生。", "约上朋友,在轻松互动里补充能量。"]; for (var c = 0; c < 6; c++) for (var i = 0; i < 10; i++) { var p = new Place { Name = $"{names[c]} · 示例 {i + 1:00}", Address = $"杭州演示区域 · 自建示例位置 {c * 10 + i + 1:00}", Description = descriptions[c] + " 本地点为虚构演示样本,不代表真实商家。", Category = (PlaceCategory)c, Longitude = centerLng + (random.NextDouble() - .5) * .045, Latitude = centerLat + (random.NextDouble() - .5) * .036, Price = i == 8 ? null : c == 2 ? 0 : c == 0 ? 25 + i * 8 : c == 1 ? 18 + i * 5 : 10 + i * 4, Rating = i == 9 ? null : Math.Round(3.6 + random.NextDouble() * 1.3, 1), OpenHour = 0, CloseHour = 0, Source = PlaceSource.Demo }; var a = c switch { 0 => new double[] { 3, 4, 1, 3, 4, 1 }, 1 => [4, 5, 2, 2, 3, 4], 2 => [4, 4, 5, 2, 2, 2], 3 => [5, 4, 2, 1, 1, 5], 4 => [4, 4, 2, 2, 2, 3], _ => [1, 4, 1, 5, 5, 1] }; p.Atmosphere = new() { PlaceId = p.Id, Quiet = a[0], Comfort = a[1], Nature = a[2], Liveliness = a[3], Social = a[4], Focus = a[5], BaselineCount = i == 8 ? 0 : 8 + i * 3, Evidence = "合成演示基线;不代表实测或真实用户评价" }; db.Places.Add(p); } await db.SaveChangesAsync(); } var places = await db.Places.Where(x => x.Source == PlaceSource.Demo).OrderBy(x => x.Name).ToArrayAsync(); var now = clock.GetUtcNow(); var today = ChinaTime.Day(now); var rng = new Random(20261006); foreach (var (user, userIndex) in members.Select((u, i) => (u, i))) { if (await db.Moods.AnyAsync(x => x.UserId == user.Id)) continue; db.Preferences.Add(new() { UserId = user.Id, FavoriteCategoriesJson = Labels.Json(new[] { PlaceCategory.Cafe, PlaceCategory.Park }) }); for (var day = 89; day >= 0; day--) { if (rng.Next(10) == 0 && day != 0) continue; var recorded = ChinaTime.Start(today.AddDays(-day)).AddHours(9 + userIndex % 6); if (recorded > now) recorded = now.AddMinutes(-15); var mood = Math.Clamp((int)Math.Round(3.4 + Math.Sin((89 - day + userIndex) / 6.0) + rng.NextDouble() - .5), 1, 5); db.Moods.Add(new() { UserId = user.Id, Mood = mood, Energy = rng.Next(2, 6), Stress = rng.Next(1, 5), RecordedAt = recorded, Thoughts = day == 0 ? "今天想给自己一点轻松的时间。路过咖啡馆时,想找个安静的角落坐一会儿。" : "合成演示日记:记录日常的小变化。", TagsJson = Labels.Json(new[] { mood >= 4 ? "开心" : mood <= 2 ? "低落" : "平静" }) }); if (day % 7 == 1) { var p = places[(day + userIndex * 3) % places.Length]; db.Experiences.Add(new() { UserId = user.Id, PlaceId = p.Id, VisitedAt = recorded, VisitDay = ChinaTime.DayKey(recorded), Satisfaction = rng.Next(3, 6), MoodBefore = 3, MoodAfter = mood, Quiet = 4, Comfort = 4, Nature = 3, Liveliness = 2, Social = 3, Focus = 3, Contribute = true, Thoughts = "合成演示体验:给忙碌的日常留一个停顿。" }); } } foreach (var p in places.Skip(userIndex % 10).Take(4)) db.Favorites.Add(new() { UserId = user.Id, PlaceId = p.Id }); } rule.DemoSeededAt = now; await db.SaveChangesAsync(); async Task SaveCredentials() { if (credentialsPath != null && credentials.Count > 0) { Directory.CreateDirectory(Path.GetDirectoryName(credentialsPath)!); await File.AppendAllLinesAsync(credentialsPath, credentials.Select(x => $"{x.Email}\t{x.Password}")); credentials.Clear(); } } } private async Task CreateUser(string email, string displayName, bool isDemo, string role, string password, List credentials) { var existing = await users.FindByEmailAsync(email); if (existing != null) return existing; var user = new ApplicationUser { UserName = email, Email = email, DisplayName = displayName, IsDemo = isDemo, EmailConfirmed = true }; var result = await users.CreateAsync(user, password); if (!result.Succeeded) throw new InvalidOperationException("初始化账号失败,请检查管理员密码要求。"); await users.AddToRoleAsync(user, role); credentials.Add(new(email, password)); return user; } private static string Password() => "Ear!" + Convert.ToHexString(RandomNumberGenerator.GetBytes(16)) + "a9"; }