From 1f72488a8d155cb2c2a0e9503506f0e5a1b3e499 Mon Sep 17 00:00:00 2001 From: ArtoriasWu Date: Mon, 27 Jul 2026 15:12:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=BD=A6=E8=BE=86=E8=A1=A8?= =?UTF-8?q?=E5=8F=8AOTA=E6=9D=83=E9=99=90=E4=B8=8E=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增车辆任务与报警表,完善实体与DbContext配置。细化OTA权限校验,增强回传会话IP安全。优化OTA上传与设置面板,调度器支持重启恢复。报警采集逻辑支持历史分段。 --- .../Domains}/Fleet/CdmTaskRecord.cs | 5 +- MiGu.DB/Domains/Fleet/FleetConfigurations.cs | 61 + .../Domains}/Fleet/VehicleAlarmRecord.cs | 6 +- MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs | 3 + .../20260727070415_AddFleetTables.Designer.cs | 1882 +++++++++++++++++ .../Sqlite/20260727070415_AddFleetTables.cs | 103 + .../Sqlite/MiGuDbContextModelSnapshot.cs | 196 ++ MiGu.Server/Controllers/OtaController.cs | 31 +- .../Controllers/OtaReceiveController.cs | 23 +- MiGu.Server/Controllers/RbacController.cs | 2 + MiGu.Server/Fleet/AlarmCollector.cs | 41 +- MiGu.Server/GlobalUsings.Db.cs | 2 + MiGu.Server/Ota/OtaJobRunner.cs | 29 + MiGu.Server/Ota/OtaModels.cs | 4 +- MiGu.Server/Ota/OtaStore.cs | 31 +- MiGu.Server/Program.cs | 8 + .../apps/simple-platform-vue/src/api/http.ts | 4 + .../apps/simple-platform-vue/src/api/ota.ts | 2 - .../src/views/shared/VehicleHubView.vue | 2 +- .../src/views/shared/ota/OtaSettingsPane.vue | 10 +- 20 files changed, 2392 insertions(+), 53 deletions(-) rename {MiGu.Server => MiGu.DB/Domains}/Fleet/CdmTaskRecord.cs (92%) create mode 100644 MiGu.DB/Domains/Fleet/FleetConfigurations.cs rename {MiGu.Server => MiGu.DB/Domains}/Fleet/VehicleAlarmRecord.cs (74%) create mode 100644 MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.Designer.cs create mode 100644 MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.cs diff --git a/MiGu.Server/Fleet/CdmTaskRecord.cs b/MiGu.DB/Domains/Fleet/CdmTaskRecord.cs similarity index 92% rename from MiGu.Server/Fleet/CdmTaskRecord.cs rename to MiGu.DB/Domains/Fleet/CdmTaskRecord.cs index 69a5973..00097dd 100644 --- a/MiGu.Server/Fleet/CdmTaskRecord.cs +++ b/MiGu.DB/Domains/Fleet/CdmTaskRecord.cs @@ -1,9 +1,8 @@ -namespace MiGu.Server.Fleet; +namespace MiGu.DB.Domains.Fleet; /// /// CDM 搬运任务的平台侧快照(表 cdm_tasks)。 -/// 以任务 Id 为主键;SimpleLite/StandardScene 把终态任务从自身 JSON 里删除,这里则永久保留=完整历史, -/// 且 SimpleLite 关闭后平台仍可从本表读取最近快照。 +/// 以任务 Id 为主键;SimpleLite/StandardScene 把终态任务从自身 JSON 里删除,这里则永久保留=完整历史。 /// public sealed class CdmTaskRecord { diff --git a/MiGu.DB/Domains/Fleet/FleetConfigurations.cs b/MiGu.DB/Domains/Fleet/FleetConfigurations.cs new file mode 100644 index 0000000..e70c8ad --- /dev/null +++ b/MiGu.DB/Domains/Fleet/FleetConfigurations.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace MiGu.DB.Domains.Fleet; + +public sealed class CdmTaskRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("cdm_tasks"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id").HasMaxLength(64); + e.Property(x => x.TaskId).HasColumnName("task_id").HasMaxLength(128).IsRequired(false); + e.Property(x => x.MissionId).HasColumnName("mission_id"); + e.Property(x => x.MissionName).HasColumnName("mission_name").HasMaxLength(128); + e.Property(x => x.MissionTypeName).HasColumnName("mission_type").HasMaxLength(128); + e.Property(x => x.SrcSiteId).HasColumnName("src_site_id"); + e.Property(x => x.SrcLabel).HasColumnName("src_label").HasMaxLength(256); + e.Property(x => x.DstSiteId).HasColumnName("dst_site_id"); + e.Property(x => x.DstLabel).HasColumnName("dst_label").HasMaxLength(256); + e.Property(x => x.Status).HasColumnName("status").HasMaxLength(32); + e.Property(x => x.StatusCode).HasColumnName("status_code").HasMaxLength(32); + e.Property(x => x.CarId).HasColumnName("car_id").IsRequired(false); + e.Property(x => x.CarName).HasColumnName("car_name").HasMaxLength(128).IsRequired(false); + e.Property(x => x.Priority).HasColumnName("priority"); + e.Property(x => x.CreateTime).HasColumnName("create_time").HasMaxLength(40).IsRequired(false); + e.Property(x => x.StartTime).HasColumnName("start_time").HasMaxLength(40).IsRequired(false); + e.Property(x => x.FinishTime).HasColumnName("finish_time").HasMaxLength(40).IsRequired(false); + e.Property(x => x.StuckReason).HasColumnName("stuck_reason").HasMaxLength(512).IsRequired(false); + e.Property(x => x.Overdue).HasColumnName("overdue"); + e.Property(x => x.FirstSeenAt).HasColumnName("first_seen_at"); + e.Property(x => x.LastSeenAt).HasColumnName("last_seen_at"); + e.HasIndex(x => x.StatusCode); + e.HasIndex(x => x.CreateTime); + } +} + +public sealed class VehicleAlarmRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("vehicle_alarms"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id").HasMaxLength(36); + e.Property(x => x.CarId).HasColumnName("car_id"); + e.Property(x => x.CarName).HasColumnName("car_name").HasMaxLength(128); + e.Property(x => x.Info).HasColumnName("info").HasColumnType("text"); + e.Property(x => x.Level).HasColumnName("level"); + e.Property(x => x.Status).HasColumnName("status").HasMaxLength(16); + e.Property(x => x.FirstAt).HasColumnName("first_at"); + e.Property(x => x.LastAt).HasColumnName("last_at"); + e.Property(x => x.ResolvedAt).HasColumnName("resolved_at").IsRequired(false); + e.Property(x => x.DurationSecs).HasColumnName("duration_secs").IsRequired(false); + e.Property(x => x.Acknowledged).HasColumnName("acknowledged"); + e.Property(x => x.AcknowledgedAt).HasColumnName("acknowledged_at").IsRequired(false); + e.Property(x => x.AcknowledgedBy).HasColumnName("acknowledged_by").HasMaxLength(128).IsRequired(false); + e.HasIndex(x => new { x.CarId, x.Status }); + e.HasIndex(x => x.Status); + e.HasIndex(x => x.FirstAt); + } +} diff --git a/MiGu.Server/Fleet/VehicleAlarmRecord.cs b/MiGu.DB/Domains/Fleet/VehicleAlarmRecord.cs similarity index 74% rename from MiGu.Server/Fleet/VehicleAlarmRecord.cs rename to MiGu.DB/Domains/Fleet/VehicleAlarmRecord.cs index a24ad3f..e9a0f00 100644 --- a/MiGu.Server/Fleet/VehicleAlarmRecord.cs +++ b/MiGu.DB/Domains/Fleet/VehicleAlarmRecord.cs @@ -1,10 +1,8 @@ -namespace MiGu.Server.Fleet; +namespace MiGu.DB.Domains.Fleet; /// /// 车辆报警的平台侧记录(表 vehicle_alarms)。 -/// SimpleLite 只在 SSE/状态里给出「当前是否报警 + 文案」,无历史;平台按车对帐: -/// 出现报警→开一条 active 记录,报警文案变化→更新,报警消失→置为 cleared 并记录恢复时间/持续时长。 -/// 永不删除=完整历史,重启/刷新不丢,SimpleLite 离线也可查。 +/// 出现报警→开一条 active 记录;文案/级别变化→先 clear 再建新 active;消失→cleared。 /// public sealed class VehicleAlarmRecord { diff --git a/MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs b/MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs index 5b44021..f938089 100644 --- a/MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs +++ b/MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using MiGu.DB.Domains.Dashboard; +using MiGu.DB.Domains.Fleet; using MiGu.DB.Domains.SimpleFields; using MiGu.DB.Domains.Transport; using MiGu.DB.Domains.Wms; @@ -25,4 +26,6 @@ public partial class MiGuDbContext public DbSet WmsTransportTaskHistories => Set(); public DbSet SimpleFields => Set(); public DbSet UserDashboardShortcuts => Set(); + public DbSet CdmTasks => Set(); + public DbSet VehicleAlarms => Set(); } diff --git a/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.Designer.cs b/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.Designer.cs new file mode 100644 index 0000000..628dd18 --- /dev/null +++ b/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.Designer.cs @@ -0,0 +1,1882 @@ +// +using System; +using MiGu.DB.Kernel.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MiGu.DB.Migrations.Sqlite +{ + [DbContext(typeof(MiGuDbContext))] + [Migration("20260727070415_AddFleetTables")] + partial class AddFleetTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("MiGu.DB.Domains.Dashboard.UserDashboardShortcut", b => + { + b.Property("UserId") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Scope") + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("scope"); + + b.Property("KeysJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("keys_json"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId", "Scope"); + + b.ToTable("user_dashboard_shortcuts", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Fleet.CdmTaskRecord", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CarId") + .HasColumnType("INTEGER") + .HasColumnName("car_id"); + + b.Property("CarName") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("car_name"); + + b.Property("CreateTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("DstLabel") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("dst_label"); + + b.Property("DstSiteId") + .HasColumnType("INTEGER") + .HasColumnName("dst_site_id"); + + b.Property("FinishTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("finish_time"); + + b.Property("FirstSeenAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("first_seen_at"); + + b.Property("LastSeenAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("last_seen_at"); + + b.Property("MissionId") + .HasColumnType("INTEGER") + .HasColumnName("mission_id"); + + b.Property("MissionName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("mission_name"); + + b.Property("MissionTypeName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("mission_type"); + + b.Property("Overdue") + .HasColumnType("INTEGER") + .HasColumnName("overdue"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("SrcLabel") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("src_label"); + + b.Property("SrcSiteId") + .HasColumnType("INTEGER") + .HasColumnName("src_site_id"); + + b.Property("StartTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("start_time"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("status_code"); + + b.Property("StuckReason") + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("stuck_reason"); + + b.Property("TaskId") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.HasKey("Id"); + + b.HasIndex("CreateTime"); + + b.HasIndex("StatusCode"); + + b.ToTable("cdm_tasks", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Fleet.VehicleAlarmRecord", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Acknowledged") + .HasColumnType("INTEGER") + .HasColumnName("acknowledged"); + + b.Property("AcknowledgedAt") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("acknowledged_at"); + + b.Property("AcknowledgedBy") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("acknowledged_by"); + + b.Property("CarId") + .HasColumnType("INTEGER") + .HasColumnName("car_id"); + + b.Property("CarName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("car_name"); + + b.Property("DurationSecs") + .HasColumnType("INTEGER") + .HasColumnName("duration_secs"); + + b.Property("FirstAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("first_at"); + + b.Property("Info") + .IsRequired() + .HasColumnType("text") + .HasColumnName("info"); + + b.Property("LastAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("last_at"); + + b.Property("Level") + .HasColumnType("INTEGER") + .HasColumnName("level"); + + b.Property("ResolvedAt") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("FirstAt"); + + b.HasIndex("Status"); + + b.HasIndex("CarId", "Status"); + + b.ToTable("vehicle_alarms", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.SimpleFields.SimpleField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CarType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("car_type"); + + b.Property("Chinese") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("chinese"); + + b.Property("CreateTime") + .IsRequired() + .HasMaxLength(19) + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("data_type"); + + b.Property("English") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("english"); + + b.Property("FieldType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("field_type"); + + b.Property("IsDefault") + .HasColumnType("INTEGER") + .HasColumnName("is_default"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("Other") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("other"); + + b.Property("UpdateTime") + .IsRequired() + .HasMaxLength(19) + .HasColumnType("TEXT") + .HasColumnName("update_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("value"); + + b.HasKey("Id"); + + b.HasIndex("CarType", "FieldType", "Key") + .IsUnique(); + + b.ToTable("simple_fields", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SourceStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("TargetStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId", "Status"); + + b.HasIndex("TargetStorageId", "Status"); + + b.ToTable("wms_transport_reservations", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SourceSelectorJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetSelectorJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskOptionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("TriggerType", "Enabled", "Priority"); + + b.ToTable("wms_transport_rules", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BusinessType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeliveryId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DispatchMissionId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DispatchStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RuleId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("TargetStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("TaskPriority") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("Status"); + + b.HasIndex("TargetStorageId"); + + b.ToTable("wms_transport_tasks", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportTaskHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OperatedAt"); + + b.HasIndex("TaskId"); + + b.ToTable("wms_transport_task_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Container", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AreaId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("REAL"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Length") + .HasColumnType("REAL"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_containers", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("EnteredAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LocationCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LocationName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId") + .IsUnique(); + + b.HasIndex("StorageId"); + + b.HasIndex("LocationType", "LocationId"); + + b.ToTable("wms_container_locations", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerLocationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AfterJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("BeforeJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("FromLocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RelationId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ToLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ToLocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_container_location_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerMaterial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("BoundAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LoadedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("MaterialId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SerialNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UnloadedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("MaterialId") + .IsUnique(); + + b.ToTable("wms_container_materials", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerMaterialHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AfterJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("BeforeJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("QuantityDelta") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RelationId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_container_material_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Material", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LifecycleStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Spec") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TypeCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UnboundAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Barcode"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("TypeCode"); + + b.HasIndex("LifecycleStatus", "UpdatedAt"); + + b.ToTable("wms_materials", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.MaterialType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BarcodePrefix") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Spec") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_material_types", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.StockEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AreaCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("FromStorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("FromStorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("FromStorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialBarcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("MaterialName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialTypeCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RefCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RefId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("RefType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("StorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ToStorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ToStorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ToStorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("MaterialId"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_stock_events", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Storage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AllowInbound") + .HasColumnType("INTEGER"); + + b.Property("AllowOutbound") + .HasColumnType("INTEGER"); + + b.Property("AreaId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Capacity") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ColumnNo") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DepthNo") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LevelNo") + .HasColumnType("INTEGER"); + + b.Property("LocationKind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SiteCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SiteId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("StorageType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ZoneCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("Barcode"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_storages", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_warehouses", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.WarehouseArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LayoutMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("WarehouseId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("WarehouseId"); + + b.ToTable("wms_areas", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.cs b/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.cs new file mode 100644 index 0000000..32e050a --- /dev/null +++ b/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.cs @@ -0,0 +1,103 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MiGu.DB.Migrations.Sqlite +{ + /// + public partial class AddFleetTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "cdm_tasks", + columns: table => new + { + id = table.Column(type: "TEXT", maxLength: 64, nullable: false), + task_id = table.Column(type: "TEXT", maxLength: 128, nullable: true), + mission_id = table.Column(type: "INTEGER", nullable: false), + mission_name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + mission_type = table.Column(type: "TEXT", maxLength: 128, nullable: false), + src_site_id = table.Column(type: "INTEGER", nullable: false), + src_label = table.Column(type: "TEXT", maxLength: 256, nullable: false), + dst_site_id = table.Column(type: "INTEGER", nullable: false), + dst_label = table.Column(type: "TEXT", maxLength: 256, nullable: false), + status = table.Column(type: "TEXT", maxLength: 32, nullable: false), + status_code = table.Column(type: "TEXT", maxLength: 32, nullable: false), + car_id = table.Column(type: "INTEGER", nullable: true), + car_name = table.Column(type: "TEXT", maxLength: 128, nullable: true), + priority = table.Column(type: "INTEGER", nullable: false), + create_time = table.Column(type: "TEXT", maxLength: 40, nullable: true), + start_time = table.Column(type: "TEXT", maxLength: 40, nullable: true), + finish_time = table.Column(type: "TEXT", maxLength: 40, nullable: true), + stuck_reason = table.Column(type: "TEXT", maxLength: 512, nullable: true), + overdue = table.Column(type: "INTEGER", nullable: false), + first_seen_at = table.Column(type: "TEXT", maxLength: 40, nullable: false), + last_seen_at = table.Column(type: "TEXT", maxLength: 40, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cdm_tasks", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "vehicle_alarms", + columns: table => new + { + id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + car_id = table.Column(type: "INTEGER", nullable: false), + car_name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + info = table.Column(type: "text", nullable: false), + level = table.Column(type: "INTEGER", nullable: false), + status = table.Column(type: "TEXT", maxLength: 16, nullable: false), + first_at = table.Column(type: "TEXT", maxLength: 40, nullable: false), + last_at = table.Column(type: "TEXT", maxLength: 40, nullable: false), + resolved_at = table.Column(type: "TEXT", maxLength: 40, nullable: true), + duration_secs = table.Column(type: "INTEGER", nullable: true), + acknowledged = table.Column(type: "INTEGER", nullable: false), + acknowledged_at = table.Column(type: "TEXT", maxLength: 40, nullable: true), + acknowledged_by = table.Column(type: "TEXT", maxLength: 128, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_vehicle_alarms", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "IX_cdm_tasks_create_time", + table: "cdm_tasks", + column: "create_time"); + + migrationBuilder.CreateIndex( + name: "IX_cdm_tasks_status_code", + table: "cdm_tasks", + column: "status_code"); + + migrationBuilder.CreateIndex( + name: "IX_vehicle_alarms_car_id_status", + table: "vehicle_alarms", + columns: new[] { "car_id", "status" }); + + migrationBuilder.CreateIndex( + name: "IX_vehicle_alarms_first_at", + table: "vehicle_alarms", + column: "first_at"); + + migrationBuilder.CreateIndex( + name: "IX_vehicle_alarms_status", + table: "vehicle_alarms", + column: "status"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "cdm_tasks"); + + migrationBuilder.DropTable( + name: "vehicle_alarms"); + } + } +} diff --git a/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs b/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs index 31568f3..6182c9f 100644 --- a/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs +++ b/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs @@ -45,6 +45,202 @@ namespace MiGu.DB.Migrations.Sqlite b.ToTable("user_dashboard_shortcuts", (string)null); }); + modelBuilder.Entity("MiGu.DB.Domains.Fleet.CdmTaskRecord", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CarId") + .HasColumnType("INTEGER") + .HasColumnName("car_id"); + + b.Property("CarName") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("car_name"); + + b.Property("CreateTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("DstLabel") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("dst_label"); + + b.Property("DstSiteId") + .HasColumnType("INTEGER") + .HasColumnName("dst_site_id"); + + b.Property("FinishTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("finish_time"); + + b.Property("FirstSeenAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("first_seen_at"); + + b.Property("LastSeenAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("last_seen_at"); + + b.Property("MissionId") + .HasColumnType("INTEGER") + .HasColumnName("mission_id"); + + b.Property("MissionName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("mission_name"); + + b.Property("MissionTypeName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("mission_type"); + + b.Property("Overdue") + .HasColumnType("INTEGER") + .HasColumnName("overdue"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("SrcLabel") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("src_label"); + + b.Property("SrcSiteId") + .HasColumnType("INTEGER") + .HasColumnName("src_site_id"); + + b.Property("StartTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("start_time"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("status_code"); + + b.Property("StuckReason") + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("stuck_reason"); + + b.Property("TaskId") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.HasKey("Id"); + + b.HasIndex("CreateTime"); + + b.HasIndex("StatusCode"); + + b.ToTable("cdm_tasks", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Fleet.VehicleAlarmRecord", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Acknowledged") + .HasColumnType("INTEGER") + .HasColumnName("acknowledged"); + + b.Property("AcknowledgedAt") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("acknowledged_at"); + + b.Property("AcknowledgedBy") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("acknowledged_by"); + + b.Property("CarId") + .HasColumnType("INTEGER") + .HasColumnName("car_id"); + + b.Property("CarName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("car_name"); + + b.Property("DurationSecs") + .HasColumnType("INTEGER") + .HasColumnName("duration_secs"); + + b.Property("FirstAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("first_at"); + + b.Property("Info") + .IsRequired() + .HasColumnType("text") + .HasColumnName("info"); + + b.Property("LastAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("last_at"); + + b.Property("Level") + .HasColumnType("INTEGER") + .HasColumnName("level"); + + b.Property("ResolvedAt") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("FirstAt"); + + b.HasIndex("Status"); + + b.HasIndex("CarId", "Status"); + + b.ToTable("vehicle_alarms", (string)null); + }); + modelBuilder.Entity("MiGu.DB.Domains.SimpleFields.SimpleField", b => { b.Property("Id") diff --git a/MiGu.Server/Controllers/OtaController.cs b/MiGu.Server/Controllers/OtaController.cs index f2f4185..04924bb 100644 --- a/MiGu.Server/Controllers/OtaController.cs +++ b/MiGu.Server/Controllers/OtaController.cs @@ -48,7 +48,10 @@ public class OtaController : ControllerBase if (string.Equals(Scope, "Platform", StringComparison.OrdinalIgnoreCase)) return true; var ops = User.FindFirst("ops")?.Value ?? ""; var set = ops.Split(' ', StringSplitOptions.RemoveEmptyEntries); - return set.Contains("*") || set.Any(o => o.StartsWith("ops.ota", StringComparison.OrdinalIgnoreCase)); + return set.Contains("*") + || set.Contains("ops.ota") + || set.Contains("ops.ota.write") + || set.Any(o => o.StartsWith("ops.ota.", StringComparison.OrdinalIgnoreCase)); } private bool DenyWrite(out ActionResult denied) @@ -147,12 +150,32 @@ public class OtaController : ControllerBase var receiveBase = $"{baseUrl.TrimEnd('/')}/api/ota/receive"; var time = DateTime.Now.ToString("yyyyMMddHHmmss"); await _wd.TriggerPullAsync(car.Ip, receiveBase, time, ct); - // 等待文件落盘 + // 等待文件落盘:绝对超时 + 收到文件后的空闲窗口,避免首个组件到达就清会话导致半包 await Task.Delay(1500, ct); - for (var i = 0; i < 40; i++) + var deadline = DateTime.UtcNow.AddSeconds(90); + var idleAfterReceive = TimeSpan.FromSeconds(8); + var lastCount = 0; + DateTime? lastProgressAt = null; + while (DateTime.UtcNow < deadline) { + ct.ThrowIfCancellationRequested(); var info = _store.ScanPackage(pkgId); - if (info.Components.Count > 0) break; + if (info.Components.Count > lastCount) + { + lastCount = info.Components.Count; + lastProgressAt = DateTime.UtcNow; + } + else if (_store.TryGetLastReceiveAt(car.Ip, out var recvAt) && + (lastProgressAt == null || recvAt > lastProgressAt.Value)) + { + lastProgressAt = recvAt; + } + + if (lastCount > 0 && + lastProgressAt != null && + DateTime.UtcNow - lastProgressAt.Value >= idleAfterReceive) + break; + await Task.Delay(500, ct); } _store.ClearActivePull(car.Ip); diff --git a/MiGu.Server/Controllers/OtaReceiveController.cs b/MiGu.Server/Controllers/OtaReceiveController.cs index 17a1d43..79dab2c 100644 --- a/MiGu.Server/Controllers/OtaReceiveController.cs +++ b/MiGu.Server/Controllers/OtaReceiveController.cs @@ -8,11 +8,14 @@ namespace MiGu.Server.Controllers; /// WatchDog 回传包接收端。 /// WatchDog 写死 POST 到 http://{config.serverIP}:8000/upload-mdcs/{routeKey}, /// 必须与参考 Electron Express :8000 路径一致;/api/ota/receive/* 仅作兼容别名。 +/// 会话校验使用 TCP 对端 IP(见 Program 中 TcpRemoteIp),忽略可伪造的 X-Forwarded-For。 /// [ApiController] [AllowAnonymous] public class OtaReceiveController : ControllerBase { + public const string TcpRemoteIpItemKey = "TcpRemoteIp"; + private readonly OtaStore _store; private readonly ILogger _log; @@ -40,15 +43,15 @@ public class OtaReceiveController : ControllerBase [RequestSizeLimit(512_000_000)] public async Task UploadHistory(string routeKey, CancellationToken ct) { - var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + var ip = ResolveTcpRemoteIp(); if (!_store.TryGetActivePullId(ip, out _)) { - _log.LogWarning("OTA history rejected without active pull session from {Ip}", ip); + _log.LogWarning("OTA history rejected without active pull session from {Ip}", ip ?? "unknown"); return BadRequest("no active pull session"); } var day = DateTime.Now.ToString("yyyy-MM-dd"); - var dir = Path.Combine(_store.HistoryDir, day, SafeFileName(ip, "unknown")); + var dir = Path.Combine(_store.HistoryDir, day, SafeFileName(ip ?? "unknown", "unknown")); Directory.CreateDirectory(dir); var file = await ReadFirstFileAsync(ct); if (file == null || file.Length == 0) return BadRequest("empty"); @@ -58,6 +61,7 @@ public class OtaReceiveController : ControllerBase var path = Path.Combine(dir, safeName); await using var fs = System.IO.File.Create(path); await file.CopyToAsync(fs, ct); + _store.NotePullReceive(ip); _log.LogInformation("OTA history receive {Route} -> {Path} ({Len})", routeKey, path, file.Length); return Ok(new { ok = true }); } @@ -66,7 +70,7 @@ public class OtaReceiveController : ControllerBase { try { - var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString(); + var clientIp = ResolveTcpRemoteIp(); if (!_store.TryGetActivePullId(clientIp, out _)) { _log.LogWarning("OTA mdcs rejected without active pull session from {Ip}", clientIp ?? "unknown"); @@ -80,8 +84,9 @@ public class OtaReceiveController : ControllerBase Directory.CreateDirectory(Path.GetDirectoryName(dest)!); await using (var fs = System.IO.File.Create(dest)) await file.CopyToAsync(fs, ct); + _store.NotePullReceive(clientIp); _log.LogInformation("OTA mdcs receive {Route} -> {Dest} ({Len})", routeKey, dest, file.Length); - return Ok(new { ok = true, path = dest }); + return Ok(new { ok = true }); } catch (Exception ex) { @@ -90,6 +95,14 @@ public class OtaReceiveController : ControllerBase } } + /// 优先取 ForwardedHeaders 之前写入的 TCP 对端 IP,避免 X-Forwarded-For 投毒。 + private string? ResolveTcpRemoteIp() + { + if (HttpContext.Items.TryGetValue(TcpRemoteIpItemKey, out var boxed) && boxed is string s && !string.IsNullOrWhiteSpace(s)) + return s; + return HttpContext.Connection.RemoteIpAddress?.ToString(); + } + private async Task ReadFirstFileAsync(CancellationToken ct) { if (!Request.HasFormContentType) return null; diff --git a/MiGu.Server/Controllers/RbacController.cs b/MiGu.Server/Controllers/RbacController.cs index 09f864b..4ac4ac7 100644 --- a/MiGu.Server/Controllers/RbacController.cs +++ b/MiGu.Server/Controllers/RbacController.cs @@ -33,6 +33,8 @@ public class RbacController : ControllerBase new("ops.task.cancel", "任务 · 取消"), new("ops.task.reassign", "任务 · 改派"), new("ops.task.boostPriority", "任务 · 提升优先级"), + new("ops.ota", "OTA · 运维读写"), + new("ops.ota.write", "OTA · 写操作"), new("monitor.note.write", "监控 · 写运营备注"), new("auth.manage", "系统 · 权限与角色管理"), }; diff --git a/MiGu.Server/Fleet/AlarmCollector.cs b/MiGu.Server/Fleet/AlarmCollector.cs index 6d527fa..6c2e919 100644 --- a/MiGu.Server/Fleet/AlarmCollector.cs +++ b/MiGu.Server/Fleet/AlarmCollector.cs @@ -168,29 +168,36 @@ public sealed class AlarmCollector var activeByCar = new Dictionary(); foreach (var a in active) activeByCar[a.CarId] = a; // 每车取一条 active - // 出现 / 更新 + // 出现 / 更新:文案或级别变化时先 clear 旧记录再开新 active,保留分段历史 foreach (var cur in current.Values) { if (activeByCar.TryGetValue(cur.CarId, out var rec)) { - rec.Info = cur.Info; - rec.Level = cur.Level; - rec.CarName = cur.CarName; - rec.LastAt = now; - } - else - { - db.VehicleAlarms.Add(new VehicleAlarmRecord + var same = + string.Equals(rec.Info, cur.Info, StringComparison.Ordinal) && + rec.Level == cur.Level; + if (same) { - CarId = cur.CarId, - CarName = cur.CarName, - Info = cur.Info, - Level = cur.Level, - Status = "active", - FirstAt = now, - LastAt = now - }); + rec.CarName = cur.CarName; + rec.LastAt = now; + continue; + } + + rec.Status = "cleared"; + rec.ResolvedAt = now; + rec.DurationSecs = (long)Math.Max(0, (now - rec.FirstAt).TotalSeconds); } + + db.VehicleAlarms.Add(new VehicleAlarmRecord + { + CarId = cur.CarId, + CarName = cur.CarName, + Info = cur.Info, + Level = cur.Level, + Status = "active", + FirstAt = now, + LastAt = now + }); } // 消失 → 恢复 diff --git a/MiGu.Server/GlobalUsings.Db.cs b/MiGu.Server/GlobalUsings.Db.cs index f6e4478..b4c7f3b 100644 --- a/MiGu.Server/GlobalUsings.Db.cs +++ b/MiGu.Server/GlobalUsings.Db.cs @@ -17,3 +17,5 @@ global using WmsTransportRule = MiGu.DB.Domains.Transport.WmsTransportRule; global using WmsTransportTask = MiGu.DB.Domains.Transport.WmsTransportTask; global using WmsTransportReservation = MiGu.DB.Domains.Transport.WmsTransportReservation; global using WmsTransportTaskHistory = MiGu.DB.Domains.Transport.WmsTransportTaskHistory; +global using CdmTaskRecord = MiGu.DB.Domains.Fleet.CdmTaskRecord; +global using VehicleAlarmRecord = MiGu.DB.Domains.Fleet.VehicleAlarmRecord; diff --git a/MiGu.Server/Ota/OtaJobRunner.cs b/MiGu.Server/Ota/OtaJobRunner.cs index 32fbffd..e8b0fba 100644 --- a/MiGu.Server/Ota/OtaJobRunner.cs +++ b/MiGu.Server/Ota/OtaJobRunner.cs @@ -16,6 +16,35 @@ public sealed class OtaJobRunner _wd = wd; _vehicles = vehicles; _log = log; + RecoverInterruptedJobs(); + } + + /// 进程重启后把落盘中非终态 job 标为 failed,避免 UI 永久显示 running。 + private void RecoverInterruptedJobs() + { + try + { + foreach (var job in _store.ListJobs(500)) + { + if (job.Status is not ("running" or "pending")) continue; + job.Status = "failed"; + job.Message = string.IsNullOrWhiteSpace(job.Message) + ? "进程重启,任务中断" + : job.Message; + job.FinishedAt = DateTimeOffset.UtcNow; + foreach (var step in job.Steps.Where(s => s.Status is "pending" or "running")) + { + step.Status = "failed"; + step.Error ??= "进程重启,任务中断"; + } + _store.SaveJob(job); + _log.LogWarning("OTA job {Id} marked failed after process restart", job.Id); + } + } + catch (Exception ex) + { + _log.LogWarning(ex, "OTA interrupted job recovery failed"); + } } public OtaJob EnqueueSync(CreateSyncJobRequest req, string? user) diff --git a/MiGu.Server/Ota/OtaModels.cs b/MiGu.Server/Ota/OtaModels.cs index 24fdc4f..02738f0 100644 --- a/MiGu.Server/Ota/OtaModels.cs +++ b/MiGu.Server/Ota/OtaModels.cs @@ -6,9 +6,11 @@ public sealed class OtaSettings public int MaxCar { get; set; } = 2; public bool LatencyEnabled { get; set; } public int RttThresholdMs { get; set; } = 200; - /// skip | confirm + /// skip | allow(历史值 confirm 视为 allow) public string OverThreshold { get; set; } = "skip"; + /// 保留字段:备份尚未实现,仅反序列化兼容。 public int BackupPeriodMinutes { get; set; } = 60; + /// 保留字段:备份尚未实现,仅反序列化兼容。 public bool BackupExe { get; set; } public string? NewVersionName { get; set; } } diff --git a/MiGu.Server/Ota/OtaStore.cs b/MiGu.Server/Ota/OtaStore.cs index 55b22eb..d891ee3 100644 --- a/MiGu.Server/Ota/OtaStore.cs +++ b/MiGu.Server/Ota/OtaStore.cs @@ -24,6 +24,8 @@ public sealed class OtaStore private string? _lastPullId; private readonly System.Collections.Concurrent.ConcurrentDictionary _pullByIp = new(StringComparer.OrdinalIgnoreCase); + private readonly System.Collections.Concurrent.ConcurrentDictionary _pullLastReceiveUtc = + new(StringComparer.OrdinalIgnoreCase); private long _jobSeq; public OtaStore(IWebHostEnvironment env, IOptions options, ILogger log) @@ -144,29 +146,44 @@ public sealed class OtaStore lock (_gate) { var ip = NormalizeIp(sourceIp); + // 必须匹配会话 IP;禁止无 IP 时回退到最近一次拉包(可被伪造/误写)。 if (ip != null && _pullByIp.TryGetValue(ip, out var byIp)) { id = byIp; return true; } - if (ip == null && _lastPullId != null) - { - id = _lastPullId; - return true; - } - id = null; return false; } } + public void NotePullReceive(string? sourceIp) + { + var ip = NormalizeIp(sourceIp); + if (ip == null) return; + _pullLastReceiveUtc[ip] = DateTime.UtcNow; + } + + public bool TryGetLastReceiveAt(string? sourceIp, out DateTime utc) + { + var ip = NormalizeIp(sourceIp); + if (ip != null && _pullLastReceiveUtc.TryGetValue(ip, out utc)) + return true; + utc = default; + return false; + } + public void ClearActivePull(string? sourceIp = null) { lock (_gate) { var ip = NormalizeIp(sourceIp); - if (ip != null) _pullByIp.TryRemove(ip, out _); + if (ip != null) + { + _pullByIp.TryRemove(ip, out _); + _pullLastReceiveUtc.TryRemove(ip, out _); + } if (_pullByIp.IsEmpty) _lastPullId = null; } } diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs index 0cbbeb1..8dd4939 100644 --- a/MiGu.Server/Program.cs +++ b/MiGu.Server/Program.cs @@ -316,6 +316,14 @@ if (app.Environment.IsDevelopment()) app.UseSwaggerUI(); } +// OTA 回传会话按 TCP 对端 IP 校验;必须在 ForwardedHeaders 改写 RemoteIpAddress 之前捕获。 +app.Use(async (ctx, next) => +{ + ctx.Items[MiGu.Server.Controllers.OtaReceiveController.TcpRemoteIpItemKey] = + ctx.Connection.RemoteIpAddress?.ToString(); + await next(); +}); + // M6:在反向代理 / 负载均衡后运行时,根据 X-Forwarded-Proto 还原真实 scheme,让 // Request.IsHttps 正确 → 登录 Cookie 的 Secure 标志在生产 HTTPS 下能正确置位。 // 默认(未配置 KnownProxies):清空 Known* 表 = 信任所有前置转发头,适合「反代与本服务同机 / diff --git a/frontends/apps/simple-platform-vue/src/api/http.ts b/frontends/apps/simple-platform-vue/src/api/http.ts index 7b37a87..c6b40a3 100644 --- a/frontends/apps/simple-platform-vue/src/api/http.ts +++ b/frontends/apps/simple-platform-vue/src/api/http.ts @@ -13,6 +13,10 @@ const http: AxiosInstance = axios.create({ }) http.interceptors.request.use((config: InternalAxiosRequestConfig) => { + // FormData 必须由浏览器自动带 multipart boundary;清掉默认 application/json。 + if (typeof FormData !== 'undefined' && config.data instanceof FormData) { + config.headers.delete('Content-Type') + } // 双轨:优先用 localStorage 里的 token(过渡期 fallback),Cookie 会自动带; // 后端首先看 Authorization: Bearer,没有再看 Cookie,两路任一通过即可。 const token = localStorage.getItem('simple.auth.token') diff --git a/frontends/apps/simple-platform-vue/src/api/ota.ts b/frontends/apps/simple-platform-vue/src/api/ota.ts index f74f6f0..2537556 100644 --- a/frontends/apps/simple-platform-vue/src/api/ota.ts +++ b/frontends/apps/simple-platform-vue/src/api/ota.ts @@ -47,7 +47,6 @@ export async function uploadOtaPackage(file: File): Promise { const form = new FormData() form.append('file', file) const { data } = await http.post('/ota/packages/upload', form, { - headers: { 'Content-Type': 'multipart/form-data' }, timeout: 300000 }) return data @@ -116,7 +115,6 @@ export async function pushOtaCustomFile(opts: { form.append('restartOps', JSON.stringify(opts.restartOps.length ? opts.restartOps : [-1])) for (const f of opts.files) form.append('files', f) const { data } = await http.post('/ota/custom-file', form, { - headers: { 'Content-Type': 'multipart/form-data' }, timeout: 300000 }) return data diff --git a/frontends/apps/simple-platform-vue/src/views/shared/VehicleHubView.vue b/frontends/apps/simple-platform-vue/src/views/shared/VehicleHubView.vue index 1be40bc..e26d774 100644 --- a/frontends/apps/simple-platform-vue/src/views/shared/VehicleHubView.vue +++ b/frontends/apps/simple-platform-vue/src/views/shared/VehicleHubView.vue @@ -148,7 +148,7 @@ const auth = useAuthStore() const canWrite = computed(() => { if (auth.scope === 'Platform') return true const ops = auth.effectivePermissions?.allowedOps ?? [] - return ops.includes('*') || ops.some((o) => o === 'ops.ota' || o.startsWith('ops.ota.')) + return ops.includes('*') || ops.some((o) => o === 'ops.ota' || o === 'ops.ota.write' || o.startsWith('ops.ota.')) }) // Tab 与 URL ?tab= 同步,支持深链接(旧 /config/vehicle、/config/fleet 已下线,统一进车辆运维)。 diff --git a/frontends/apps/simple-platform-vue/src/views/shared/ota/OtaSettingsPane.vue b/frontends/apps/simple-platform-vue/src/views/shared/ota/OtaSettingsPane.vue index d9673dd..88c1815 100644 --- a/frontends/apps/simple-platform-vue/src/views/shared/ota/OtaSettingsPane.vue +++ b/frontends/apps/simple-platform-vue/src/views/shared/ota/OtaSettingsPane.vue @@ -21,18 +21,10 @@ 跳过该车 - 仍允许(需确认) + 仍允许下发 -

备份

- - - - - - -

展示