merge
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using MiGu.Server.Wms;
|
||||
|
||||
namespace MiGu.Server.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[TypeFilter(typeof(WmsExceptionFilter))]
|
||||
[Route("api/wms")]
|
||||
public sealed class WmsController : ControllerBase
|
||||
{
|
||||
private readonly WmsService _service;
|
||||
|
||||
public WmsController(WmsService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
[HttpGet("areas")]
|
||||
public Task<List<WarehouseArea>> Areas([FromQuery] string? q) => _service.Areas(q);
|
||||
|
||||
[HttpPost("areas")]
|
||||
public async Task<IActionResult> SaveArea([FromBody] MasterDataRequest req) => Ok(await _service.SaveArea(req, User.ActorName()));
|
||||
|
||||
[HttpPut("areas/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateArea(Guid id, [FromBody] MasterDataRequest req) =>
|
||||
Ok(await _service.SaveArea(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("areas/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteArea(Guid id, [FromQuery] long? version)
|
||||
{
|
||||
await _service.DeleteEntity<WarehouseArea>(id, version, User.ActorName());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("storages")]
|
||||
public Task<List<Storage>> Storages([FromQuery] string? q) => _service.Storages(q);
|
||||
|
||||
[HttpPost("storages")]
|
||||
public async Task<IActionResult> SaveStorage([FromBody] StorageRequest req) => Ok(await _service.SaveStorage(req, User.ActorName()));
|
||||
|
||||
[HttpPut("storages/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateStorage(Guid id, [FromBody] StorageRequest req) =>
|
||||
Ok(await _service.SaveStorage(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("storages/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteStorage(Guid id, [FromQuery] long? version)
|
||||
{
|
||||
await _service.DeleteEntity<Storage>(id, version, User.ActorName());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("containers")]
|
||||
public Task<List<Container>> Containers([FromQuery] string? q) => _service.Containers(q);
|
||||
|
||||
[HttpPost("containers")]
|
||||
public async Task<IActionResult> SaveContainer([FromBody] MasterDataRequest req) => Ok(await _service.SaveContainer(req, User.ActorName()));
|
||||
|
||||
[HttpPut("containers/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateContainer(Guid id, [FromBody] MasterDataRequest req) =>
|
||||
Ok(await _service.SaveContainer(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("containers/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteContainer(Guid id, [FromQuery] long? version)
|
||||
{
|
||||
await _service.DeleteEntity<Container>(id, version, User.ActorName());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("materials")]
|
||||
public Task<List<Material>> Materials([FromQuery] string? q) => _service.Materials(q);
|
||||
|
||||
[HttpPost("materials")]
|
||||
public async Task<IActionResult> SaveMaterial([FromBody] MaterialRequest req) => Ok(await _service.SaveMaterial(req, User.ActorName()));
|
||||
|
||||
[HttpPut("materials/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateMaterial(Guid id, [FromBody] MaterialRequest req) =>
|
||||
Ok(await _service.SaveMaterial(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("materials/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteMaterial(Guid id, [FromQuery] long? version)
|
||||
{
|
||||
await _service.DeleteEntity<Material>(id, version, User.ActorName());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("container-locations")]
|
||||
public Task<List<ContainerLocation>> ContainerLocations([FromQuery] string? locationType, [FromQuery] string? q) =>
|
||||
_service.ContainerLocations(locationType, q);
|
||||
|
||||
[HttpPost("container-locations")]
|
||||
public async Task<IActionResult> SaveContainerLocation([FromBody] ContainerLocationRequest req) =>
|
||||
Ok(await _service.BindOrTransferLocation(req, User.ActorName()));
|
||||
|
||||
[HttpPost("container-locations/transfer")]
|
||||
public async Task<IActionResult> TransferContainerLocation([FromBody] ContainerLocationRequest req) =>
|
||||
Ok(await _service.BindOrTransferLocation(req, User.ActorName()));
|
||||
|
||||
[HttpDelete("container-locations/{containerId:guid}")]
|
||||
public async Task<IActionResult> UnbindContainerLocation(Guid containerId, [FromQuery] string? reason)
|
||||
{
|
||||
await _service.UnbindLocation(containerId, User.ActorName(), reason ?? "");
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("container-materials")]
|
||||
public Task<List<ContainerMaterial>> ContainerMaterials([FromQuery] string? q) => _service.ContainerMaterials(q);
|
||||
|
||||
[HttpPost("container-materials")]
|
||||
public async Task<IActionResult> SaveContainerMaterial([FromBody] ContainerMaterialRequest req) =>
|
||||
Ok(await _service.SaveContainerMaterial(req, User.ActorName()));
|
||||
|
||||
[HttpPut("container-materials/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateContainerMaterial(Guid id, [FromBody] ContainerMaterialRequest req) =>
|
||||
Ok(await _service.SaveContainerMaterial(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("container-materials/{id:guid}")]
|
||||
public async Task<IActionResult> UnloadContainerMaterial(Guid id, [FromQuery] string? reason)
|
||||
{
|
||||
await _service.UnloadMaterial(id, User.ActorName(), reason ?? "");
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("container-location-history")]
|
||||
public Task<List<ContainerLocationHistory>> ContainerLocationHistory([FromQuery] Guid? containerId) =>
|
||||
_service.LocationHistory(containerId);
|
||||
|
||||
[HttpGet("container-material-history")]
|
||||
public Task<List<ContainerMaterialHistory>> ContainerMaterialHistory([FromQuery] Guid? containerId, [FromQuery] Guid? materialId) =>
|
||||
_service.MaterialHistory(containerId, materialId);
|
||||
}
|
||||
|
||||
public sealed class WmsExceptionFilter : IExceptionFilter
|
||||
{
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
if (context.Exception is not InvalidOperationException ex) return;
|
||||
context.Result = new BadRequestObjectResult(new { message = ex.Message });
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace MiGu.Server.Persistence;
|
||||
|
||||
public abstract class EntityBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTimeOffset? DeletedAt { get; set; }
|
||||
public string DeletedBy { get; set; } = "";
|
||||
public long Version { get; set; }
|
||||
public bool IsLock { get; set; }
|
||||
public string CreatedBy { get; set; } = "";
|
||||
public string UpdatedBy { get; set; } = "";
|
||||
public string Remark { get; set; } = "";
|
||||
public string Extend { get; set; } = "{}";
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MiGu.Server.Wms;
|
||||
|
||||
namespace MiGu.Server.Persistence;
|
||||
|
||||
public sealed class PlatformDbContext : DbContext
|
||||
{
|
||||
public PlatformDbContext(DbContextOptions<PlatformDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<WarehouseArea> WarehouseAreas => Set<WarehouseArea>();
|
||||
public DbSet<Storage> Storages => Set<Storage>();
|
||||
public DbSet<Container> Containers => Set<Container>();
|
||||
public DbSet<Material> Materials => Set<Material>();
|
||||
public DbSet<ContainerLocation> ContainerLocations => Set<ContainerLocation>();
|
||||
public DbSet<ContainerMaterial> ContainerMaterials => Set<ContainerMaterial>();
|
||||
public DbSet<ContainerLocationHistory> ContainerLocationHistories => Set<ContainerLocationHistory>();
|
||||
public DbSet<ContainerMaterialHistory> ContainerMaterialHistories => Set<ContainerMaterialHistory>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
var guid = new ValueConverter<Guid, string>(
|
||||
v => v.ToString("D"),
|
||||
v => Guid.Parse(v));
|
||||
var nullableGuid = new ValueConverter<Guid?, string?>(
|
||||
v => v.HasValue ? v.Value.ToString("D") : null,
|
||||
v => string.IsNullOrWhiteSpace(v) ? null : Guid.Parse(v));
|
||||
|
||||
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(Guid)))
|
||||
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(guid).HasMaxLength(36);
|
||||
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(Guid?)))
|
||||
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(nullableGuid).HasMaxLength(36);
|
||||
}
|
||||
|
||||
ConfigureEntityBase<WarehouseArea>(modelBuilder, "wms_areas");
|
||||
ConfigureEntityBase<Storage>(modelBuilder, "wms_storages");
|
||||
ConfigureEntityBase<Container>(modelBuilder, "wms_containers");
|
||||
ConfigureEntityBase<Material>(modelBuilder, "wms_materials");
|
||||
ConfigureEntityBase<ContainerLocation>(modelBuilder, "wms_container_locations");
|
||||
ConfigureEntityBase<ContainerMaterial>(modelBuilder, "wms_container_materials");
|
||||
|
||||
ConfigureHistory<ContainerLocationHistory>(modelBuilder, "wms_container_location_history");
|
||||
ConfigureHistory<ContainerMaterialHistory>(modelBuilder, "wms_container_material_history");
|
||||
|
||||
modelBuilder.Entity<WarehouseArea>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.AreaId);
|
||||
modelBuilder.Entity<Container>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Material>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<ContainerLocation>().HasIndex(x => x.ContainerId).IsUnique();
|
||||
modelBuilder.Entity<ContainerLocation>().HasIndex(x => new { x.LocationType, x.LocationId });
|
||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => new { x.ContainerId, x.MaterialId, x.BatchNo, x.SerialNo }).IsUnique();
|
||||
|
||||
modelBuilder.Entity<ContainerMaterial>().Property(x => x.Quantity).HasPrecision(18, 4);
|
||||
modelBuilder.Entity<ContainerMaterialHistory>().Property(x => x.QuantityDelta).HasPrecision(18, 4);
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
StampEntities();
|
||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
||||
{
|
||||
StampEntities();
|
||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||
}
|
||||
|
||||
private void StampEntities()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
foreach (var e in ChangeTracker.Entries<EntityBase>())
|
||||
{
|
||||
if (e.State == EntityState.Added)
|
||||
{
|
||||
if (e.Entity.Id == Guid.Empty) e.Entity.Id = Guid.NewGuid();
|
||||
e.Entity.CreatedAt = now;
|
||||
e.Entity.UpdatedAt = now;
|
||||
e.Entity.Version = Math.Max(1, e.Entity.Version);
|
||||
if (string.IsNullOrWhiteSpace(e.Entity.Extend)) e.Entity.Extend = "{}";
|
||||
}
|
||||
else if (e.State == EntityState.Modified)
|
||||
{
|
||||
e.Entity.UpdatedAt = now;
|
||||
e.Entity.Version += 1;
|
||||
if (string.IsNullOrWhiteSpace(e.Entity.Extend)) e.Entity.Extend = "{}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureEntityBase<T>(ModelBuilder modelBuilder, string table) where T : EntityBase
|
||||
{
|
||||
var e = modelBuilder.Entity<T>();
|
||||
e.ToTable(table);
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
||||
e.Property(x => x.UpdatedBy).HasMaxLength(128);
|
||||
e.Property(x => x.DeletedBy).HasMaxLength(128);
|
||||
e.Property(x => x.Remark).HasMaxLength(1000);
|
||||
e.Property(x => x.Extend).HasColumnType("text");
|
||||
e.HasQueryFilter(x => !x.IsDeleted);
|
||||
}
|
||||
|
||||
private static void ConfigureHistory<T>(ModelBuilder modelBuilder, string table) where T : WarehouseHistoryBase
|
||||
{
|
||||
var e = modelBuilder.Entity<T>();
|
||||
e.ToTable(table);
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.EventType).HasMaxLength(64);
|
||||
e.Property(x => x.BeforeJson).HasColumnType("text");
|
||||
e.Property(x => x.AfterJson).HasColumnType("text");
|
||||
e.Property(x => x.Operator).HasMaxLength(128);
|
||||
e.Property(x => x.Source).HasMaxLength(32);
|
||||
e.Property(x => x.Reason).HasMaxLength(500);
|
||||
e.Property(x => x.Remark).HasMaxLength(1000);
|
||||
e.Property(x => x.Extend).HasColumnType("text");
|
||||
e.HasIndex(x => x.ContainerId);
|
||||
e.HasIndex(x => x.OperatedAt);
|
||||
e.HasIndex(x => x.EventType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using MiGu.Server.Wms;
|
||||
|
||||
namespace MiGu.Server.Persistence;
|
||||
|
||||
public static class PlatformPersistence
|
||||
{
|
||||
public static IServiceCollection AddPlatformPersistence(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddDbContext<PlatformDbContext>((sp, options) =>
|
||||
{
|
||||
var env = sp.GetRequiredService<IWebHostEnvironment>();
|
||||
var provider = configuration["Database:Provider"] ?? "sqlite";
|
||||
var connection = ResolveConnectionString(configuration, env, provider);
|
||||
|
||||
switch (provider.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "sqlite":
|
||||
options.UseSqlite(connection);
|
||||
break;
|
||||
case "mysql":
|
||||
options.UseMySql(connection, ServerVersion.AutoDetect(connection));
|
||||
break;
|
||||
case "postgres":
|
||||
case "postgresql":
|
||||
case "npgsql":
|
||||
options.UseNpgsql(connection);
|
||||
break;
|
||||
case "sqlserver":
|
||||
case "mssql":
|
||||
options.UseSqlServer(connection);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"未知数据库 Provider: {provider}");
|
||||
}
|
||||
});
|
||||
|
||||
services.AddScoped<WmsReferenceValidator>();
|
||||
services.AddScoped<WmsService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static async Task EnsurePlatformDatabaseAsync(this IServiceProvider services)
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<PlatformDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
}
|
||||
|
||||
private static string ResolveConnectionString(IConfiguration configuration, IWebHostEnvironment env, string provider)
|
||||
{
|
||||
var key = provider.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"postgres" or "postgresql" or "npgsql" => "PostgreSQL",
|
||||
"mssql" => "SqlServer",
|
||||
_ => provider
|
||||
};
|
||||
var configured = configuration.GetConnectionString(key) ?? configuration.GetConnectionString("Platform");
|
||||
if (!string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
return IsSqlite(provider) ? NormalizeSqliteConnection(configured, env) : configured;
|
||||
}
|
||||
|
||||
var dataDir = Path.Combine(env.ContentRootPath, "data");
|
||||
Directory.CreateDirectory(dataDir);
|
||||
return $"Data Source={Path.Combine(dataDir, "platform.db")}";
|
||||
}
|
||||
|
||||
private static bool IsSqlite(string provider) =>
|
||||
string.Equals(provider.Trim(), "sqlite", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string NormalizeSqliteConnection(string connection, IWebHostEnvironment env)
|
||||
{
|
||||
var builder = new SqliteConnectionStringBuilder(connection);
|
||||
if (string.IsNullOrWhiteSpace(builder.DataSource)) return connection;
|
||||
if (builder.DataSource is ":memory:") return connection;
|
||||
if (!Path.IsPathRooted(builder.DataSource))
|
||||
{
|
||||
builder.DataSource = Path.Combine(env.ContentRootPath, builder.DataSource);
|
||||
}
|
||||
var dir = Path.GetDirectoryName(builder.DataSource);
|
||||
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using Microsoft.OpenApi.Models;
|
||||
using MiGu.Server.Auth;
|
||||
using MiGu.Server.Configs;
|
||||
using MiGu.Server.Launcher;
|
||||
using MiGu.Server.OpenApi;
|
||||
using MiGu.Server.Persistence;
|
||||
using Yarp.ReverseProxy.Transforms;
|
||||
|
||||
static string? FindSourceContentRoot(string startDir)
|
||||
@@ -216,6 +216,7 @@ builder.Services.AddSingleton<ConfigStore>();
|
||||
builder.Services.AddSingleton<OpsAuditStore>();
|
||||
// OpsController 真实转发 SimpleLite reflection execute 所需的 HttpClient 工厂。
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddPlatformPersistence(builder.Configuration);
|
||||
|
||||
// 会话 N+1(启动反转):把 SimpleLite 子进程拉起器接入 DI;AuthController 登录成功后按 LaunchMode 调 MaybeStart。
|
||||
// 配置段绑定 appsettings.json:SimpleLite,可被环境变量 SIMPLELITE__XXX 覆盖。
|
||||
@@ -223,6 +224,7 @@ builder.Services.Configure<SimpleLiteOptions>(builder.Configuration.GetSection("
|
||||
builder.Services.AddSingleton<SimpleLiteLauncher>();
|
||||
|
||||
var app = builder.Build();
|
||||
await app.Services.EnsurePlatformDatabaseAsync();
|
||||
|
||||
// 启动期主动构造 JwtIssuer / InternalTokenStore:让 secret 校验日志在请求来之前打印。
|
||||
_ = app.Services.GetRequiredService<JwtIssuer>();
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MiGu.Server.Persistence;
|
||||
|
||||
namespace MiGu.Server.Wms;
|
||||
|
||||
public abstract class WarehouseHistoryBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid? RelationId { get; set; }
|
||||
public string EventType { get; set; } = "";
|
||||
public string BeforeJson { get; set; } = "{}";
|
||||
public string AfterJson { get; set; } = "{}";
|
||||
public Guid? ContainerId { get; set; }
|
||||
public string Operator { get; set; } = "";
|
||||
public DateTimeOffset OperatedAt { get; set; }
|
||||
public string Source { get; set; } = "Manual";
|
||||
public string Reason { get; set; } = "";
|
||||
public string Remark { get; set; } = "";
|
||||
public string Extend { get; set; } = "{}";
|
||||
}
|
||||
|
||||
public sealed class WarehouseArea : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string Type { get; set; } = "Storage";
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public sealed class Storage : EntityBase
|
||||
{
|
||||
public Guid AreaId { get; set; }
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string StorageType { get; set; } = "Storage";
|
||||
[MaxLength(64)] public string SiteId { get; set; } = "";
|
||||
public int Capacity { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class Container : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string ContainerType { get; set; } = "Box";
|
||||
[MaxLength(64)] public string Status { get; set; } = "Idle";
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class Material : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(128)] public string Spec { get; set; } = "";
|
||||
[MaxLength(32)] public string Unit { get; set; } = "pcs";
|
||||
[MaxLength(64)] public string Category { get; set; } = "";
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class ContainerLocation : EntityBase
|
||||
{
|
||||
public Guid ContainerId { get; set; }
|
||||
[MaxLength(32)] public string LocationType { get; set; } = ContainerLocationTypes.Storage;
|
||||
[MaxLength(64)] public string LocationId { get; set; } = "";
|
||||
[MaxLength(64)] public string LocationCode { get; set; } = "";
|
||||
[MaxLength(128)] public string LocationName { get; set; } = "";
|
||||
[MaxLength(32)] public string Status { get; set; } = ContainerLocationStatuses.Active;
|
||||
public DateTimeOffset EnteredAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ContainerMaterial : EntityBase
|
||||
{
|
||||
public Guid ContainerId { get; set; }
|
||||
public Guid MaterialId { get; set; }
|
||||
public decimal Quantity { get; set; }
|
||||
[MaxLength(64)] public string BatchNo { get; set; } = "";
|
||||
[MaxLength(64)] public string SerialNo { get; set; } = "";
|
||||
[MaxLength(32)] public string Status { get; set; } = ContainerMaterialStatuses.Loaded;
|
||||
public DateTimeOffset LoadedAt { get; set; }
|
||||
public DateTimeOffset? UnloadedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ContainerLocationHistory : WarehouseHistoryBase
|
||||
{
|
||||
[MaxLength(32)] public string FromLocationType { get; set; } = "";
|
||||
[MaxLength(64)] public string FromLocationId { get; set; } = "";
|
||||
[MaxLength(32)] public string ToLocationType { get; set; } = "";
|
||||
[MaxLength(64)] public string ToLocationId { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class ContainerMaterialHistory : WarehouseHistoryBase
|
||||
{
|
||||
public Guid? MaterialId { get; set; }
|
||||
public decimal QuantityDelta { get; set; }
|
||||
}
|
||||
|
||||
public static class ContainerLocationTypes
|
||||
{
|
||||
public const string Storage = "Storage";
|
||||
public const string Car = "Car";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Storage, Car };
|
||||
}
|
||||
|
||||
public static class ContainerLocationStatuses
|
||||
{
|
||||
public const string Active = "Active";
|
||||
public const string Locked = "Locked";
|
||||
public const string Exception = "Exception";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Active, Locked, Exception };
|
||||
}
|
||||
|
||||
public static class ContainerMaterialStatuses
|
||||
{
|
||||
public const string Loaded = "Loaded";
|
||||
public const string Unloaded = "Unloaded";
|
||||
public const string Adjusted = "Adjusted";
|
||||
public const string Frozen = "Frozen";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Loaded, Unloaded, Adjusted, Frozen };
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MiGu.Server.Persistence;
|
||||
|
||||
namespace MiGu.Server.Wms;
|
||||
|
||||
public sealed class WmsReferenceValidator
|
||||
{
|
||||
private readonly PlatformDbContext _db;
|
||||
|
||||
public WmsReferenceValidator(PlatformDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task EnsureAreaAsync(Guid id)
|
||||
{
|
||||
if (!await _db.WarehouseAreas.AnyAsync(x => x.Id == id))
|
||||
throw new InvalidOperationException("库区不存在");
|
||||
}
|
||||
|
||||
public async Task EnsureStorageAsync(Guid id)
|
||||
{
|
||||
if (!await _db.Storages.AnyAsync(x => x.Id == id))
|
||||
throw new InvalidOperationException("库位不存在");
|
||||
}
|
||||
|
||||
public async Task EnsureContainerAsync(Guid id)
|
||||
{
|
||||
if (!await _db.Containers.AnyAsync(x => x.Id == id))
|
||||
throw new InvalidOperationException("容器不存在");
|
||||
}
|
||||
|
||||
public async Task EnsureMaterialAsync(Guid id)
|
||||
{
|
||||
if (!await _db.Materials.AnyAsync(x => x.Id == id))
|
||||
throw new InvalidOperationException("物料不存在");
|
||||
}
|
||||
|
||||
public async Task<(string Code, string Name)> ResolveLocationSnapshotAsync(string locationType, string locationId)
|
||||
{
|
||||
if (!ContainerLocationTypes.All.Contains(locationType))
|
||||
throw new InvalidOperationException("位置类型无效");
|
||||
if (string.IsNullOrWhiteSpace(locationId))
|
||||
throw new InvalidOperationException("位置 ID 不能为空");
|
||||
|
||||
if (string.Equals(locationType, ContainerLocationTypes.Storage, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!Guid.TryParse(locationId, out var id)) throw new InvalidOperationException("库位 ID 格式无效");
|
||||
var s = await _db.Storages.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (s == null) throw new InvalidOperationException("库位不存在");
|
||||
return (s.Code, s.Name);
|
||||
}
|
||||
|
||||
// 车辆来自现有调度/投影系统,首期不建库内外键,保留原始 ID 并作为快照展示。
|
||||
return (locationId, locationId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MiGu.Server.Persistence;
|
||||
|
||||
namespace MiGu.Server.Wms;
|
||||
|
||||
public sealed class WmsService
|
||||
{
|
||||
private readonly PlatformDbContext _db;
|
||||
private readonly WmsReferenceValidator _refs;
|
||||
private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public WmsService(PlatformDbContext db, WmsReferenceValidator refs)
|
||||
{
|
||||
_db = db;
|
||||
_refs = refs;
|
||||
}
|
||||
|
||||
public Task<List<WarehouseArea>> Areas(string? q = null) =>
|
||||
FilterByKeyword(_db.WarehouseAreas.AsNoTracking().OrderBy(x => x.SortOrder).ThenBy(x => x.Code), q).ToListAsync();
|
||||
|
||||
public Task<List<Storage>> Storages(string? q = null) =>
|
||||
FilterByKeyword(_db.Storages.AsNoTracking().OrderBy(x => x.Code), q).ToListAsync();
|
||||
|
||||
public Task<List<Container>> Containers(string? q = null) =>
|
||||
FilterByKeyword(_db.Containers.AsNoTracking().OrderBy(x => x.Code), q).ToListAsync();
|
||||
|
||||
public Task<List<Material>> Materials(string? q = null) =>
|
||||
FilterByKeyword(_db.Materials.AsNoTracking().OrderBy(x => x.Code), q).ToListAsync();
|
||||
|
||||
public Task<List<ContainerLocation>> ContainerLocations(string? locationType = null, string? q = null)
|
||||
{
|
||||
var query = _db.ContainerLocations.AsNoTracking().OrderBy(x => x.ContainerId).AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(locationType)) query = query.Where(x => x.LocationType == locationType);
|
||||
return FilterByKeyword(query, q).ToListAsync();
|
||||
}
|
||||
|
||||
public Task<List<ContainerMaterial>> ContainerMaterials(string? q = null) =>
|
||||
FilterByKeyword(_db.ContainerMaterials.AsNoTracking().OrderBy(x => x.ContainerId), q).ToListAsync();
|
||||
|
||||
public async Task<WarehouseArea> SaveArea(MasterDataRequest req, string actor)
|
||||
{
|
||||
WarehouseArea entity;
|
||||
if (req.Id.HasValue)
|
||||
{
|
||||
entity = await FindEditable(_db.WarehouseAreas, req.Id.Value, req.Version);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = new WarehouseArea();
|
||||
StampCreate(entity, actor);
|
||||
_db.WarehouseAreas.Add(entity);
|
||||
}
|
||||
await EnsureUnique(_db.WarehouseAreas, x => x.Code == req.Code && x.Id != entity.Id, "库区编码已存在");
|
||||
entity.Code = req.Code.Trim();
|
||||
entity.Name = req.Name.Trim();
|
||||
entity.Type = req.Type.TrimOr("Storage");
|
||||
entity.Enabled = req.Enabled;
|
||||
entity.SortOrder = req.SortOrder;
|
||||
ApplyCommon(entity, req, actor);
|
||||
await _db.SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task<Storage> SaveStorage(StorageRequest req, string actor)
|
||||
{
|
||||
await _refs.EnsureAreaAsync(req.AreaId);
|
||||
Storage entity;
|
||||
if (req.Id.HasValue)
|
||||
{
|
||||
entity = await FindEditable(_db.Storages, req.Id.Value, req.Version);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = new Storage();
|
||||
StampCreate(entity, actor);
|
||||
_db.Storages.Add(entity);
|
||||
}
|
||||
await EnsureUnique(_db.Storages, x => x.Code == req.Code && x.Id != entity.Id, "库位编码已存在");
|
||||
entity.AreaId = req.AreaId;
|
||||
entity.Code = req.Code.Trim();
|
||||
entity.Name = req.Name.Trim();
|
||||
entity.StorageType = req.StorageType.TrimOr("Storage");
|
||||
entity.SiteId = req.SiteId.TrimOr("");
|
||||
entity.Capacity = req.Capacity;
|
||||
entity.Enabled = req.Enabled;
|
||||
ApplyCommon(entity, req, actor);
|
||||
await _db.SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task<Container> SaveContainer(MasterDataRequest req, string actor)
|
||||
{
|
||||
Container entity;
|
||||
if (req.Id.HasValue)
|
||||
{
|
||||
entity = await FindEditable(_db.Containers, req.Id.Value, req.Version);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = new Container();
|
||||
StampCreate(entity, actor);
|
||||
_db.Containers.Add(entity);
|
||||
}
|
||||
await EnsureUnique(_db.Containers, x => x.Code == req.Code && x.Id != entity.Id, "容器编码已存在");
|
||||
entity.Code = req.Code.Trim();
|
||||
entity.Name = req.Name.Trim();
|
||||
entity.ContainerType = req.Type.TrimOr("Box");
|
||||
entity.Status = req.Status.TrimOr("Idle");
|
||||
entity.Enabled = req.Enabled;
|
||||
ApplyCommon(entity, req, actor);
|
||||
await _db.SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task<Material> SaveMaterial(MaterialRequest req, string actor)
|
||||
{
|
||||
Material entity;
|
||||
if (req.Id.HasValue)
|
||||
{
|
||||
entity = await FindEditable(_db.Materials, req.Id.Value, req.Version);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = new Material();
|
||||
StampCreate(entity, actor);
|
||||
_db.Materials.Add(entity);
|
||||
}
|
||||
await EnsureUnique(_db.Materials, x => x.Code == req.Code && x.Id != entity.Id, "物料编码已存在");
|
||||
entity.Code = req.Code.Trim();
|
||||
entity.Name = req.Name.Trim();
|
||||
entity.Spec = req.Spec.TrimOr("");
|
||||
entity.Unit = req.Unit.TrimOr("pcs");
|
||||
entity.Category = req.Category.TrimOr("");
|
||||
entity.Enabled = req.Enabled;
|
||||
ApplyCommon(entity, req, actor);
|
||||
await _db.SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task DeleteEntity<T>(Guid id, long? version, string actor) where T : EntityBase
|
||||
{
|
||||
var set = _db.Set<T>();
|
||||
var entity = await FindEditable(set, id, version);
|
||||
entity.IsDeleted = true;
|
||||
entity.DeletedAt = DateTimeOffset.UtcNow;
|
||||
entity.DeletedBy = actor;
|
||||
entity.UpdatedBy = actor;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<ContainerLocation> BindOrTransferLocation(ContainerLocationRequest req, string actor)
|
||||
{
|
||||
await _refs.EnsureContainerAsync(req.ContainerId);
|
||||
var (code, name) = await _refs.ResolveLocationSnapshotAsync(req.LocationType, req.LocationId);
|
||||
if (!ContainerLocationStatuses.All.Contains(req.Status))
|
||||
throw new InvalidOperationException("容器位置状态无效");
|
||||
|
||||
var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == req.ContainerId);
|
||||
var before = current == null ? null : Snapshot(current);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
current = new ContainerLocation { ContainerId = req.ContainerId, EnteredAt = now };
|
||||
StampCreate(current, actor);
|
||||
_db.ContainerLocations.Add(current);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureVersion(current, req.Version);
|
||||
EnsureUnlocked(current);
|
||||
}
|
||||
|
||||
current.LocationType = req.LocationType;
|
||||
current.LocationId = req.LocationId.Trim();
|
||||
current.LocationCode = code;
|
||||
current.LocationName = name;
|
||||
current.Status = req.Status;
|
||||
current.EnteredAt = req.EnteredAt ?? now;
|
||||
ApplyCommon(current, req, actor);
|
||||
|
||||
_db.ContainerLocationHistories.Add(new ContainerLocationHistory
|
||||
{
|
||||
RelationId = current.Id,
|
||||
ContainerId = current.ContainerId,
|
||||
EventType = before == null ? "Bind" : "Transfer",
|
||||
FromLocationType = before?.LocationType ?? "",
|
||||
FromLocationId = before?.LocationId ?? "",
|
||||
ToLocationType = current.LocationType,
|
||||
ToLocationId = current.LocationId,
|
||||
BeforeJson = before == null ? "{}" : JsonSerializer.Serialize(before, _json),
|
||||
AfterJson = JsonSerializer.Serialize(Snapshot(current), _json),
|
||||
Operator = actor,
|
||||
OperatedAt = now,
|
||||
Source = req.Source.TrimOr("Manual"),
|
||||
Reason = req.Reason.TrimOr(""),
|
||||
Remark = req.Remark.TrimOr(""),
|
||||
Extend = NormalizeExtend(req.Extend)
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return current;
|
||||
}
|
||||
|
||||
public async Task UnbindLocation(Guid containerId, string actor, string reason = "")
|
||||
{
|
||||
var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == containerId)
|
||||
?? throw new InvalidOperationException("容器当前位置不存在");
|
||||
EnsureUnlocked(current);
|
||||
var before = Snapshot(current);
|
||||
_db.ContainerLocationHistories.Add(new ContainerLocationHistory
|
||||
{
|
||||
RelationId = current.Id,
|
||||
ContainerId = current.ContainerId,
|
||||
EventType = "Unbind",
|
||||
FromLocationType = current.LocationType,
|
||||
FromLocationId = current.LocationId,
|
||||
BeforeJson = JsonSerializer.Serialize(before, _json),
|
||||
AfterJson = "{}",
|
||||
Operator = actor,
|
||||
OperatedAt = DateTimeOffset.UtcNow,
|
||||
Source = "Manual",
|
||||
Reason = reason
|
||||
});
|
||||
_db.ContainerLocations.Remove(current);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<ContainerMaterial> SaveContainerMaterial(ContainerMaterialRequest req, string actor)
|
||||
{
|
||||
await _refs.EnsureContainerAsync(req.ContainerId);
|
||||
await _refs.EnsureMaterialAsync(req.MaterialId);
|
||||
if (req.Quantity <= 0) throw new InvalidOperationException("数量必须大于 0");
|
||||
if (!ContainerMaterialStatuses.All.Contains(req.Status))
|
||||
throw new InvalidOperationException("容器物料状态无效");
|
||||
|
||||
var current = await _db.ContainerMaterials.FirstOrDefaultAsync(x =>
|
||||
x.ContainerId == req.ContainerId && x.MaterialId == req.MaterialId &&
|
||||
x.BatchNo == req.BatchNo.TrimOr("") && x.SerialNo == req.SerialNo.TrimOr(""));
|
||||
var before = current == null ? null : Snapshot(current);
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
current = new ContainerMaterial { ContainerId = req.ContainerId, MaterialId = req.MaterialId, LoadedAt = req.LoadedAt ?? DateTimeOffset.UtcNow };
|
||||
StampCreate(current, actor);
|
||||
_db.ContainerMaterials.Add(current);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureVersion(current, req.Version);
|
||||
EnsureUnlocked(current);
|
||||
}
|
||||
|
||||
var oldQty = current.Quantity;
|
||||
current.Quantity = req.Quantity;
|
||||
current.BatchNo = req.BatchNo.TrimOr("");
|
||||
current.SerialNo = req.SerialNo.TrimOr("");
|
||||
current.Status = req.Status;
|
||||
current.LoadedAt = req.LoadedAt ?? current.LoadedAt;
|
||||
current.UnloadedAt = req.UnloadedAt;
|
||||
ApplyCommon(current, req, actor);
|
||||
|
||||
_db.ContainerMaterialHistories.Add(new ContainerMaterialHistory
|
||||
{
|
||||
RelationId = current.Id,
|
||||
ContainerId = current.ContainerId,
|
||||
MaterialId = current.MaterialId,
|
||||
EventType = before == null ? "Load" : "Adjust",
|
||||
QuantityDelta = current.Quantity - oldQty,
|
||||
BeforeJson = before == null ? "{}" : JsonSerializer.Serialize(before, _json),
|
||||
AfterJson = JsonSerializer.Serialize(Snapshot(current), _json),
|
||||
Operator = actor,
|
||||
OperatedAt = DateTimeOffset.UtcNow,
|
||||
Source = req.Source.TrimOr("Manual"),
|
||||
Reason = req.Reason.TrimOr(""),
|
||||
Remark = req.Remark.TrimOr(""),
|
||||
Extend = NormalizeExtend(req.Extend)
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return current;
|
||||
}
|
||||
|
||||
public async Task UnloadMaterial(Guid id, string actor, string reason = "")
|
||||
{
|
||||
var current = await _db.ContainerMaterials.FirstOrDefaultAsync(x => x.Id == id)
|
||||
?? throw new InvalidOperationException("容器物料不存在");
|
||||
EnsureUnlocked(current);
|
||||
var before = Snapshot(current);
|
||||
_db.ContainerMaterialHistories.Add(new ContainerMaterialHistory
|
||||
{
|
||||
RelationId = current.Id,
|
||||
ContainerId = current.ContainerId,
|
||||
MaterialId = current.MaterialId,
|
||||
EventType = "Unload",
|
||||
QuantityDelta = -current.Quantity,
|
||||
BeforeJson = JsonSerializer.Serialize(before, _json),
|
||||
AfterJson = "{}",
|
||||
Operator = actor,
|
||||
OperatedAt = DateTimeOffset.UtcNow,
|
||||
Source = "Manual",
|
||||
Reason = reason
|
||||
});
|
||||
_db.ContainerMaterials.Remove(current);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<ContainerLocationHistory>> LocationHistory(Guid? containerId = null)
|
||||
{
|
||||
var rows = await (containerId.HasValue
|
||||
? _db.ContainerLocationHistories.AsNoTracking().Where(x => x.ContainerId == containerId.Value)
|
||||
: _db.ContainerLocationHistories.AsNoTracking())
|
||||
.ToListAsync();
|
||||
|
||||
return rows.OrderByDescending(x => x.OperatedAt).Take(500).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<ContainerMaterialHistory>> MaterialHistory(Guid? containerId = null, Guid? materialId = null)
|
||||
{
|
||||
var query = _db.ContainerMaterialHistories.AsNoTracking().AsQueryable();
|
||||
if (containerId.HasValue) query = query.Where(x => x.ContainerId == containerId.Value);
|
||||
if (materialId.HasValue) query = query.Where(x => x.MaterialId == materialId.Value);
|
||||
|
||||
var rows = await query.ToListAsync();
|
||||
return rows.OrderByDescending(x => x.OperatedAt).Take(500).ToList();
|
||||
}
|
||||
|
||||
private async Task<T> FindEditable<T>(DbSet<T> set, Guid id, long? version) where T : EntityBase
|
||||
{
|
||||
var entity = await set.FirstOrDefaultAsync(x => x.Id == id) ?? throw new InvalidOperationException("数据不存在");
|
||||
EnsureVersion(entity, version);
|
||||
EnsureUnlocked(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static void EnsureVersion(EntityBase entity, long? version)
|
||||
{
|
||||
if (version.HasValue && entity.Version != version.Value)
|
||||
throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试");
|
||||
}
|
||||
|
||||
private static void EnsureUnlocked(EntityBase entity)
|
||||
{
|
||||
if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改");
|
||||
}
|
||||
|
||||
private static void StampCreate(EntityBase entity, string actor)
|
||||
{
|
||||
entity.CreatedBy = actor;
|
||||
entity.UpdatedBy = actor;
|
||||
}
|
||||
|
||||
private void ApplyCommon(EntityBase entity, CommonRequest req, string actor)
|
||||
{
|
||||
entity.IsLock = req.IsLock;
|
||||
entity.Remark = req.Remark.TrimOr("");
|
||||
entity.Extend = NormalizeExtend(req.Extend);
|
||||
entity.UpdatedBy = actor;
|
||||
}
|
||||
|
||||
private static async Task EnsureUnique<T>(IQueryable<T> query, System.Linq.Expressions.Expression<Func<T, bool>> predicate, string message)
|
||||
{
|
||||
if (await query.AnyAsync(predicate)) throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
private string NormalizeExtend(string? extend)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(extend)) return "{}";
|
||||
if (extend.Length > 4000) throw new InvalidOperationException("扩展字段过长");
|
||||
using var doc = JsonDocument.Parse(extend);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) throw new InvalidOperationException("扩展字段必须是 JSON object");
|
||||
return extend;
|
||||
}
|
||||
|
||||
private static IQueryable<T> FilterByKeyword<T>(IQueryable<T> query, string? q)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(q)) return query;
|
||||
var s = q.Trim();
|
||||
return typeof(T).Name switch
|
||||
{
|
||||
nameof(WarehouseArea) => (IQueryable<T>)((IQueryable<WarehouseArea>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s)),
|
||||
nameof(Storage) => (IQueryable<T>)((IQueryable<Storage>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s) || x.SiteId.Contains(s)),
|
||||
nameof(Container) => (IQueryable<T>)((IQueryable<Container>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s)),
|
||||
nameof(Material) => (IQueryable<T>)((IQueryable<Material>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s) || x.Spec.Contains(s)),
|
||||
nameof(ContainerLocation) => (IQueryable<T>)((IQueryable<ContainerLocation>)query).Where(x => x.LocationCode.Contains(s) || x.LocationName.Contains(s)),
|
||||
nameof(ContainerMaterial) => query,
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
|
||||
private static ContainerLocationSnapshot Snapshot(ContainerLocation x) => new(
|
||||
x.Id, x.ContainerId, x.LocationType, x.LocationId, x.LocationCode, x.LocationName, x.Status, x.EnteredAt, x.Version);
|
||||
|
||||
private static ContainerMaterialSnapshot Snapshot(ContainerMaterial x) => new(
|
||||
x.Id, x.ContainerId, x.MaterialId, x.Quantity, x.BatchNo, x.SerialNo, x.Status, x.LoadedAt, x.UnloadedAt, x.Version);
|
||||
}
|
||||
|
||||
public sealed record ContainerLocationSnapshot(
|
||||
Guid Id,
|
||||
Guid ContainerId,
|
||||
string LocationType,
|
||||
string LocationId,
|
||||
string LocationCode,
|
||||
string LocationName,
|
||||
string Status,
|
||||
DateTimeOffset EnteredAt,
|
||||
long Version);
|
||||
|
||||
public sealed record ContainerMaterialSnapshot(
|
||||
Guid Id,
|
||||
Guid ContainerId,
|
||||
Guid MaterialId,
|
||||
decimal Quantity,
|
||||
string BatchNo,
|
||||
string SerialNo,
|
||||
string Status,
|
||||
DateTimeOffset LoadedAt,
|
||||
DateTimeOffset? UnloadedAt,
|
||||
long Version);
|
||||
|
||||
public abstract record CommonRequest(
|
||||
Guid? Id,
|
||||
long? Version,
|
||||
bool IsLock,
|
||||
string Remark,
|
||||
string Extend);
|
||||
|
||||
public sealed record MasterDataRequest(
|
||||
Guid? Id,
|
||||
long? Version,
|
||||
string Code,
|
||||
string Name,
|
||||
string Type,
|
||||
string Status,
|
||||
bool Enabled,
|
||||
int SortOrder,
|
||||
bool IsLock,
|
||||
string Remark,
|
||||
string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||
|
||||
public sealed record StorageRequest(
|
||||
Guid? Id,
|
||||
long? Version,
|
||||
Guid AreaId,
|
||||
string Code,
|
||||
string Name,
|
||||
string StorageType,
|
||||
string SiteId,
|
||||
int Capacity,
|
||||
bool Enabled,
|
||||
bool IsLock,
|
||||
string Remark,
|
||||
string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||
|
||||
public sealed record MaterialRequest(
|
||||
Guid? Id,
|
||||
long? Version,
|
||||
string Code,
|
||||
string Name,
|
||||
string Spec,
|
||||
string Unit,
|
||||
string Category,
|
||||
bool Enabled,
|
||||
bool IsLock,
|
||||
string Remark,
|
||||
string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||
|
||||
public sealed record ContainerLocationRequest(
|
||||
Guid? Id,
|
||||
long? Version,
|
||||
Guid ContainerId,
|
||||
string LocationType,
|
||||
string LocationId,
|
||||
string Status,
|
||||
DateTimeOffset? EnteredAt,
|
||||
string Source,
|
||||
string Reason,
|
||||
bool IsLock,
|
||||
string Remark,
|
||||
string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||
|
||||
public sealed record ContainerMaterialRequest(
|
||||
Guid? Id,
|
||||
long? Version,
|
||||
Guid ContainerId,
|
||||
Guid MaterialId,
|
||||
decimal Quantity,
|
||||
string BatchNo,
|
||||
string SerialNo,
|
||||
string Status,
|
||||
DateTimeOffset? LoadedAt,
|
||||
DateTimeOffset? UnloadedAt,
|
||||
string Source,
|
||||
string Reason,
|
||||
bool IsLock,
|
||||
string Remark,
|
||||
string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||
|
||||
public static class WmsStringExtensions
|
||||
{
|
||||
public static string TrimOr(this string? value, string fallback) =>
|
||||
string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
|
||||
|
||||
public static string ActorName(this ClaimsPrincipal user) =>
|
||||
user.FindFirst(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.UniqueName)?.Value
|
||||
?? user.Identity?.Name
|
||||
?? "system";
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Gl4ghnjUoi2/dEh1Uv4DE5qjuqefIqSpVwg5/ZbIlvdq1g93+vFuwX30N+NRSdzU
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"section": "auth",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.4979022+00:00",
|
||||
"payload": {
|
||||
"roles": [
|
||||
{
|
||||
"id": "role-admin",
|
||||
"name": "\u7BA1\u7406\u5458",
|
||||
"scope": "Platform",
|
||||
"permissions": [
|
||||
"*"
|
||||
],
|
||||
"widgetGrants": []
|
||||
},
|
||||
{
|
||||
"id": "role-ops",
|
||||
"name": "\u8FD0\u8425",
|
||||
"scope": "RCSMonitor",
|
||||
"permissions": [
|
||||
"ops.car.pause",
|
||||
"ops.car.resume",
|
||||
"ops.car.gohome",
|
||||
"ops.task.pause",
|
||||
"ops.task.cancel",
|
||||
"ops.task.reassign",
|
||||
"ops.task.boostPriority",
|
||||
"monitor.note.write"
|
||||
],
|
||||
"widgetGrants": [
|
||||
{
|
||||
"widgetId": "MapEditor",
|
||||
"visibility": "readonly"
|
||||
},
|
||||
{
|
||||
"widgetId": "CadToolbar",
|
||||
"visibility": "hidden"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"id": "u-admin",
|
||||
"username": "admin",
|
||||
"roles": [
|
||||
"role-admin"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "u-ops",
|
||||
"username": "ops",
|
||||
"roles": [
|
||||
"role-ops"
|
||||
],
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"section": "charge",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.4849334+00:00",
|
||||
"payload": {
|
||||
"allowMidTaskCharge": false,
|
||||
"idleChargeAfterSec": 300,
|
||||
"priority": [
|
||||
{
|
||||
"id": "CP1",
|
||||
"condition": "soc\u003C0.2",
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"id": "CP2",
|
||||
"condition": "idle\u003E5min",
|
||||
"weight": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"section": "deployment",
|
||||
"version": 7,
|
||||
"updatedAt": "2026-06-09T01:43:46.9419888+00:00",
|
||||
"payload": {
|
||||
"configured": true,
|
||||
"platformType": "standard",
|
||||
"modules": [
|
||||
"wms"
|
||||
],
|
||||
"navigationKinds": [
|
||||
"qrcode",
|
||||
"laser"
|
||||
],
|
||||
"scenarios": [
|
||||
"tpl-p2p"
|
||||
],
|
||||
"updatedBy": "admin"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"section": "device",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.5034914+00:00",
|
||||
"payload": {
|
||||
"drivers": [
|
||||
{
|
||||
"id": "drv-elev",
|
||||
"deviceType": "\u7535\u68AF",
|
||||
"driverName": "OpcUaElevatorDriver",
|
||||
"version": "1.2.0"
|
||||
},
|
||||
{
|
||||
"id": "drv-chrg",
|
||||
"deviceType": "\u5145\u7535\u6869",
|
||||
"driverName": "ModbusChargerDriver",
|
||||
"version": "1.0.5"
|
||||
},
|
||||
{
|
||||
"id": "drv-cam",
|
||||
"deviceType": "\u6444\u50CF\u5934",
|
||||
"driverName": "OnvifCameraDriver",
|
||||
"version": "2.1.0"
|
||||
}
|
||||
],
|
||||
"devices": [
|
||||
{
|
||||
"id": "dev-elev-1",
|
||||
"name": "#1 \u7535\u68AF",
|
||||
"deviceType": "\u7535\u68AF",
|
||||
"protocol": "opc-ua",
|
||||
"address": "opc.tcp://10.0.2.20:4840",
|
||||
"driverId": "drv-elev",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "dev-chrg-1",
|
||||
"name": "\u5145\u7535\u6869-A1",
|
||||
"deviceType": "\u5145\u7535\u6869",
|
||||
"protocol": "modbus-tcp",
|
||||
"address": "10.0.2.30:502",
|
||||
"driverId": "drv-chrg",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"healthPolicy": {
|
||||
"heartbeatSec": 5,
|
||||
"offlineSec": 30
|
||||
},
|
||||
"alarmPolicy": {
|
||||
"enabled": true,
|
||||
"rules": [
|
||||
{
|
||||
"level": "warn",
|
||||
"condition": "offline\u003E30s"
|
||||
},
|
||||
{
|
||||
"level": "error",
|
||||
"condition": "driverException"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"section": "fleet",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.5109848+00:00",
|
||||
"payload": {
|
||||
"groups": [
|
||||
{
|
||||
"id": "G-A",
|
||||
"name": "A \u533A\u8F66\u961F",
|
||||
"floor": "F1",
|
||||
"region": "A",
|
||||
"carIds": [
|
||||
"C01",
|
||||
"C02",
|
||||
"C03"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "G-B",
|
||||
"name": "B \u533A\u8F66\u961F",
|
||||
"floor": "F1",
|
||||
"region": "B",
|
||||
"carIds": [
|
||||
"C04",
|
||||
"C05"
|
||||
]
|
||||
}
|
||||
],
|
||||
"ota": {
|
||||
"enabled": true,
|
||||
"batchSize": 2,
|
||||
"rollbackOnFail": true
|
||||
},
|
||||
"batchOps": {
|
||||
"confirmationRequired": true,
|
||||
"maxBatch": 10
|
||||
},
|
||||
"networkDiag": {
|
||||
"rttThresholdMs": 80,
|
||||
"packetLossThreshold": 0.02
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"section": "integrations",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.4625787+00:00",
|
||||
"payload": {
|
||||
"mes": [
|
||||
{
|
||||
"id": "mes-1",
|
||||
"name": "MES \u4E3B\u7EBF",
|
||||
"url": "http://mes.lan/api",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"wms": [
|
||||
{
|
||||
"id": "wms-1",
|
||||
"name": "WMS \u4ED3\u50A8",
|
||||
"url": "http://wms.lan/api",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"rcs": [],
|
||||
"ptl": [
|
||||
{
|
||||
"id": "ptl-1",
|
||||
"name": "PTL \u62E3\u9009",
|
||||
"url": "http://ptl.lan/api",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"section": "location",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.5220964+00:00",
|
||||
"payload": {
|
||||
"locations": [
|
||||
{
|
||||
"id": "L01",
|
||||
"code": "A-01",
|
||||
"name": "A \u533A\u8D27\u67B6 1",
|
||||
"siteId": "S001",
|
||||
"capacity": 20,
|
||||
"occupied": 12
|
||||
},
|
||||
{
|
||||
"id": "L02",
|
||||
"code": "A-02",
|
||||
"name": "A \u533A\u8D27\u67B6 2",
|
||||
"siteId": "S002",
|
||||
"capacity": 20,
|
||||
"occupied": 7
|
||||
},
|
||||
{
|
||||
"id": "L03",
|
||||
"code": "B-01",
|
||||
"name": "B \u533A\u7F13\u5B58",
|
||||
"siteId": "S003",
|
||||
"capacity": 30,
|
||||
"occupied": 25
|
||||
}
|
||||
],
|
||||
"inventoryRules": [
|
||||
{
|
||||
"id": "IR1",
|
||||
"itemType": "PalletA",
|
||||
"minQty": 5,
|
||||
"maxQty": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"section": "ops",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.5273514+00:00",
|
||||
"payload": {
|
||||
"playback": {
|
||||
"retentionDays": 30,
|
||||
"samplingHz": 5
|
||||
},
|
||||
"logRetention": {
|
||||
"hotDays": 7,
|
||||
"coldDays": 180
|
||||
},
|
||||
"version": {
|
||||
"keepReleases": 5
|
||||
},
|
||||
"monitor": {
|
||||
"car": {
|
||||
"propertyKeys": [],
|
||||
"statusKeys": [],
|
||||
"actionKeys": []
|
||||
},
|
||||
"site": {
|
||||
"propertyKeys": [],
|
||||
"statusKeys": [],
|
||||
"actionKeys": []
|
||||
},
|
||||
"track": {
|
||||
"propertyKeys": [],
|
||||
"statusKeys": [],
|
||||
"actionKeys": []
|
||||
},
|
||||
"carActionByType": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"section": "routing",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.4685445+00:00",
|
||||
"payload": {
|
||||
"algorithm": "astar",
|
||||
"weights": {
|
||||
"distance": 1,
|
||||
"congestion": 0.5,
|
||||
"turnPenalty": 0.2
|
||||
},
|
||||
"avoidance": [
|
||||
{
|
||||
"id": "AV1",
|
||||
"zoneId": "Z-NORTH",
|
||||
"rule": "no-entry-while-loading"
|
||||
}
|
||||
],
|
||||
"zoneSpeedLimits": [
|
||||
{
|
||||
"zoneId": "Z-NARROW",
|
||||
"maxSpeedMps": 0.5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"section": "scenario",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.5171692+00:00",
|
||||
"payload": {
|
||||
"templates": [
|
||||
{
|
||||
"id": "tpl-sps",
|
||||
"name": "SPS \u7269\u6599\u914D\u9001",
|
||||
"category": "SPS",
|
||||
"version": "1.0.0",
|
||||
"baselineJson": "{}"
|
||||
},
|
||||
{
|
||||
"id": "tpl-pack",
|
||||
"name": "\u7535\u6C60 Pack \u81EA\u52A8\u5316\u4EA7\u7EBF",
|
||||
"category": "BatteryPack",
|
||||
"version": "1.0.0",
|
||||
"baselineJson": "{}"
|
||||
},
|
||||
{
|
||||
"id": "tpl-loop",
|
||||
"name": "\u73AF\u7EBF\u8FD0\u884C",
|
||||
"category": "Loop",
|
||||
"version": "1.0.0",
|
||||
"baselineJson": "{}"
|
||||
},
|
||||
{
|
||||
"id": "tpl-p2p",
|
||||
"name": "\u70B9\u5BF9\u70B9\u67D4\u6027\u642C\u8FD0",
|
||||
"category": "P2P",
|
||||
"version": "1.0.0",
|
||||
"baselineJson": "{}"
|
||||
}
|
||||
],
|
||||
"dslPolicy": {
|
||||
"enabled": true,
|
||||
"schemaVersion": "1"
|
||||
},
|
||||
"lowCode": {
|
||||
"enabled": false,
|
||||
"editor": "json"
|
||||
},
|
||||
"versionPolicy": {
|
||||
"keepVersions": 10,
|
||||
"allowRollback": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"section": "system",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.4470202+00:00",
|
||||
"payload": {
|
||||
"dispatchLoopHz": 50,
|
||||
"log": {
|
||||
"level": "info",
|
||||
"rollDays": 7,
|
||||
"maxSizeMB": 256
|
||||
},
|
||||
"security": {
|
||||
"jwtExpireMin": 1440,
|
||||
"enableSwagger": false,
|
||||
"corsWhitelist": [
|
||||
"http://localhost:5173"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"section": "task",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.4893595+00:00",
|
||||
"payload": {
|
||||
"mode": "leastLoad",
|
||||
"loadBalance": true,
|
||||
"maxQueuePerCar": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"section": "traffic",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.4927371+00:00",
|
||||
"payload": {
|
||||
"intersections": [
|
||||
{
|
||||
"id": "IX1",
|
||||
"siteIds": [
|
||||
"S006",
|
||||
"S007"
|
||||
],
|
||||
"mode": "mutex"
|
||||
}
|
||||
],
|
||||
"mutex": [
|
||||
{
|
||||
"id": "MZ1",
|
||||
"zoneIds": [
|
||||
"Z-CROSS"
|
||||
]
|
||||
}
|
||||
],
|
||||
"yields": [
|
||||
{
|
||||
"id": "YD1",
|
||||
"from": "A \u533A",
|
||||
"to": "B \u533A",
|
||||
"condition": "priority\u003Cpeer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"section": "vehicle",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.4789283+00:00",
|
||||
"payload": {
|
||||
"lowBatteryThreshold": 0.3,
|
||||
"criticalBatteryThreshold": 0.15,
|
||||
"faultReport": {
|
||||
"enabled": true,
|
||||
"emailTo": [
|
||||
"ops@example.com"
|
||||
]
|
||||
},
|
||||
"autoRepair": {
|
||||
"enabled": false,
|
||||
"cooldownSec": 600
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"section": "widget",
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-08T09:43:37.5362337+00:00",
|
||||
"payload": {
|
||||
"items": [
|
||||
{
|
||||
"id": "widget-call-button",
|
||||
"name": "\u547C\u53EB\u6309\u94AE",
|
||||
"schemaJson": "{\u0022fields\u0022:[{\u0022name\u0022:\u0022siteId\u0022}]}",
|
||||
"layoutJson": "{\u0022x\u0022:0,\u0022y\u0022:0,\u0022w\u0022:2,\u0022h\u0022:1}",
|
||||
"bindToScopes": [
|
||||
"RCSMonitor"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"version": 1,
|
||||
"roles": [
|
||||
{
|
||||
"id": "role-admin",
|
||||
"name": "\u8D85\u7EA7\u7BA1\u7406\u5458",
|
||||
"description": "\u62E5\u6709\u5168\u90E8\u9875\u9762\u4E0E\u64CD\u4F5C\u6743\u9650\u7684\u5185\u7F6E\u89D2\u8272",
|
||||
"scope": "*",
|
||||
"pages": [
|
||||
"*"
|
||||
],
|
||||
"ops": [
|
||||
"*"
|
||||
],
|
||||
"widgetGrants": [],
|
||||
"system": true
|
||||
},
|
||||
{
|
||||
"id": "role-ops",
|
||||
"name": "\u8FD0\u8425\u4EBA\u5458",
|
||||
"description": "\u8FD0\u8425\u76D1\u63A7\u7AEF\u9ED8\u8BA4\u89D2\u8272\uFF1A\u53EF\u6267\u884C\u8FD0\u7EF4\u64CD\u4F5C\u3001\u67E5\u770B\u76D1\u63A7",
|
||||
"scope": "RCSMonitor",
|
||||
"pages": [
|
||||
"monitor-dashboard",
|
||||
"monitor-map",
|
||||
"monitor-ops",
|
||||
"monitor-notes"
|
||||
],
|
||||
"ops": [
|
||||
"ops.car.pause",
|
||||
"ops.car.resume",
|
||||
"ops.car.gohome",
|
||||
"ops.car.resetSession",
|
||||
"ops.car.manualCharge",
|
||||
"ops.task.pause",
|
||||
"ops.task.cancel",
|
||||
"ops.task.reassign",
|
||||
"ops.task.boostPriority",
|
||||
"monitor.note.write"
|
||||
],
|
||||
"widgetGrants": [
|
||||
{
|
||||
"widgetId": "MapEditor",
|
||||
"visibility": "readonly"
|
||||
},
|
||||
{
|
||||
"widgetId": "CadToolbar",
|
||||
"visibility": "hidden"
|
||||
},
|
||||
{
|
||||
"widgetId": "CarPanel",
|
||||
"visibility": "readonly"
|
||||
},
|
||||
{
|
||||
"widgetId": "MissionEditor",
|
||||
"visibility": "readonly"
|
||||
},
|
||||
{
|
||||
"widgetId": "OpsActionPanel",
|
||||
"visibility": "interactive"
|
||||
},
|
||||
{
|
||||
"widgetId": "ConfigCenter",
|
||||
"visibility": "hidden"
|
||||
}
|
||||
],
|
||||
"system": true
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"id": "u-admin",
|
||||
"username": "admin",
|
||||
"displayName": "\u7CFB\u7EDF\u7BA1\u7406\u5458",
|
||||
"enabled": true,
|
||||
"roleIds": [
|
||||
"role-admin"
|
||||
],
|
||||
"salt": "pQlotjtkEe0S5MTxecJd4A==",
|
||||
"passwordHash": "qs6A3I/Y3OwV\u002Bs95orIWvzcUz8bF4ZY9l1jq4SQd1cI="
|
||||
},
|
||||
{
|
||||
"id": "u-ops",
|
||||
"username": "ops",
|
||||
"displayName": "\u8FD0\u8425\u4EBA\u5458",
|
||||
"enabled": true,
|
||||
"roleIds": [
|
||||
"role-ops"
|
||||
],
|
||||
"salt": "G6c5\u002BnLEjf3fEor1LgNjyg==",
|
||||
"passwordHash": "Zb1QiDzHctduFbZGKniTEy4OLJIvIOeoyLfc\u002Bo0y\u002BVg="
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user