SeedData.cs 21.6 KB
using Microsoft.EntityFrameworkCore;
using RobotProductionSystem.Api.Domain;
using RobotProductionSystem.Api.Services;

namespace RobotProductionSystem.Api.Infrastructure;

public static class SeedData
{
    public static async Task InitializeAsync(AppDbContext db, CancellationToken cancellationToken = default)
    {
        await SeedUsersAsync(db, cancellationToken);
        await SeedSettingsAsync(db, cancellationToken);
        await SeedDeviceTypesAsync(db, cancellationToken);
        await SeedWorkOrdersAsync(db, cancellationToken);
        await SeedSnAsync(db, cancellationToken);
        await SeedOperationsAsync(db, cancellationToken);
        await SeedDashboardAsync(db, cancellationToken);
        await SeedShellAsync(db, cancellationToken);
    }

    private static DateTimeOffset Now() => DateTimeOffset.UtcNow;
    private static string EventId(string prefix) => $"{prefix}-{Guid.NewGuid():N}";

    private static async Task SeedUsersAsync(AppDbContext db, CancellationToken ct)
    {
        if (await db.Users.AnyAsync(ct))
        {
            return;
        }

        db.Users.AddRange(
            new User
            {
                Id = 1001,
                Username = "admin",
                PasswordHash = PasswordHasher.Hash("123456"),
                Name = "系统管理员",
                Email = "admin@robot.local",
                Station = "总控台",
                AvatarSrc = "https://i.pravatar.cc/128?u=robot-admin",
                AvatarAlt = "系统管理员",
                Roles = [new UserRole { Role = "admin" }, new UserRole { Role = "qa_manager" }]
            },
            new User
            {
                Id = 1002,
                Username = "operator",
                PasswordHash = PasswordHasher.Hash("123456"),
                Name = "装配工位操作员",
                Email = "operator@robot.local",
                Station = "装配工位 A01",
                AvatarSrc = "https://i.pravatar.cc/128?u=robot-operator",
                AvatarAlt = "装配工位操作员",
                Roles = [new UserRole { Role = "operator" }]
            });

        await db.SaveChangesAsync(ct);
    }

    private static async Task SeedSettingsAsync(AppDbContext db, CancellationToken ct)
    {
        if (!await db.SettingsProfiles.AnyAsync(ct))
        {
            db.SettingsProfiles.Add(new SettingsProfile { Id = 1, Name = "Benjamin Canac", Email = "ben@nuxtlabs.com", Username = "benjamincanac" });
        }

        if (!await db.SettingsNotifications.AnyAsync(ct))
        {
            db.SettingsNotifications.Add(new SettingsNotification { Id = 1, Email = true, Desktop = false, ProductUpdates = true, WeeklyDigest = false, ImportantUpdates = true });
        }

        if (!await db.SecurityAccounts.AnyAsync(ct))
        {
            db.SecurityAccounts.Add(new SecurityAccount { Id = 1, PasswordHash = PasswordHasher.Hash("12345678"), AccountDeleted = false });
        }

        if (!await db.Members.AnyAsync(ct))
        {
            db.Members.AddRange(
                Member(1, "Anthony Fu", "antfu@robot.local", "antfu", "member", "Assembly line member", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/antfu"),
                Member(2, "Baptiste Leproux", "larbish@robot.local", "larbish", "member", "Quality check support", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/larbish"),
                Member(3, "Benjamin Canac", "ben@nuxtlabs.com", "benjamincanac", "admin", "System admin", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/benjamincanac"),
                Member(4, "Celine Dumerc", "celinedumerc@robot.local", "celinedumerc", "customer", "Customer representative", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/celinedumerc"),
                Member(5, "Daniel Roe", "danielroe@robot.local", "danielroe", "member", "Production operator", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/danielroe"),
                Member(6, "Hugo Richard", "hugorcd@robot.local", "hugorcd", "admin", "Operation supervisor", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/hugorcd"));
        }

        await db.SaveChangesAsync(ct);
    }

    private static Member Member(int id, string name, string email, string username, string role, string bio, string avatar) =>
        new() { Id = id, Name = name, Email = email, Username = username, Role = role, Bio = bio, AvatarSrc = avatar, AvatarAlt = name };

    private static async Task SeedDeviceTypesAsync(AppDbContext db, CancellationToken ct)
    {
        if (await db.DeviceTypes.AnyAsync(ct))
        {
            return;
        }

        var now = Now();
        db.DeviceTypes.AddRange(
            new DeviceType { Id = 1001, Name = "协作机械臂控制柜", Model = "RPS-CB-500", Category = "controller", LengthMm = 680, WidthMm = 520, HeightMm = 1480, WeightKg = 92, HasBattery = true, BatterySpec = "24V 15Ah 锂电", Description = "用于协作机械臂整机控制与供电管理。", UpdatedAt = now },
            new DeviceType { Id = 1002, Name = "末端视觉模组", Model = "RPS-VM-220", Category = "vision", LengthMm = 210, WidthMm = 135, HeightMm = 118, WeightKg = 2.8m, HasBattery = false, Description = "安装于末端执行器位置,用于识别与定位。", UpdatedAt = now },
            new DeviceType { Id = 1003, Name = "底盘驱动单元", Model = "RPS-CH-920", Category = "chassis", LengthMm = 1020, WidthMm = 740, HeightMm = 360, WeightKg = 118, HasBattery = true, BatterySpec = "48V 40Ah 磷酸铁锂", Description = "AGV 底盘驱动与转向一体化单元。", UpdatedAt = now },
            new DeviceType { Id = 1004, Name = "工业网关", Model = "RPS-GW-110", Category = "communication", LengthMm = 280, WidthMm = 180, HeightMm = 86, WeightKg = 3.2m, HasBattery = false, Description = "用于产线网络与设备数据转发。", UpdatedAt = now });

        await db.SaveChangesAsync(ct);
    }

    private static async Task SeedWorkOrdersAsync(AppDbContext db, CancellationToken ct)
    {
        if (await db.WorkOrders.AnyAsync(ct))
        {
            return;
        }

        db.WorkOrders.AddRange(
            WorkOrder(4001, "WO-202605-001", "RPS-CB-500", "BATCH-202605-A", "装配一线", "danielroe", "Daniel Roe", "2026-05-20", "draft", 0, 50, "系统管理员"),
            WorkOrder(4002, "WO-202605-002", "RPS-VM-220", "BATCH-202605-B", "装配二线", "antfu", "Anthony Fu", "2026-05-21", "pending_dispatch", 10, 80, "系统管理员"),
            WorkOrder(4003, "WO-202605-003", "RPS-CH-920", "BATCH-202605-C", "测试一线", "larbish", "Baptiste Leproux", "2026-05-22", "running", 28, 100, "系统管理员"),
            WorkOrder(4004, "WO-202605-004", "RPS-GW-110", "BATCH-202605-D", "质检线", "hugorcd", "Hugo Richard", "2026-05-18", "pending_qc", 40, 40, "质量经理"),
            WorkOrder(4005, "WO-202605-005", "RPS-CB-500", "BATCH-202605-E", "装配三线", "benjamincanac", "Benjamin Canac", "2026-05-16", "closed", 60, 60, "系统管理员"));

        await db.SaveChangesAsync(ct);
    }

    private static WorkOrder WorkOrder(int id, string orderNo, string deviceCode, string batchNo, string line, string ownerUsername, string ownerName, string plannedDate, string status, int completedSn, int totalSn, string by)
    {
        var createdAt = Now();
        var initial = new WorkOrderEvent { EventId = EventId("wo-evt"), Action = "create", FromStatus = null, ToStatus = "draft", Operator = by, At = createdAt, Remark = "初始化工单" };
        var events = new List<WorkOrderEvent> { initial };
        var lastAction = "create";
        var lastAt = createdAt;
        if (status != "draft")
        {
            lastAction = "dispatch";
            lastAt = createdAt.AddMilliseconds(1);
            events.Insert(0, new WorkOrderEvent { EventId = EventId("wo-evt"), Action = "dispatch", FromStatus = "draft", ToStatus = status, Operator = by, At = lastAt, Remark = "初始状态同步" });
        }

        return new WorkOrder { Id = id, OrderNo = orderNo, DeviceCode = deviceCode, BatchNo = batchNo, Line = line, OwnerUsername = ownerUsername, OwnerName = ownerName, PlannedDate = DateOnly.Parse(plannedDate), Status = status, CompletedSn = completedSn, TotalSn = totalSn, CreatedBy = by, CreatedAt = createdAt, UpdatedBy = by, UpdatedAt = lastAt, LastAction = lastAction, LastActionAt = lastAt, LastActionBy = by, Events = events };
    }

    private static async Task SeedSnAsync(AppDbContext db, CancellationToken ct)
    {
        if (await db.SnItems.AnyAsync(ct))
        {
            return;
        }

        db.SnItems.AddRange(
            Sn(1001, "SN-RBTX5-0001", "WO-202605-001", "in_process", ProcessStep.Assembly, "none", "系统管理员"),
            Sn(1002, "SN-RBTX5-0002", "WO-202605-001", "frozen", ProcessStep.Testing, "open", "质量经理", freeze: "测试电流波动超阈值"),
            Sn(1003, "SN-RBTM2-0001", "WO-202605-003", "completed", ProcessStep.FinalInspection, "closed", "系统管理员"),
            Sn(1004, "SN-RBTM2-0002", "WO-202605-003", "pending", ProcessStep.Assembly, "none", "系统管理员"),
            Sn(1005, "SN-RBTQA-0001", "WO-202605-004", "scrapped", ProcessStep.Rework, "rework", "质量经理", scrap: "主控板损坏无法修复"),
            Sn(1006, "SN-RBTX5-0003", "WO-202605-002", "in_process", ProcessStep.Testing, "none", "系统管理员"),
            Sn(1007, "SN-RBTX5-0004", "WO-202605-002", "frozen", ProcessStep.Testing, "rework", "质量经理", freeze: "重复扫码冲突待复核"),
            Sn(1008, "SN-RBTX5-0005", "WO-202605-002", "completed", ProcessStep.Warehousing, "none", "系统管理员"),
            Sn(1009, "SN-RBTQX-0001", "WO-202605-005", "scrapped", ProcessStep.FinalInspection, "closed", "系统管理员", scrap: "外壳结构缺陷"),
            Sn(1010, "SN-RBTQX-0002", "WO-202605-005", "pending", ProcessStep.Assembly, "none", "系统管理员"),
            Sn(1011, "SN-RBTX5-0006", "WO-202605-001", "in_process", ProcessStep.Debug, "none", "系统管理员"),
            Sn(1012, "SN-RBTM2-0003", "WO-202605-003", "frozen", ProcessStep.Rework, "open", "质量经理", freeze: "异常单 EX-202605-009 未关闭"),
            Sn(1013, "SN-RBTM2-0004", "WO-202605-003", "completed", ProcessStep.Warehousing, "none", "系统管理员"),
            Sn(1014, "SN-RBTQA-0002", "WO-202605-004", "pending", ProcessStep.Assembly, "none", "系统管理员"),
            Sn(1015, "SN-RBTQA-0003", "WO-202605-004", "in_process", ProcessStep.Testing, "open", "质量经理"),
            Sn(1016, "SN-RBTQA-0004", "WO-202605-004", "completed", ProcessStep.FinalInspection, "none", "系统管理员"),
            Sn(1017, "SN-RBTX5-0007", "WO-202605-001", "frozen", ProcessStep.Debug, "rework", "质量经理", freeze: "电机抖动异常待返修"),
            Sn(1018, "SN-RBTX5-0008", "WO-202605-001", "pending", ProcessStep.Assembly, "none", "系统管理员"),
            Sn(1019, "SN-RBTQX-0003", "WO-202605-005", "in_process", ProcessStep.Testing, "none", "系统管理员"),
            Sn(1020, "SN-RBTQX-0004", "WO-202605-005", "completed", ProcessStep.Warehousing, "closed", "系统管理员"));

        await db.SaveChangesAsync(ct);
    }

    private static SnItem Sn(int id, string sn, string wo, string status, ProcessStep step, string exception, string by, string? freeze = null, string? scrap = null)
    {
        var at = Now();
        var item = new SnItem
        {
            Id = id,
            Sn = sn,
            WorkOrderNo = wo,
            Status = status,
            ExceptionStatus = exception,
            FreezeReason = freeze,
            ScrapReason = scrap,
            CreatedBy = by,
            CreatedAt = at,
            UpdatedBy = by,
            UpdatedAt = at,
            LastAction = "import",
            LastActionAt = at,
            LastActionBy = by
        };
        if (!item.TryInitializeCurrentStep(step, out var error))
        {
            throw new InvalidOperationException($"初始化 SN 工序失败: {error}");
        }

        var importEvent = new SnEvent
        {
            EventId = EventId("sn-evt"),
            Action = "import",
            FromStatus = null,
            ToStatus = status,
            Operator = by,
            At = at,
            Reason = "初始化导入",
            ExceptionStatus = exception
        };
        importEvent.CaptureCurrentStep(item.CurrentStep);
        item.Events = [importEvent];
        return item;
    }

    private static async Task SeedOperationsAsync(AppDbContext db, CancellationToken ct)
    {
        if (await db.OperationTasks.AnyAsync(ct))
        {
            return;
        }

        db.OperationTasks.AddRange(
            Op(9001, "WO-202605-001", "SN-RBTX5-0001", ProcessStep.Assembly, "A01", "ASM-JIG-01", "装配工位操作员", "pending", "pending"),
            Op(9002, "WO-202605-001", "SN-RBTX5-0002", ProcessStep.Assembly, "A02", "ASM-JIG-02", "装配工位操作员", "in_progress", "pending", "2026-05-18T07:15:00.000Z"),
            Op(9003, "WO-202605-002", "SN-RBTX5-0003", ProcessStep.Testing, "T01", "TEST-BENCH-07", "质量经理", "pending_test", "pending", "2026-05-18T06:45:00.000Z"),
            Op(9004, "WO-202605-003", "SN-RBTM2-0001", ProcessStep.FinalInspection, "Q01", "QC-STATION-03", "质量经理", "completed", "pass", "2026-05-18T05:00:00.000Z", "2026-05-18T05:30:00.000Z"),
            Op(9005, "WO-202605-004", "SN-RBTQA-0001", ProcessStep.Testing, "T02", "TEST-BENCH-11", "质量经理", "failed", "fail", "2026-05-18T04:10:00.000Z", "2026-05-18T04:20:00.000Z"),
            Op(9006, "WO-202605-004", "SN-RBTQA-0002", ProcessStep.Rework, "R01", "REPAIR-DESK-02", "装配工位操作员", "rework", "pending", "2026-05-18T03:00:00.000Z"),
            Op(9007, "WO-202605-005", "SN-RBTQX-0003", ProcessStep.Assembly, "A03", "ASM-JIG-05", "装配工位操作员", "skipped", "pending"),
            Op(9008, "WO-202605-003", "SN-RBTM2-0003", ProcessStep.Testing, "T01", "TEST-BENCH-07", "质量经理", "pending_test", "pending", "2026-05-18T02:10:00.000Z"));

        await db.SaveChangesAsync(ct);
    }

    private static OperationTask Op(int id, string wo, string sn, ProcessStep step, string station, string device, string op, string status, string result, string? started = null, string? ended = null)
    {
        var at = Now();
        var next = Rules.OperationNextAction[status];
        return new OperationTask { Id = id, WorkOrderNo = wo, Sn = sn, StepName = step, Workstation = station, Device = device, Operator = op, Status = status, Result = result, StartedAt = started is null ? null : DateTimeOffset.Parse(started), EndedAt = ended is null ? null : DateTimeOffset.Parse(ended), NextAction = next, AuditEvents = [new OperationTaskEvent { EventId = EventId("op-evt"), Action = "start", FromStatus = null, ToStatus = status, Operator = op, At = at, Remark = "任务初始化", NextAction = next }] };
    }

    private static async Task SeedDashboardAsync(AppDbContext db, CancellationToken ct)
    {
        if (await db.DashboardWorkOrderSnapshots.AnyAsync(ct))
        {
            return;
        }

        (string orderNo, string line, string status, string date, int completed, int total, int inProcess, int frozen, int firstPassed, int firstTotal, int retestPassed, int retestTotal, int exceptions)[] rows =
        [
            ("WO-202605-001", "L1-装配线", "running", "2026-05-17", 82, 120, 31, 7, 70, 82, 8, 12, 9),
            ("WO-202605-002", "L2-测试线", "pending_qc", "2026-05-17", 56, 56, 0, 0, 49, 56, 4, 6, 3),
            ("WO-202605-003", "L3-总装线", "in_exception", "2026-05-16", 35, 90, 43, 12, 28, 35, 3, 5, 14),
            ("WO-202605-004", "L1-装配线", "completed", "2026-05-16", 130, 130, 0, 0, 120, 130, 7, 9, 2),
            ("WO-202605-005", "L2-测试线", "pending_dispatch", "2026-05-15", 0, 75, 75, 0, 0, 0, 0, 0, 0),
            ("WO-202605-006", "L3-总装线", "running", "2026-05-15", 46, 88, 39, 3, 40, 46, 3, 4, 5),
            ("WO-202605-007", "L1-装配线", "pending_qc", "2026-05-14", 64, 64, 0, 0, 57, 64, 5, 7, 4),
            ("WO-202605-008", "L2-测试线", "completed", "2026-05-13", 108, 108, 0, 0, 100, 108, 6, 7, 2),
            ("WO-202605-009", "L3-总装线", "running", "2026-05-12", 52, 100, 42, 6, 45, 52, 4, 6, 7),
            ("WO-202605-010", "L1-装配线", "in_exception", "2026-05-11", 28, 72, 33, 11, 21, 28, 2, 4, 12),
            ("WO-202605-011", "L2-测试线", "running", "2026-05-10", 61, 92, 26, 5, 54, 61, 4, 6, 6),
            ("WO-202605-012", "L3-总装线", "pending_dispatch", "2026-05-09", 0, 80, 80, 0, 0, 0, 0, 0, 0)
        ];

        var id = 1;
        db.DashboardWorkOrderSnapshots.AddRange(rows.Select(x => new DashboardWorkOrderSnapshot { Id = id++, OrderNo = x.orderNo, Line = x.line, Status = x.status, PlannedDate = DateOnly.Parse(x.date), CompletedSn = x.completed, TotalSn = x.total, InProcessSn = x.inProcess, FrozenSn = x.frozen, FirstTestPassed = x.firstPassed, FirstTestTotal = x.firstTotal, RetestPassed = x.retestPassed, RetestTotal = x.retestTotal, ExceptionCount = x.exceptions }));
        await db.SaveChangesAsync(ct);
    }

    private static async Task SeedShellAsync(AppDbContext db, CancellationToken ct)
    {
        await UpsertShellItem(db, "customers", """[{"id":1,"name":"Alex Smith","email":"alex.smith@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=1"},"status":"subscribed","location":"New York, USA"},{"id":2,"name":"Jordan Brown","email":"jordan.brown@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=2"},"status":"unsubscribed","location":"London, UK"},{"id":3,"name":"Taylor Green","email":"taylor.green@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=3"},"status":"bounced","location":"Paris, France"},{"id":4,"name":"Morgan White","email":"morgan.white@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=4"},"status":"subscribed","location":"Berlin, Germany"},{"id":5,"name":"Casey Gray","email":"casey.gray@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=5"},"status":"subscribed","location":"Tokyo, Japan"},{"id":6,"name":"Jamie Johnson","email":"jamie.johnson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=6"},"status":"subscribed","location":"Sydney, Australia"},{"id":7,"name":"Riley Davis","email":"riley.davis@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=7"},"status":"subscribed","location":"New York, USA"},{"id":8,"name":"Kelly Wilson","email":"kelly.wilson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=8"},"status":"subscribed","location":"London, UK"},{"id":9,"name":"Drew Moore","email":"drew.moore@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=9"},"status":"bounced","location":"Paris, France"},{"id":10,"name":"Jordan Taylor","email":"jordan.taylor@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=10"},"status":"subscribed","location":"Berlin, Germany"},{"id":11,"name":"Morgan Anderson","email":"morgan.anderson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=11"},"status":"subscribed","location":"Tokyo, Japan"},{"id":12,"name":"Casey Thomas","email":"casey.thomas@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=12"},"status":"unsubscribed","location":"Sydney, Australia"},{"id":13,"name":"Jamie Jackson","email":"jamie.jackson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=13"},"status":"unsubscribed","location":"New York, USA"},{"id":14,"name":"Riley White","email":"riley.white@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=14"},"status":"unsubscribed","location":"London, UK"},{"id":15,"name":"Kelly Harris","email":"kelly.harris@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=15"},"status":"subscribed","location":"Paris, France"},{"id":16,"name":"Drew Martin","email":"drew.martin@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=16"},"status":"subscribed","location":"Berlin, Germany"},{"id":17,"name":"Alex Thompson","email":"alex.thompson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=17"},"status":"unsubscribed","location":"Tokyo, Japan"},{"id":18,"name":"Jordan Garcia","email":"jordan.garcia@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=18"},"status":"subscribed","location":"Sydney, Australia"},{"id":19,"name":"Taylor Rodriguez","email":"taylor.rodriguez@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=19"},"status":"bounced","location":"New York, USA"},{"id":20,"name":"Morgan Lopez","email":"morgan.lopez@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=20"},"status":"subscribed","location":"London, UK"}]""", ct);
        await UpsertShellItem(db, "mails", """[{"id":1,"from":{"name":"Alex Smith","email":"alex.smith@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=1"}},"subject":"Meeting Schedule: Q1 Marketing Strategy Review","body":"Production planning meeting reminder.","date":"2026-05-18T00:00:00.000Z"},{"id":2,"unread":true,"from":{"name":"Jordan Brown","email":"jordan.brown@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=2"}},"subject":"Project Phoenix - Sprint 3 Update","body":"Quick update on sprint deliverables.","date":"2026-05-17T23:53:00.000Z"}]""", ct);
        await UpsertShellItem(db, "notifications", """[{"id":1,"unread":true,"sender":{"name":"Jordan Brown","email":"jordan.brown@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=2"}},"body":"sent you a message","date":"2026-05-17T23:53:00.000Z"},{"id":2,"sender":{"name":"Lindsay Walton"},"body":"subscribed to your email list","date":"2026-05-17T23:00:00.000Z"}]""", ct);

        await db.SaveChangesAsync(ct);
    }

    private static async Task UpsertShellItem(AppDbContext db, string kind, string payloadJson, CancellationToken ct)
    {
        var item = await db.ShellJsonItems.SingleOrDefaultAsync(x => x.Kind == kind, ct);
        if (item is null)
        {
            db.ShellJsonItems.Add(new ShellJsonItem { Kind = kind, PayloadJson = payloadJson });
            return;
        }

        item.PayloadJson = payloadJson;
    }
}