新增车辆表及OTA权限与逻辑优化
新增车辆任务与报警表,完善实体与DbContext配置。细化OTA权限校验,增强回传会话IP安全。优化OTA上传与设置面板,调度器支持重启恢复。报警采集逻辑支持历史分段。
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
namespace MiGu.Server.Fleet;
|
namespace MiGu.DB.Domains.Fleet;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CDM 搬运任务的平台侧快照(表 cdm_tasks)。
|
/// CDM 搬运任务的平台侧快照(表 cdm_tasks)。
|
||||||
/// 以任务 Id 为主键;SimpleLite/StandardScene 把终态任务从自身 JSON 里删除,这里则永久保留=完整历史,
|
/// 以任务 Id 为主键;SimpleLite/StandardScene 把终态任务从自身 JSON 里删除,这里则永久保留=完整历史。
|
||||||
/// 且 SimpleLite 关闭后平台仍可从本表读取最近快照。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CdmTaskRecord
|
public sealed class CdmTaskRecord
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace MiGu.DB.Domains.Fleet;
|
||||||
|
|
||||||
|
public sealed class CdmTaskRecordConfiguration : IEntityTypeConfiguration<CdmTaskRecord>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<CdmTaskRecord> 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<VehicleAlarmRecord>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<VehicleAlarmRecord> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-4
@@ -1,10 +1,8 @@
|
|||||||
namespace MiGu.Server.Fleet;
|
namespace MiGu.DB.Domains.Fleet;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 车辆报警的平台侧记录(表 vehicle_alarms)。
|
/// 车辆报警的平台侧记录(表 vehicle_alarms)。
|
||||||
/// SimpleLite 只在 SSE/状态里给出「当前是否报警 + 文案」,无历史;平台按车对帐:
|
/// 出现报警→开一条 active 记录;文案/级别变化→先 clear 再建新 active;消失→cleared。
|
||||||
/// 出现报警→开一条 active 记录,报警文案变化→更新,报警消失→置为 cleared 并记录恢复时间/持续时长。
|
|
||||||
/// 永不删除=完整历史,重启/刷新不丢,SimpleLite 离线也可查。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class VehicleAlarmRecord
|
public sealed class VehicleAlarmRecord
|
||||||
{
|
{
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MiGu.DB.Domains.Dashboard;
|
using MiGu.DB.Domains.Dashboard;
|
||||||
|
using MiGu.DB.Domains.Fleet;
|
||||||
using MiGu.DB.Domains.SimpleFields;
|
using MiGu.DB.Domains.SimpleFields;
|
||||||
using MiGu.DB.Domains.Transport;
|
using MiGu.DB.Domains.Transport;
|
||||||
using MiGu.DB.Domains.Wms;
|
using MiGu.DB.Domains.Wms;
|
||||||
@@ -25,4 +26,6 @@ public partial class MiGuDbContext
|
|||||||
public DbSet<WmsTransportTaskHistory> WmsTransportTaskHistories => Set<WmsTransportTaskHistory>();
|
public DbSet<WmsTransportTaskHistory> WmsTransportTaskHistories => Set<WmsTransportTaskHistory>();
|
||||||
public DbSet<SimpleField> SimpleFields => Set<SimpleField>();
|
public DbSet<SimpleField> SimpleFields => Set<SimpleField>();
|
||||||
public DbSet<UserDashboardShortcut> UserDashboardShortcuts => Set<UserDashboardShortcut>();
|
public DbSet<UserDashboardShortcut> UserDashboardShortcuts => Set<UserDashboardShortcut>();
|
||||||
|
public DbSet<CdmTaskRecord> CdmTasks => Set<CdmTaskRecord>();
|
||||||
|
public DbSet<VehicleAlarmRecord> VehicleAlarms => Set<VehicleAlarmRecord>();
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MiGu.DB.Migrations.Sqlite
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFleetTables : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "cdm_tasks",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||||
|
task_id = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true),
|
||||||
|
mission_id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
mission_name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
mission_type = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
src_site_id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
src_label = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false),
|
||||||
|
dst_site_id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
dst_label = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false),
|
||||||
|
status = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||||
|
status_code = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||||
|
car_id = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
car_name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true),
|
||||||
|
priority = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
create_time = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||||
|
start_time = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||||
|
finish_time = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||||
|
stuck_reason = table.Column<string>(type: "TEXT", maxLength: 512, nullable: true),
|
||||||
|
overdue = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
first_seen_at = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||||
|
last_seen_at = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||||
|
car_id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
car_name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
info = table.Column<string>(type: "text", nullable: false),
|
||||||
|
level = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
status = table.Column<string>(type: "TEXT", maxLength: 16, nullable: false),
|
||||||
|
first_at = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||||
|
last_at = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||||
|
resolved_at = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||||
|
duration_secs = table.Column<long>(type: "INTEGER", nullable: true),
|
||||||
|
acknowledged = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
acknowledged_at = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||||
|
acknowledged_by = table.Column<string>(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "cdm_tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "vehicle_alarms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,202 @@ namespace MiGu.DB.Migrations.Sqlite
|
|||||||
b.ToTable("user_dashboard_shortcuts", (string)null);
|
b.ToTable("user_dashboard_shortcuts", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MiGu.DB.Domains.Fleet.CdmTaskRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<int?>("CarId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("car_id");
|
||||||
|
|
||||||
|
b.Property<string>("CarName")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("car_name");
|
||||||
|
|
||||||
|
b.Property<string>("CreateTime")
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("create_time");
|
||||||
|
|
||||||
|
b.Property<string>("DstLabel")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("dst_label");
|
||||||
|
|
||||||
|
b.Property<int>("DstSiteId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("dst_site_id");
|
||||||
|
|
||||||
|
b.Property<string>("FinishTime")
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finish_time");
|
||||||
|
|
||||||
|
b.Property<string>("FirstSeenAt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("first_seen_at");
|
||||||
|
|
||||||
|
b.Property<string>("LastSeenAt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_seen_at");
|
||||||
|
|
||||||
|
b.Property<int>("MissionId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("mission_id");
|
||||||
|
|
||||||
|
b.Property<string>("MissionName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("mission_name");
|
||||||
|
|
||||||
|
b.Property<string>("MissionTypeName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("mission_type");
|
||||||
|
|
||||||
|
b.Property<bool>("Overdue")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("overdue");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("priority");
|
||||||
|
|
||||||
|
b.Property<string>("SrcLabel")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("src_label");
|
||||||
|
|
||||||
|
b.Property<int>("SrcSiteId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("src_site_id");
|
||||||
|
|
||||||
|
b.Property<string>("StartTime")
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_time");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("StatusCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status_code");
|
||||||
|
|
||||||
|
b.Property<string>("StuckReason")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("stuck_reason");
|
||||||
|
|
||||||
|
b.Property<string>("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<string>("Id")
|
||||||
|
.HasMaxLength(36)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Acknowledged")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("acknowledged");
|
||||||
|
|
||||||
|
b.Property<string>("AcknowledgedAt")
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("acknowledged_at");
|
||||||
|
|
||||||
|
b.Property<string>("AcknowledgedBy")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("acknowledged_by");
|
||||||
|
|
||||||
|
b.Property<int>("CarId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("car_id");
|
||||||
|
|
||||||
|
b.Property<string>("CarName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("car_name");
|
||||||
|
|
||||||
|
b.Property<long?>("DurationSecs")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("duration_secs");
|
||||||
|
|
||||||
|
b.Property<string>("FirstAt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("first_at");
|
||||||
|
|
||||||
|
b.Property<string>("Info")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("info");
|
||||||
|
|
||||||
|
b.Property<string>("LastAt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_at");
|
||||||
|
|
||||||
|
b.Property<int>("Level")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("level");
|
||||||
|
|
||||||
|
b.Property<string>("ResolvedAt")
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("resolved_at");
|
||||||
|
|
||||||
|
b.Property<string>("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 =>
|
modelBuilder.Entity("MiGu.DB.Domains.SimpleFields.SimpleField", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ public class OtaController : ControllerBase
|
|||||||
if (string.Equals(Scope, "Platform", StringComparison.OrdinalIgnoreCase)) return true;
|
if (string.Equals(Scope, "Platform", StringComparison.OrdinalIgnoreCase)) return true;
|
||||||
var ops = User.FindFirst("ops")?.Value ?? "";
|
var ops = User.FindFirst("ops")?.Value ?? "";
|
||||||
var set = ops.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
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)
|
private bool DenyWrite(out ActionResult denied)
|
||||||
@@ -147,12 +150,32 @@ public class OtaController : ControllerBase
|
|||||||
var receiveBase = $"{baseUrl.TrimEnd('/')}/api/ota/receive";
|
var receiveBase = $"{baseUrl.TrimEnd('/')}/api/ota/receive";
|
||||||
var time = DateTime.Now.ToString("yyyyMMddHHmmss");
|
var time = DateTime.Now.ToString("yyyyMMddHHmmss");
|
||||||
await _wd.TriggerPullAsync(car.Ip, receiveBase, time, ct);
|
await _wd.TriggerPullAsync(car.Ip, receiveBase, time, ct);
|
||||||
// 等待文件落盘
|
// 等待文件落盘:绝对超时 + 收到文件后的空闲窗口,避免首个组件到达就清会话导致半包
|
||||||
await Task.Delay(1500, 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);
|
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);
|
await Task.Delay(500, ct);
|
||||||
}
|
}
|
||||||
_store.ClearActivePull(car.Ip);
|
_store.ClearActivePull(car.Ip);
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ namespace MiGu.Server.Controllers;
|
|||||||
/// WatchDog 回传包接收端。
|
/// WatchDog 回传包接收端。
|
||||||
/// WatchDog 写死 POST 到 http://{config.serverIP}:8000/upload-mdcs/{routeKey},
|
/// WatchDog 写死 POST 到 http://{config.serverIP}:8000/upload-mdcs/{routeKey},
|
||||||
/// 必须与参考 Electron Express :8000 路径一致;/api/ota/receive/* 仅作兼容别名。
|
/// 必须与参考 Electron Express :8000 路径一致;/api/ota/receive/* 仅作兼容别名。
|
||||||
|
/// 会话校验使用 TCP 对端 IP(见 Program 中 TcpRemoteIp),忽略可伪造的 X-Forwarded-For。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public class OtaReceiveController : ControllerBase
|
public class OtaReceiveController : ControllerBase
|
||||||
{
|
{
|
||||||
|
public const string TcpRemoteIpItemKey = "TcpRemoteIp";
|
||||||
|
|
||||||
private readonly OtaStore _store;
|
private readonly OtaStore _store;
|
||||||
private readonly ILogger<OtaReceiveController> _log;
|
private readonly ILogger<OtaReceiveController> _log;
|
||||||
|
|
||||||
@@ -40,15 +43,15 @@ public class OtaReceiveController : ControllerBase
|
|||||||
[RequestSizeLimit(512_000_000)]
|
[RequestSizeLimit(512_000_000)]
|
||||||
public async Task<IActionResult> UploadHistory(string routeKey, CancellationToken ct)
|
public async Task<IActionResult> UploadHistory(string routeKey, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
var ip = ResolveTcpRemoteIp();
|
||||||
if (!_store.TryGetActivePullId(ip, out _))
|
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");
|
return BadRequest("no active pull session");
|
||||||
}
|
}
|
||||||
|
|
||||||
var day = DateTime.Now.ToString("yyyy-MM-dd");
|
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);
|
Directory.CreateDirectory(dir);
|
||||||
var file = await ReadFirstFileAsync(ct);
|
var file = await ReadFirstFileAsync(ct);
|
||||||
if (file == null || file.Length == 0) return BadRequest("empty");
|
if (file == null || file.Length == 0) return BadRequest("empty");
|
||||||
@@ -58,6 +61,7 @@ public class OtaReceiveController : ControllerBase
|
|||||||
var path = Path.Combine(dir, safeName);
|
var path = Path.Combine(dir, safeName);
|
||||||
await using var fs = System.IO.File.Create(path);
|
await using var fs = System.IO.File.Create(path);
|
||||||
await file.CopyToAsync(fs, ct);
|
await file.CopyToAsync(fs, ct);
|
||||||
|
_store.NotePullReceive(ip);
|
||||||
_log.LogInformation("OTA history receive {Route} -> {Path} ({Len})", routeKey, path, file.Length);
|
_log.LogInformation("OTA history receive {Route} -> {Path} ({Len})", routeKey, path, file.Length);
|
||||||
return Ok(new { ok = true });
|
return Ok(new { ok = true });
|
||||||
}
|
}
|
||||||
@@ -66,7 +70,7 @@ public class OtaReceiveController : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
var clientIp = ResolveTcpRemoteIp();
|
||||||
if (!_store.TryGetActivePullId(clientIp, out _))
|
if (!_store.TryGetActivePullId(clientIp, out _))
|
||||||
{
|
{
|
||||||
_log.LogWarning("OTA mdcs rejected without active pull session from {Ip}", clientIp ?? "unknown");
|
_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)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
|
||||||
await using (var fs = System.IO.File.Create(dest))
|
await using (var fs = System.IO.File.Create(dest))
|
||||||
await file.CopyToAsync(fs, ct);
|
await file.CopyToAsync(fs, ct);
|
||||||
|
_store.NotePullReceive(clientIp);
|
||||||
_log.LogInformation("OTA mdcs receive {Route} -> {Dest} ({Len})", routeKey, dest, file.Length);
|
_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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -90,6 +95,14 @@ public class OtaReceiveController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>优先取 ForwardedHeaders 之前写入的 TCP 对端 IP,避免 X-Forwarded-For 投毒。</summary>
|
||||||
|
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<IFormFile?> ReadFirstFileAsync(CancellationToken ct)
|
private async Task<IFormFile?> ReadFirstFileAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!Request.HasFormContentType) return null;
|
if (!Request.HasFormContentType) return null;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public class RbacController : ControllerBase
|
|||||||
new("ops.task.cancel", "任务 · 取消"),
|
new("ops.task.cancel", "任务 · 取消"),
|
||||||
new("ops.task.reassign", "任务 · 改派"),
|
new("ops.task.reassign", "任务 · 改派"),
|
||||||
new("ops.task.boostPriority", "任务 · 提升优先级"),
|
new("ops.task.boostPriority", "任务 · 提升优先级"),
|
||||||
|
new("ops.ota", "OTA · 运维读写"),
|
||||||
|
new("ops.ota.write", "OTA · 写操作"),
|
||||||
new("monitor.note.write", "监控 · 写运营备注"),
|
new("monitor.note.write", "监控 · 写运营备注"),
|
||||||
new("auth.manage", "系统 · 权限与角色管理"),
|
new("auth.manage", "系统 · 权限与角色管理"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -168,29 +168,36 @@ public sealed class AlarmCollector
|
|||||||
var activeByCar = new Dictionary<int, VehicleAlarmRecord>();
|
var activeByCar = new Dictionary<int, VehicleAlarmRecord>();
|
||||||
foreach (var a in active) activeByCar[a.CarId] = a; // 每车取一条 active
|
foreach (var a in active) activeByCar[a.CarId] = a; // 每车取一条 active
|
||||||
|
|
||||||
// 出现 / 更新
|
// 出现 / 更新:文案或级别变化时先 clear 旧记录再开新 active,保留分段历史
|
||||||
foreach (var cur in current.Values)
|
foreach (var cur in current.Values)
|
||||||
{
|
{
|
||||||
if (activeByCar.TryGetValue(cur.CarId, out var rec))
|
if (activeByCar.TryGetValue(cur.CarId, out var rec))
|
||||||
{
|
{
|
||||||
rec.Info = cur.Info;
|
var same =
|
||||||
rec.Level = cur.Level;
|
string.Equals(rec.Info, cur.Info, StringComparison.Ordinal) &&
|
||||||
rec.CarName = cur.CarName;
|
rec.Level == cur.Level;
|
||||||
rec.LastAt = now;
|
if (same)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
db.VehicleAlarms.Add(new VehicleAlarmRecord
|
|
||||||
{
|
{
|
||||||
CarId = cur.CarId,
|
rec.CarName = cur.CarName;
|
||||||
CarName = cur.CarName,
|
rec.LastAt = now;
|
||||||
Info = cur.Info,
|
continue;
|
||||||
Level = cur.Level,
|
}
|
||||||
Status = "active",
|
|
||||||
FirstAt = now,
|
rec.Status = "cleared";
|
||||||
LastAt = now
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消失 → 恢复
|
// 消失 → 恢复
|
||||||
|
|||||||
@@ -17,3 +17,5 @@ global using WmsTransportRule = MiGu.DB.Domains.Transport.WmsTransportRule;
|
|||||||
global using WmsTransportTask = MiGu.DB.Domains.Transport.WmsTransportTask;
|
global using WmsTransportTask = MiGu.DB.Domains.Transport.WmsTransportTask;
|
||||||
global using WmsTransportReservation = MiGu.DB.Domains.Transport.WmsTransportReservation;
|
global using WmsTransportReservation = MiGu.DB.Domains.Transport.WmsTransportReservation;
|
||||||
global using WmsTransportTaskHistory = MiGu.DB.Domains.Transport.WmsTransportTaskHistory;
|
global using WmsTransportTaskHistory = MiGu.DB.Domains.Transport.WmsTransportTaskHistory;
|
||||||
|
global using CdmTaskRecord = MiGu.DB.Domains.Fleet.CdmTaskRecord;
|
||||||
|
global using VehicleAlarmRecord = MiGu.DB.Domains.Fleet.VehicleAlarmRecord;
|
||||||
|
|||||||
@@ -16,6 +16,35 @@ public sealed class OtaJobRunner
|
|||||||
_wd = wd;
|
_wd = wd;
|
||||||
_vehicles = vehicles;
|
_vehicles = vehicles;
|
||||||
_log = log;
|
_log = log;
|
||||||
|
RecoverInterruptedJobs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>进程重启后把落盘中非终态 job 标为 failed,避免 UI 永久显示 running。</summary>
|
||||||
|
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)
|
public OtaJob EnqueueSync(CreateSyncJobRequest req, string? user)
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ public sealed class OtaSettings
|
|||||||
public int MaxCar { get; set; } = 2;
|
public int MaxCar { get; set; } = 2;
|
||||||
public bool LatencyEnabled { get; set; }
|
public bool LatencyEnabled { get; set; }
|
||||||
public int RttThresholdMs { get; set; } = 200;
|
public int RttThresholdMs { get; set; } = 200;
|
||||||
/// <summary>skip | confirm</summary>
|
/// <summary>skip | allow(历史值 confirm 视为 allow)</summary>
|
||||||
public string OverThreshold { get; set; } = "skip";
|
public string OverThreshold { get; set; } = "skip";
|
||||||
|
/// <summary>保留字段:备份尚未实现,仅反序列化兼容。</summary>
|
||||||
public int BackupPeriodMinutes { get; set; } = 60;
|
public int BackupPeriodMinutes { get; set; } = 60;
|
||||||
|
/// <summary>保留字段:备份尚未实现,仅反序列化兼容。</summary>
|
||||||
public bool BackupExe { get; set; }
|
public bool BackupExe { get; set; }
|
||||||
public string? NewVersionName { get; set; }
|
public string? NewVersionName { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public sealed class OtaStore
|
|||||||
private string? _lastPullId;
|
private string? _lastPullId;
|
||||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, string> _pullByIp =
|
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, string> _pullByIp =
|
||||||
new(StringComparer.OrdinalIgnoreCase);
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, DateTime> _pullLastReceiveUtc =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
private long _jobSeq;
|
private long _jobSeq;
|
||||||
|
|
||||||
public OtaStore(IWebHostEnvironment env, IOptions<OtaOptions> options, ILogger<OtaStore> log)
|
public OtaStore(IWebHostEnvironment env, IOptions<OtaOptions> options, ILogger<OtaStore> log)
|
||||||
@@ -144,29 +146,44 @@ public sealed class OtaStore
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
var ip = NormalizeIp(sourceIp);
|
var ip = NormalizeIp(sourceIp);
|
||||||
|
// 必须匹配会话 IP;禁止无 IP 时回退到最近一次拉包(可被伪造/误写)。
|
||||||
if (ip != null && _pullByIp.TryGetValue(ip, out var byIp))
|
if (ip != null && _pullByIp.TryGetValue(ip, out var byIp))
|
||||||
{
|
{
|
||||||
id = byIp;
|
id = byIp;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ip == null && _lastPullId != null)
|
|
||||||
{
|
|
||||||
id = _lastPullId;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
id = null;
|
id = null;
|
||||||
return false;
|
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)
|
public void ClearActivePull(string? sourceIp = null)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
var ip = NormalizeIp(sourceIp);
|
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;
|
if (_pullByIp.IsEmpty) _lastPullId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,6 +316,14 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.UseSwaggerUI();
|
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,让
|
// M6:在反向代理 / 负载均衡后运行时,根据 X-Forwarded-Proto 还原真实 scheme,让
|
||||||
// Request.IsHttps 正确 → 登录 Cookie 的 Secure 标志在生产 HTTPS 下能正确置位。
|
// Request.IsHttps 正确 → 登录 Cookie 的 Secure 标志在生产 HTTPS 下能正确置位。
|
||||||
// 默认(未配置 KnownProxies):清空 Known* 表 = 信任所有前置转发头,适合「反代与本服务同机 /
|
// 默认(未配置 KnownProxies):清空 Known* 表 = 信任所有前置转发头,适合「反代与本服务同机 /
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ const http: AxiosInstance = axios.create({
|
|||||||
})
|
})
|
||||||
|
|
||||||
http.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
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 会自动带;
|
// 双轨:优先用 localStorage 里的 token(过渡期 fallback),Cookie 会自动带;
|
||||||
// 后端首先看 Authorization: Bearer,没有再看 Cookie,两路任一通过即可。
|
// 后端首先看 Authorization: Bearer,没有再看 Cookie,两路任一通过即可。
|
||||||
const token = localStorage.getItem('simple.auth.token')
|
const token = localStorage.getItem('simple.auth.token')
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export async function uploadOtaPackage(file: File): Promise<OtaPackageInfo> {
|
|||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', file)
|
form.append('file', file)
|
||||||
const { data } = await http.post<OtaPackageInfo>('/ota/packages/upload', form, {
|
const { data } = await http.post<OtaPackageInfo>('/ota/packages/upload', form, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
timeout: 300000
|
timeout: 300000
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
@@ -116,7 +115,6 @@ export async function pushOtaCustomFile(opts: {
|
|||||||
form.append('restartOps', JSON.stringify(opts.restartOps.length ? opts.restartOps : [-1]))
|
form.append('restartOps', JSON.stringify(opts.restartOps.length ? opts.restartOps : [-1]))
|
||||||
for (const f of opts.files) form.append('files', f)
|
for (const f of opts.files) form.append('files', f)
|
||||||
const { data } = await http.post<OtaJob>('/ota/custom-file', form, {
|
const { data } = await http.post<OtaJob>('/ota/custom-file', form, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
timeout: 300000
|
timeout: 300000
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const auth = useAuthStore()
|
|||||||
const canWrite = computed(() => {
|
const canWrite = computed(() => {
|
||||||
if (auth.scope === 'Platform') return true
|
if (auth.scope === 'Platform') return true
|
||||||
const ops = auth.effectivePermissions?.allowedOps ?? []
|
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 已下线,统一进车辆运维)。
|
// Tab 与 URL ?tab= 同步,支持深链接(旧 /config/vehicle、/config/fleet 已下线,统一进车辆运维)。
|
||||||
|
|||||||
@@ -21,18 +21,10 @@
|
|||||||
<el-form-item label="超限策略">
|
<el-form-item label="超限策略">
|
||||||
<el-radio-group v-model="form.overThreshold">
|
<el-radio-group v-model="form.overThreshold">
|
||||||
<el-radio value="skip">跳过该车</el-radio>
|
<el-radio value="skip">跳过该车</el-radio>
|
||||||
<el-radio value="confirm">仍允许(需确认)</el-radio>
|
<el-radio value="confirm">仍允许下发</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<h4>备份</h4>
|
|
||||||
<el-form-item label="周期 (分钟)">
|
|
||||||
<el-input-number v-model="form.backupPeriodMinutes" :min="0" :max="10080" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备份可执行文件">
|
|
||||||
<el-switch v-model="form.backupExe" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h4>展示</h4>
|
<h4>展示</h4>
|
||||||
<el-form-item label="目标版本名称">
|
<el-form-item label="目标版本名称">
|
||||||
<el-input v-model="form.newVersionName" placeholder="可选显示名" />
|
<el-input v-model="form.newVersionName" placeholder="可选显示名" />
|
||||||
|
|||||||
Reference in New Issue
Block a user