diff --git a/.gitignore b/.gitignore index 535cae5..10f0913 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ frontends/apps/simple-platform-vue/auto-imports.d.ts # 临时构建输出 .tmp-build*/ +_build_out.txt +.tools/ # SimpleLite 运行目录 /SimpleLite/ diff --git a/MiGu.DB/Abstractions/Entities/Capabilities.cs b/MiGu.DB/Abstractions/Entities/Capabilities.cs new file mode 100644 index 0000000..b3a27e4 --- /dev/null +++ b/MiGu.DB/Abstractions/Entities/Capabilities.cs @@ -0,0 +1,45 @@ +namespace MiGu.DB.Abstractions.Entities; + +public interface IEntity +{ + TKey Id { get; set; } +} + +public interface IAuditable +{ + DateTimeOffset CreatedAt { get; set; } + DateTimeOffset UpdatedAt { get; set; } + string CreatedBy { get; set; } + string UpdatedBy { get; set; } +} + +public interface ISoftDeletable +{ + bool IsDeleted { get; set; } + DateTimeOffset? DeletedAt { get; set; } + string DeletedBy { get; set; } +} + +public interface IVersioned +{ + long Version { get; set; } +} + +public interface ILockable +{ + bool IsLock { get; set; } +} + +public interface IRemarkable +{ + string Remark { get; set; } +} + +public interface IExtendable +{ + string Extend { get; set; } +} + +public interface IHistoryEntry +{ +} diff --git a/MiGu.DB/Abstractions/Exceptions/Exceptions.cs b/MiGu.DB/Abstractions/Exceptions/Exceptions.cs new file mode 100644 index 0000000..c0322c8 --- /dev/null +++ b/MiGu.DB/Abstractions/Exceptions/Exceptions.cs @@ -0,0 +1,42 @@ +namespace MiGu.DB.Abstractions.Exceptions; + +public sealed class ConcurrencyConflictException : Exception +{ + public string EntityType { get; } + public object? EntityId { get; } + public long? ExpectedVersion { get; } + + public ConcurrencyConflictException(string entityType, object? entityId, long? expectedVersion = null) + : base("数据已被其他用户修改,请刷新后重试") + { + EntityType = entityType; + EntityId = entityId; + ExpectedVersion = expectedVersion; + } +} + +public sealed class EntityLockedException : Exception +{ + public string EntityType { get; } + public object? EntityId { get; } + + public EntityLockedException(string entityType, object? entityId) + : base("数据已锁定,不能修改") + { + EntityType = entityType; + EntityId = entityId; + } +} + +public sealed class EntityNotFoundException : Exception +{ + public string EntityType { get; } + public object? EntityId { get; } + + public EntityNotFoundException(string entityType, object? entityId) + : base("数据不存在") + { + EntityType = entityType; + EntityId = entityId; + } +} diff --git a/MiGu.DB/Abstractions/Modules/IEntityModule.cs b/MiGu.DB/Abstractions/Modules/IEntityModule.cs new file mode 100644 index 0000000..7033706 --- /dev/null +++ b/MiGu.DB/Abstractions/Modules/IEntityModule.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace MiGu.DB.Abstractions.Modules; + +public interface IEntityModule +{ + void ConfigureModel(ModelBuilder modelBuilder); + void RegisterServices(IServiceCollection services); +} diff --git a/MiGu.DB/Abstractions/Persistence/PersistenceContracts.cs b/MiGu.DB/Abstractions/Persistence/PersistenceContracts.cs new file mode 100644 index 0000000..757102a --- /dev/null +++ b/MiGu.DB/Abstractions/Persistence/PersistenceContracts.cs @@ -0,0 +1,32 @@ +using System.Linq.Expressions; +using MiGu.DB.Abstractions.Entities; + +namespace MiGu.DB.Abstractions.Persistence; + +public interface IRepository where TEntity : class, IEntity +{ + IQueryable Query(bool asNoTracking = true); + Task FindAsync(TKey id, CancellationToken ct = default); + Task AddAsync(TEntity entity, CancellationToken ct = default); + void Update(TEntity entity); +} + +public interface IEditableRepository : IRepository + where TEntity : class, IEntity, ISoftDeletable, IVersioned, ILockable +{ + Task GetEditableAsync(Guid id, long? expectedVersion, CancellationToken ct = default); + Task SoftDeleteAsync(Guid id, long? expectedVersion, CancellationToken ct = default); + Task EnsureUniqueAsync(Expression> predicate, string errorMessage, CancellationToken ct = default); +} + +public interface IHistoryRepository : IRepository + where TEntity : class, IEntity, IHistoryEntry +{ + Task AppendAsync(TEntity entry, CancellationToken ct = default); +} + +public interface IUnitOfWork +{ + Task SaveChangesAsync(CancellationToken ct = default); + Task ExecuteInTransactionAsync(Func action, CancellationToken ct = default); +} diff --git a/MiGu.DB/Abstractions/Providers/IDbProviderSetup.cs b/MiGu.DB/Abstractions/Providers/IDbProviderSetup.cs new file mode 100644 index 0000000..d1aa3f0 --- /dev/null +++ b/MiGu.DB/Abstractions/Providers/IDbProviderSetup.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; + +namespace MiGu.DB.Abstractions.Providers; + +public interface IDbProviderSetup +{ + string Name { get; } + void Configure(DbContextOptionsBuilder builder, string connectionString); + string MigrationsAssemblyName { get; } + string MigrationsNamespace { get; } +} diff --git a/MiGu.DB/Abstractions/Runtime/RuntimeContracts.cs b/MiGu.DB/Abstractions/Runtime/RuntimeContracts.cs new file mode 100644 index 0000000..b92490f --- /dev/null +++ b/MiGu.DB/Abstractions/Runtime/RuntimeContracts.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; + +namespace MiGu.DB.Abstractions.Runtime; + +public interface IActorContext +{ + string Name { get; } +} + +public interface IActorContextAccessor +{ + IActorContext Current { get; set; } +} + +public interface IDataMigrator +{ + int Order { get; } + string Name { get; } + Task MigrateAsync(DbContext db, CancellationToken ct = default); +} + +/// +/// 启动期 Schema 初始化策略。 +/// :版本化迁移(发版/现场); +/// :按当前模型建库(开发期,改模型需删库重建)。 +/// +public enum MiGuSchemaMode +{ + Migrate = 0, + EnsureCreated = 1 +} + +public sealed class MiGuDbOptions +{ + public string Provider { get; set; } = "sqlite"; + public string ConnectionString { get; set; } = ""; + public string ContentRootPath { get; set; } = ""; + + /// Schema 初始化模式,对应配置 Database:SchemaMode。 + public MiGuSchemaMode SchemaMode { get; set; } = MiGuSchemaMode.Migrate; + + /// 为 true 时,Schema 初始化后按 Order 执行全部 IDataMigrator(默认开启)。 + public bool ApplyDataMigratorsOnStartup { get; set; } = true; +} diff --git a/MiGu.DB/Domains/.gitkeep b/MiGu.DB/Domains/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/MiGu.Server/Dashboard/UserDashboardShortcut.cs b/MiGu.DB/Domains/Dashboard/UserDashboardShortcut.cs similarity index 65% rename from MiGu.Server/Dashboard/UserDashboardShortcut.cs rename to MiGu.DB/Domains/Dashboard/UserDashboardShortcut.cs index 7d39f3a..ca831f2 100644 --- a/MiGu.Server/Dashboard/UserDashboardShortcut.cs +++ b/MiGu.DB/Domains/Dashboard/UserDashboardShortcut.cs @@ -1,6 +1,5 @@ -namespace MiGu.Server.Dashboard; +namespace MiGu.DB.Domains.Dashboard; -/// 用户 Dashboard 快捷入口配置(按 user + scope 一行)。 public sealed class UserDashboardShortcut { public string UserId { get; set; } = ""; diff --git a/MiGu.Server/Fleet/CdmTaskRecord.cs b/MiGu.DB/Domains/Fleet/CdmTaskRecord.cs similarity index 92% rename from MiGu.Server/Fleet/CdmTaskRecord.cs rename to MiGu.DB/Domains/Fleet/CdmTaskRecord.cs index 69a5973..00097dd 100644 --- a/MiGu.Server/Fleet/CdmTaskRecord.cs +++ b/MiGu.DB/Domains/Fleet/CdmTaskRecord.cs @@ -1,9 +1,8 @@ -namespace MiGu.Server.Fleet; +namespace MiGu.DB.Domains.Fleet; /// /// CDM 搬运任务的平台侧快照(表 cdm_tasks)。 -/// 以任务 Id 为主键;SimpleLite/StandardScene 把终态任务从自身 JSON 里删除,这里则永久保留=完整历史, -/// 且 SimpleLite 关闭后平台仍可从本表读取最近快照。 +/// 以任务 Id 为主键;SimpleLite/StandardScene 把终态任务从自身 JSON 里删除,这里则永久保留=完整历史。 /// public sealed class CdmTaskRecord { diff --git a/MiGu.DB/Domains/Fleet/FleetConfigurations.cs b/MiGu.DB/Domains/Fleet/FleetConfigurations.cs new file mode 100644 index 0000000..e70c8ad --- /dev/null +++ b/MiGu.DB/Domains/Fleet/FleetConfigurations.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace MiGu.DB.Domains.Fleet; + +public sealed class CdmTaskRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("cdm_tasks"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id").HasMaxLength(64); + e.Property(x => x.TaskId).HasColumnName("task_id").HasMaxLength(128).IsRequired(false); + e.Property(x => x.MissionId).HasColumnName("mission_id"); + e.Property(x => x.MissionName).HasColumnName("mission_name").HasMaxLength(128); + e.Property(x => x.MissionTypeName).HasColumnName("mission_type").HasMaxLength(128); + e.Property(x => x.SrcSiteId).HasColumnName("src_site_id"); + e.Property(x => x.SrcLabel).HasColumnName("src_label").HasMaxLength(256); + e.Property(x => x.DstSiteId).HasColumnName("dst_site_id"); + e.Property(x => x.DstLabel).HasColumnName("dst_label").HasMaxLength(256); + e.Property(x => x.Status).HasColumnName("status").HasMaxLength(32); + e.Property(x => x.StatusCode).HasColumnName("status_code").HasMaxLength(32); + e.Property(x => x.CarId).HasColumnName("car_id").IsRequired(false); + e.Property(x => x.CarName).HasColumnName("car_name").HasMaxLength(128).IsRequired(false); + e.Property(x => x.Priority).HasColumnName("priority"); + e.Property(x => x.CreateTime).HasColumnName("create_time").HasMaxLength(40).IsRequired(false); + e.Property(x => x.StartTime).HasColumnName("start_time").HasMaxLength(40).IsRequired(false); + e.Property(x => x.FinishTime).HasColumnName("finish_time").HasMaxLength(40).IsRequired(false); + e.Property(x => x.StuckReason).HasColumnName("stuck_reason").HasMaxLength(512).IsRequired(false); + e.Property(x => x.Overdue).HasColumnName("overdue"); + e.Property(x => x.FirstSeenAt).HasColumnName("first_seen_at"); + e.Property(x => x.LastSeenAt).HasColumnName("last_seen_at"); + e.HasIndex(x => x.StatusCode); + e.HasIndex(x => x.CreateTime); + } +} + +public sealed class VehicleAlarmRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder e) + { + e.ToTable("vehicle_alarms"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id").HasMaxLength(36); + e.Property(x => x.CarId).HasColumnName("car_id"); + e.Property(x => x.CarName).HasColumnName("car_name").HasMaxLength(128); + e.Property(x => x.Info).HasColumnName("info").HasColumnType("text"); + e.Property(x => x.Level).HasColumnName("level"); + e.Property(x => x.Status).HasColumnName("status").HasMaxLength(16); + e.Property(x => x.FirstAt).HasColumnName("first_at"); + e.Property(x => x.LastAt).HasColumnName("last_at"); + e.Property(x => x.ResolvedAt).HasColumnName("resolved_at").IsRequired(false); + e.Property(x => x.DurationSecs).HasColumnName("duration_secs").IsRequired(false); + e.Property(x => x.Acknowledged).HasColumnName("acknowledged"); + e.Property(x => x.AcknowledgedAt).HasColumnName("acknowledged_at").IsRequired(false); + e.Property(x => x.AcknowledgedBy).HasColumnName("acknowledged_by").HasMaxLength(128).IsRequired(false); + e.HasIndex(x => new { x.CarId, x.Status }); + e.HasIndex(x => x.Status); + e.HasIndex(x => x.FirstAt); + } +} diff --git a/MiGu.Server/Fleet/VehicleAlarmRecord.cs b/MiGu.DB/Domains/Fleet/VehicleAlarmRecord.cs similarity index 74% rename from MiGu.Server/Fleet/VehicleAlarmRecord.cs rename to MiGu.DB/Domains/Fleet/VehicleAlarmRecord.cs index a24ad3f..e9a0f00 100644 --- a/MiGu.Server/Fleet/VehicleAlarmRecord.cs +++ b/MiGu.DB/Domains/Fleet/VehicleAlarmRecord.cs @@ -1,10 +1,8 @@ -namespace MiGu.Server.Fleet; +namespace MiGu.DB.Domains.Fleet; /// /// 车辆报警的平台侧记录(表 vehicle_alarms)。 -/// SimpleLite 只在 SSE/状态里给出「当前是否报警 + 文案」,无历史;平台按车对帐: -/// 出现报警→开一条 active 记录,报警文案变化→更新,报警消失→置为 cleared 并记录恢复时间/持续时长。 -/// 永不删除=完整历史,重启/刷新不丢,SimpleLite 离线也可查。 +/// 出现报警→开一条 active 记录;文案/级别变化→先 clear 再建新 active;消失→cleared。 /// public sealed class VehicleAlarmRecord { diff --git a/MiGu.DB/Domains/Migrators/DataMigrators.cs b/MiGu.DB/Domains/Migrators/DataMigrators.cs new file mode 100644 index 0000000..6e3a643 --- /dev/null +++ b/MiGu.DB/Domains/Migrators/DataMigrators.cs @@ -0,0 +1,86 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using MiGu.DB.Abstractions.Runtime; +using MiGu.DB.Domains.Wms; +using MiGu.DB.Kernel.Context; + +namespace MiGu.DB.Domains.Migrators; + +// 启动期数据修补:Schema 初始化之后按 Order 执行,须幂等。 +// 优先 LINQ / ExecuteUpdateAsync;枚举 converter 无法匹配旧字符串时允许定点 raw UPDATE(Sqlite)。 + +/// 将 simple_fields.other 回填到 car_type(替代原 PlatformPersistence raw UPDATE)。 +public sealed class SimpleFieldsCarTypeBackfillMigrator : IDataMigrator +{ + public int Order => 10; + public string Name => "SimpleFields.CarTypeBackfill"; + + public async Task MigrateAsync(DbContext db, CancellationToken ct = default) + { + if (db is not MiGuDbContext ctx) return; + + await ctx.SimpleFields + .Where(x => (x.CarType == null || x.CarType == "") && x.Other != "") + .ExecuteUpdateAsync(s => s.SetProperty(x => x.CarType, x => x.Other), ct); + + await ctx.SimpleFields + .Where(x => x.Other != "" && x.Other == x.CarType) + .ExecuteUpdateAsync(s => s.SetProperty(x => x.Other, ""), ct); + } +} + +/// 库区 LayoutMode 空值补 Flat(raw UPDATE,避开枚举 converter)。 +public sealed class AreaLayoutModeDefaultMigrator : IDataMigrator +{ + public int Order => 15; + public string Name => "Wms.AreaLayoutModeDefault"; + + public async Task MigrateAsync(DbContext db, CancellationToken ct = default) + { + if (db is not MiGuDbContext) return; + + await db.Database.ExecuteSqlRawAsync( + """ + UPDATE wms_areas + SET LayoutMode = 'Flat' + WHERE LayoutMode IS NULL OR LayoutMode = '' + """, + ct); + } +} + +/// +/// LocationType=Storage 时回填 StorageId,供库存 join;写路径 BindOrTransferLocation 也会维护该字段。 +/// +public sealed class ContainerLocationStorageIdBackfillMigrator : IDataMigrator +{ + public int Order => 20; + public string Name => "Wms.ContainerLocation.StorageId"; + + public async Task MigrateAsync(DbContext db, CancellationToken ct = default) + { + if (db is not MiGuDbContext ctx) return; + + var rows = await ctx.ContainerLocations + .Where(x => x.LocationType == ContainerLocationType.Storage && x.StorageId == null) + .ToListAsync(ct); + foreach (var row in rows) + { + if (Guid.TryParse(row.LocationId, out var sid)) + row.StorageId = sid; + } + if (rows.Count > 0) + await ctx.SaveChangesAsync(ct); + } +} + +public static class DataMigratorRegistration +{ + public static IServiceCollection AddMiGuDataMigrators(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } +} diff --git a/MiGu.DB/Domains/SimpleFields/SimpleField.cs b/MiGu.DB/Domains/SimpleFields/SimpleField.cs new file mode 100644 index 0000000..9bd0c65 --- /dev/null +++ b/MiGu.DB/Domains/SimpleFields/SimpleField.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using MiGu.DB.Kernel.Entities; + +namespace MiGu.DB.Domains.SimpleFields; + +public sealed class SimpleField : Entity +{ + [MaxLength(64)] public string CarType { get; set; } = ""; + [MaxLength(64)] public string FieldType { get; set; } = ""; + [MaxLength(128)] public string Key { get; set; } = ""; + public string Value { get; set; } = ""; + [MaxLength(128)] public string DataType { get; set; } = ""; + [MaxLength(256)] public string? Chinese { get; set; } + [MaxLength(256)] public string? English { get; set; } + [MaxLength(512)] public string Other { get; set; } = ""; + public bool IsDefault { get; set; } + public DateTimeOffset CreateTime { get; set; } + public DateTimeOffset UpdateTime { get; set; } +} + +public static class SimpleFieldDateTime +{ + public const string Format = "yyyy-MM-dd HH:mm:ss"; + + public static DateTimeOffset Now => DateTimeOffset.UtcNow; + + public static string ToStorage(DateTimeOffset value) + => value.UtcDateTime.ToString(Format); + + public static DateTimeOffset FromStorage(string value) + => DateTimeOffset.Parse(value); +} diff --git a/MiGu.DB/Domains/Transport/TransportEntities.cs b/MiGu.DB/Domains/Transport/TransportEntities.cs new file mode 100644 index 0000000..a35a05b --- /dev/null +++ b/MiGu.DB/Domains/Transport/TransportEntities.cs @@ -0,0 +1,98 @@ +using System.ComponentModel.DataAnnotations; +using MiGu.DB.Kernel.Entities; + +namespace MiGu.DB.Domains.Transport; + +// 运输规则/任务/预占实体。状态字段为枚举;复数 *Statuses 辅助类供 Server 解析 DTO 字符串。 + +public sealed class WmsTransportRule : AggregateRoot +{ + [MaxLength(64)] public string Code { get; set; } = ""; + [MaxLength(128)] public string Name { get; set; } = ""; + public WmsTransportTriggerType TriggerType { get; set; } = WmsTransportTriggerType.MaterialCall; + public bool Enabled { get; set; } = true; + public int Priority { get; set; } + public string SourceSelectorJson { get; set; } = "{}"; + public string TargetSelectorJson { get; set; } = "{}"; + public string TaskOptionsJson { get; set; } = "{}"; +} + +public sealed class WmsTransportTask : AggregateRoot +{ + [MaxLength(64)] public string BusinessType { get; set; } = ""; + public Guid? RuleId { get; set; } + public Guid SourceStorageId { get; set; } + public Guid TargetStorageId { get; set; } + public Guid ContainerId { get; set; } + public Guid? MaterialId { get; set; } + public decimal? Quantity { get; set; } + public WmsTransportTaskStatus Status { get; set; } = WmsTransportTaskStatus.Pending; + [MaxLength(64)] public string DispatchMissionId { get; set; } = ""; + [MaxLength(64)] public string DeliveryId { get; set; } = ""; + [MaxLength(32)] public string DispatchStatus { get; set; } = ""; + [MaxLength(500)] public string Reason { get; set; } = ""; + public string SnapshotJson { get; set; } = "{}"; + [MaxLength(1000)] public string ErrorMessage { get; set; } = ""; + public int TaskPriority { get; set; } +} + +public sealed class WmsTransportReservation : AggregateRoot +{ + public Guid TaskId { get; set; } + public Guid ContainerId { get; set; } + public Guid SourceStorageId { get; set; } + public Guid TargetStorageId { get; set; } + public WmsReservationStatus Status { get; set; } = WmsReservationStatus.Active; + public DateTimeOffset? ExpiresAt { get; set; } +} + +public sealed class WmsTransportTaskHistory : Entity +{ + public Guid TaskId { get; set; } + [MaxLength(32)] public string FromStatus { get; set; } = ""; + [MaxLength(32)] public string ToStatus { get; set; } = ""; + [MaxLength(128)] public string Operator { get; set; } = ""; + public DateTimeOffset OperatedAt { get; set; } + [MaxLength(500)] public string Reason { get; set; } = ""; + [MaxLength(1000)] public string ErrorMessage { get; set; } = ""; + public string SnapshotJson { get; set; } = "{}"; +} + +public static class WmsTransportTriggerTypes +{ + // 复数辅助类:与枚举分离,避免与属性/类型同名冲突,并提供 ParseOr / All + public const WmsTransportTriggerType MaterialCall = WmsTransportTriggerType.MaterialCall; + public const WmsTransportTriggerType FinishedGoodsOffline = WmsTransportTriggerType.FinishedGoodsOffline; + public const WmsTransportTriggerType AutoTransfer = WmsTransportTriggerType.AutoTransfer; + public static readonly HashSet All = new() + { MaterialCall, FinishedGoodsOffline, AutoTransfer }; + + public static bool IsDefined(string? value) => + Enum.TryParse(value, true, out var e) && All.Contains(e); + + public static WmsTransportTriggerType ParseOr(string? value, WmsTransportTriggerType fallback = WmsTransportTriggerType.MaterialCall) => + Enum.TryParse(value, true, out var e) && All.Contains(e) ? e : fallback; +} + +public static class WmsTransportTaskStatuses +{ + public const WmsTransportTaskStatus Pending = WmsTransportTaskStatus.Pending; + public const WmsTransportTaskStatus Reserved = WmsTransportTaskStatus.Reserved; + public const WmsTransportTaskStatus Dispatched = WmsTransportTaskStatus.Dispatched; + public const WmsTransportTaskStatus InTransit = WmsTransportTaskStatus.InTransit; + public const WmsTransportTaskStatus Completed = WmsTransportTaskStatus.Completed; + public const WmsTransportTaskStatus Failed = WmsTransportTaskStatus.Failed; + public const WmsTransportTaskStatus Cancelled = WmsTransportTaskStatus.Cancelled; + + public static readonly HashSet Active = new() + { Pending, Reserved, Dispatched, InTransit }; + public static readonly HashSet All = new() + { Pending, Reserved, Dispatched, InTransit, Completed, Failed, Cancelled }; +} + +public static class WmsReservationStatuses +{ + public const WmsReservationStatus Active = WmsReservationStatus.Active; + public const WmsReservationStatus Released = WmsReservationStatus.Released; + public const WmsReservationStatus Expired = WmsReservationStatus.Expired; +} diff --git a/MiGu.DB/Domains/Transport/TransportEnums.cs b/MiGu.DB/Domains/Transport/TransportEnums.cs new file mode 100644 index 0000000..014477c --- /dev/null +++ b/MiGu.DB/Domains/Transport/TransportEnums.cs @@ -0,0 +1,29 @@ +namespace MiGu.DB.Domains.Transport; + +/// +/// 运输域状态/触发类型枚举。存储规则同 Wms:HasConversion<string>,成员名 = 列值。 +/// +public enum WmsTransportTriggerType +{ + MaterialCall, + FinishedGoodsOffline, + AutoTransfer +} + +public enum WmsTransportTaskStatus +{ + Pending, + Reserved, + Dispatched, + InTransit, + Completed, + Failed, + Cancelled +} + +public enum WmsReservationStatus +{ + Active, + Released, + Expired +} diff --git a/MiGu.DB/Domains/TransportAndOtherConfigurations.cs b/MiGu.DB/Domains/TransportAndOtherConfigurations.cs new file mode 100644 index 0000000..2971f5a --- /dev/null +++ b/MiGu.DB/Domains/TransportAndOtherConfigurations.cs @@ -0,0 +1,112 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MiGu.DB.Domains.Dashboard; +using MiGu.DB.Domains.SimpleFields; +using MiGu.DB.Domains.Transport; + +namespace MiGu.DB.Domains.Transport +{ + public sealed class WmsTransportRuleConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_transport_rules"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.Code).IsUnique(); + b.HasIndex(x => new { x.TriggerType, x.Enabled, x.Priority }); + b.Property(x => x.SourceSelectorJson).HasColumnType("text"); + b.Property(x => x.TargetSelectorJson).HasColumnType("text"); + b.Property(x => x.TaskOptionsJson).HasColumnType("text"); + } + } + + public sealed class WmsTransportTaskConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_transport_tasks"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.Status); + b.HasIndex(x => x.ContainerId); + b.HasIndex(x => x.TargetStorageId); + b.Property(x => x.Quantity).HasPrecision(18, 4); + b.Property(x => x.SnapshotJson).HasColumnType("text"); + } + } + + public sealed class WmsTransportReservationConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_transport_reservations"); + b.HasKey(x => x.Id); + b.HasIndex(x => new { x.ContainerId, x.Status }); + b.HasIndex(x => new { x.TargetStorageId, x.Status }); + } + } + + public sealed class WmsTransportTaskHistoryConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_transport_task_history"); + b.HasKey(x => x.Id); + b.Property(x => x.FromStatus).HasMaxLength(32); + b.Property(x => x.ToStatus).HasMaxLength(32); + b.Property(x => x.Operator).HasMaxLength(128); + b.Property(x => x.Reason).HasMaxLength(500); + b.Property(x => x.ErrorMessage).HasMaxLength(1000); + b.Property(x => x.SnapshotJson).HasColumnType("text"); + b.HasIndex(x => x.TaskId); + b.HasIndex(x => x.OperatedAt); + } + } +} + +namespace MiGu.DB.Domains.SimpleFields +{ + public sealed class SimpleFieldConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder b) + { + b.ToTable("simple_fields"); + b.HasKey(x => x.Id); + b.Property(x => x.Id).HasColumnName("id"); + b.Property(x => x.CarType).HasColumnName("car_type").HasMaxLength(64); + b.Property(x => x.FieldType).HasColumnName("field_type").HasMaxLength(64); + b.Property(x => x.Key).HasColumnName("key").HasMaxLength(128); + b.Property(x => x.Value).HasColumnName("value"); + b.Property(x => x.DataType).HasColumnName("data_type").HasMaxLength(128); + b.Property(x => x.Chinese).HasColumnName("chinese").HasMaxLength(256).IsRequired(false); + b.Property(x => x.English).HasColumnName("english").HasMaxLength(256).IsRequired(false); + b.Property(x => x.Other).HasColumnName("other").HasMaxLength(512); + b.Property(x => x.IsDefault).HasColumnName("is_default"); + var dateTime = new ValueConverter( + v => SimpleFieldDateTime.ToStorage(v), + v => SimpleFieldDateTime.FromStorage(v)); + b.Property(x => x.CreateTime).HasColumnName("create_time").HasConversion(dateTime).HasMaxLength(19); + b.Property(x => x.UpdateTime).HasColumnName("update_time").HasConversion(dateTime).HasMaxLength(19); + b.HasIndex(x => new { x.CarType, x.FieldType, x.Key }).IsUnique(); + } + } +} + +namespace MiGu.DB.Domains.Dashboard +{ + public sealed class UserDashboardShortcutConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder b) + { + b.ToTable("user_dashboard_shortcuts"); + b.HasKey(x => new { x.UserId, x.Scope }); + b.Property(x => x.UserId).HasColumnName("user_id").HasMaxLength(64); + b.Property(x => x.Scope).HasColumnName("scope").HasMaxLength(32); + b.Property(x => x.KeysJson).HasColumnName("keys_json").HasColumnType("text"); + var dateTime = new ValueConverter( + v => v.UtcDateTime.ToString("O"), + v => DateTimeOffset.Parse(v)); + b.Property(x => x.UpdatedAt).HasColumnName("updated_at").HasConversion(dateTime).HasMaxLength(40); + } + } +} diff --git a/MiGu.DB/Domains/Wms/WmsConfigurations.cs b/MiGu.DB/Domains/Wms/WmsConfigurations.cs new file mode 100644 index 0000000..86c59c6 --- /dev/null +++ b/MiGu.DB/Domains/Wms/WmsConfigurations.cs @@ -0,0 +1,170 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MiGu.DB.Domains.Dashboard; +using MiGu.DB.Domains.SimpleFields; +using MiGu.DB.Domains.Transport; +using MiGu.DB.Domains.Wms; +using MiGu.DB.Kernel.Entities; + +namespace MiGu.DB.Domains.Wms; + +public sealed class WarehouseConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_warehouses"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.Code).IsUnique(); + } +} + +public sealed class WarehouseAreaConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_areas"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.Code).IsUnique(); + b.HasIndex(x => x.WarehouseId); + } +} + +public sealed class StorageConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_storages"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.Code).IsUnique(); + b.HasIndex(x => x.AreaId); + b.HasIndex(x => x.Barcode); + } +} + +public sealed class ContainerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_containers"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.Code).IsUnique(); + } +} + +public sealed class MaterialTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_material_types"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.Code).IsUnique(); + } +} + +public sealed class MaterialConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_materials"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.Code).IsUnique(); + b.HasIndex(x => x.Barcode); + b.HasIndex(x => new { x.LifecycleStatus, x.UpdatedAt }); + b.HasIndex(x => x.TypeCode); + } +} + +public sealed class ContainerLocationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_container_locations"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.ContainerId).IsUnique(); + b.HasIndex(x => new { x.LocationType, x.LocationId }); + b.HasIndex(x => x.StorageId); + } +} + +public sealed class ContainerMaterialConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_container_materials"); + b.HasKey(x => x.Id); + b.HasIndex(x => x.MaterialId).IsUnique(); + b.HasIndex(x => x.ContainerId); + b.Property(x => x.Quantity).HasPrecision(18, 4); + } +} + +public sealed class StockEventConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("wms_stock_events"); + b.HasKey(x => x.Id); + b.Property(x => x.EventType).HasMaxLength(32); + b.Property(x => x.MaterialCode).HasMaxLength(64); + b.Property(x => x.MaterialName).HasMaxLength(128); + b.Property(x => x.MaterialBarcode).HasMaxLength(128); + b.Property(x => x.MaterialTypeCode).HasMaxLength(64); + b.Property(x => x.ContainerCode).HasMaxLength(64); + b.Property(x => x.ContainerName).HasMaxLength(128); + b.Property(x => x.StorageCode).HasMaxLength(64); + b.Property(x => x.StorageName).HasMaxLength(128); + b.Property(x => x.AreaCode).HasMaxLength(64); + b.Property(x => x.FromStorageCode).HasMaxLength(64); + b.Property(x => x.FromStorageName).HasMaxLength(128); + b.Property(x => x.ToStorageCode).HasMaxLength(64); + b.Property(x => x.ToStorageName).HasMaxLength(128); + b.Property(x => x.RefType).HasMaxLength(64); + b.Property(x => x.RefCode).HasMaxLength(64); + b.Property(x => x.Operator).HasMaxLength(128); + b.Property(x => x.Reason).HasMaxLength(500); + b.HasIndex(x => x.OperatedAt); + b.HasIndex(x => x.EventType); + b.HasIndex(x => x.MaterialId); + b.HasIndex(x => x.ContainerId); + } +} + +public abstract class HistoryConfigurationBase : IEntityTypeConfiguration where T : HistoryEntity +{ + private readonly string _table; + protected HistoryConfigurationBase(string table) => _table = table; + + public virtual void Configure(EntityTypeBuilder b) + { + b.ToTable(_table); + b.HasKey(x => x.Id); + b.Property(x => x.EventType).HasMaxLength(64); + b.Property(x => x.BeforeJson).HasColumnType("text"); + b.Property(x => x.AfterJson).HasColumnType("text"); + b.Property(x => x.Operator).HasMaxLength(128); + b.Property(x => x.Source).HasMaxLength(32); + b.Property(x => x.Reason).HasMaxLength(500); + b.Property(x => x.Remark).HasMaxLength(1000); + b.Property(x => x.Extend).HasColumnType("text"); + b.HasIndex(x => x.ContainerId); + b.HasIndex(x => x.OperatedAt); + b.HasIndex(x => x.EventType); + } +} + +public sealed class ContainerLocationHistoryConfiguration : HistoryConfigurationBase +{ + public ContainerLocationHistoryConfiguration() : base("wms_container_location_history") { } +} + +public sealed class ContainerMaterialHistoryConfiguration : HistoryConfigurationBase +{ + public ContainerMaterialHistoryConfiguration() : base("wms_container_material_history") { } + + public override void Configure(EntityTypeBuilder b) + { + base.Configure(b); + b.Property(x => x.QuantityDelta).HasPrecision(18, 4); + } +} diff --git a/MiGu.DB/Domains/Wms/WmsEntities.cs b/MiGu.DB/Domains/Wms/WmsEntities.cs new file mode 100644 index 0000000..f35f125 --- /dev/null +++ b/MiGu.DB/Domains/Wms/WmsEntities.cs @@ -0,0 +1,174 @@ +using System.ComponentModel.DataAnnotations; +using MiGu.DB.Kernel.Entities; + +namespace MiGu.DB.Domains.Wms; + +// WMS 聚合实体。状态/类型字段为枚举(库内 TEXT,见全局 HasConversion);DTO 仍用字符串,由 *Statuses 辅助类解析。 + +public sealed class Warehouse : AggregateRoot +{ + [MaxLength(64)] public string Code { get; set; } = ""; + [MaxLength(128)] public string Name { get; set; } = ""; + [MaxLength(64)] public string Type { get; set; } = "Default"; + public bool Enabled { get; set; } = true; + public int SortOrder { get; set; } +} + +public sealed class WarehouseArea : AggregateRoot +{ + public Guid WarehouseId { get; set; } + [MaxLength(64)] public string Code { get; set; } = ""; + [MaxLength(128)] public string Name { get; set; } = ""; + [MaxLength(64)] public string Type { get; set; } = "Storage"; + public AreaLayoutMode LayoutMode { get; set; } = AreaLayoutMode.Flat; + [MaxLength(32)] public string State { get; set; } = "Default"; + public bool Enabled { get; set; } = true; + public int SortOrder { get; set; } +} + +public sealed class Storage : AggregateRoot +{ + 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; } = StorageTypeCodes.Storage; + // 属性名与枚举类型同名合法;默认值引用枚举成员 LocationKind.Station(勿再引入同名静态类) + public LocationKind LocationKind { get; set; } = LocationKind.Station; + public int ColumnNo { get; set; } + public int LevelNo { get; set; } = 1; + public int DepthNo { get; set; } = 1; + [MaxLength(64)] public string SiteId { get; set; } = ""; + [MaxLength(64)] public string SiteCode { get; set; } = ""; + [MaxLength(128)] public string Barcode { get; set; } = ""; + public int Capacity { get; set; } + public StorageStatus Status { get; set; } = StorageStatus.Empty; + [MaxLength(64)] public string Usage { get; set; } = ""; + public int Priority { get; set; } + [MaxLength(64)] public string ZoneCode { get; set; } = ""; + public bool AllowInbound { get; set; } = true; + public bool AllowOutbound { get; set; } = true; + public bool Enabled { get; set; } = true; +} + +public sealed class Container : AggregateRoot +{ + public Guid? AreaId { get; set; } + [MaxLength(64)] public string Code { get; set; } = ""; + [MaxLength(128)] public string Name { get; set; } = ""; + [MaxLength(64)] public string ContainerType { get; set; } = "Box"; + public ContainerStatus Status { get; set; } = ContainerStatus.EmptyMaterial; + [MaxLength(128)] public string Barcode { get; set; } = ""; + public double Length { get; set; } + public double Width { get; set; } + public double Height { get; set; } + public bool Enabled { get; set; } = true; +} + +public sealed class MaterialType : AggregateRoot +{ + [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; } = ""; + [MaxLength(64)] public string BarcodePrefix { get; set; } = ""; + public bool Enabled { get; set; } = true; +} + +public sealed class Material : AggregateRoot +{ + [MaxLength(64)] public string Code { get; set; } = ""; + [MaxLength(128)] public string Name { get; set; } = ""; + [MaxLength(64)] public string TypeCode { get; set; } = ""; + [MaxLength(128)] public string Barcode { 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 MaterialLifecycle LifecycleStatus { get; set; } = MaterialLifecycle.Active; + public DateTimeOffset? UnboundAt { get; set; } + public bool Enabled { get; set; } = true; +} + +public sealed class ContainerLocation : AggregateRoot +{ + public Guid ContainerId { get; set; } + public ContainerLocationType LocationType { get; set; } = ContainerLocationType.Storage; + [MaxLength(64)] public string LocationId { get; set; } = ""; + /// LocationType=Storage 时的库位 Id;由 LocationId 回填,供库存 join 下推。 + public Guid? StorageId { get; set; } + [MaxLength(64)] public string LocationCode { get; set; } = ""; + [MaxLength(128)] public string LocationName { get; set; } = ""; + public ContainerLocationStatus Status { get; set; } = ContainerLocationStatus.Active; + public DateTimeOffset EnteredAt { get; set; } +} + +public sealed class ContainerMaterial : AggregateRoot +{ + public Guid ContainerId { get; set; } + public Guid MaterialId { get; set; } + public decimal Quantity { get; set; } = 1; + [MaxLength(64)] public string BatchNo { get; set; } = ""; + [MaxLength(64)] public string SerialNo { get; set; } = ""; + public ContainerMaterialStatus Status { get; set; } = ContainerMaterialStatus.Bound; + public DateTimeOffset BoundAt { get; set; } + public DateTimeOffset LoadedAt { get; set; } + public DateTimeOffset? UnloadedAt { get; set; } +} + +public sealed class StockEvent : Kernel.Entities.Entity +{ + public StockEventType EventType { get; set; } + public Guid? MaterialId { get; set; } + [MaxLength(64)] public string MaterialCode { get; set; } = ""; + [MaxLength(128)] public string MaterialName { get; set; } = ""; + [MaxLength(128)] public string MaterialBarcode { get; set; } = ""; + [MaxLength(64)] public string MaterialTypeCode { get; set; } = ""; + public Guid? ContainerId { get; set; } + [MaxLength(64)] public string ContainerCode { get; set; } = ""; + [MaxLength(128)] public string ContainerName { get; set; } = ""; + public Guid? StorageId { get; set; } + [MaxLength(64)] public string StorageCode { get; set; } = ""; + [MaxLength(128)] public string StorageName { get; set; } = ""; + [MaxLength(64)] public string AreaCode { get; set; } = ""; + public Guid? FromStorageId { get; set; } + [MaxLength(64)] public string FromStorageCode { get; set; } = ""; + [MaxLength(128)] public string FromStorageName { get; set; } = ""; + public Guid? ToStorageId { get; set; } + [MaxLength(64)] public string ToStorageCode { get; set; } = ""; + [MaxLength(128)] public string ToStorageName { get; set; } = ""; + [MaxLength(64)] public string RefType { get; set; } = "Manual"; + public Guid? RefId { get; set; } + [MaxLength(64)] public string RefCode { get; set; } = ""; + [MaxLength(128)] public string Operator { get; set; } = ""; + public DateTimeOffset OperatedAt { get; set; } + [MaxLength(500)] public string Reason { get; set; } = ""; +} + +public sealed class ContainerLocationHistory : HistoryEntity +{ + [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 : HistoryEntity +{ + public Guid? MaterialId { get; set; } + public decimal QuantityDelta { get; set; } +} + +public static class StorageTypeCodes +{ + public const string Storage = "Storage"; + public const string LineSide = "LineSide"; + public const string OfflinePoint = "OfflinePoint"; + public const string Buffer = "Buffer"; + public const string FinishedGoods = "FinishedGoods"; +} + +public static class WmsDefaults +{ + public const string DefaultWarehouseCode = "DEFAULT"; + public const string DefaultWarehouseName = "默认仓库"; +} diff --git a/MiGu.DB/Domains/Wms/WmsEnums.cs b/MiGu.DB/Domains/Wms/WmsEnums.cs new file mode 100644 index 0000000..e5bf428 --- /dev/null +++ b/MiGu.DB/Domains/Wms/WmsEnums.cs @@ -0,0 +1,65 @@ +namespace MiGu.DB.Domains.Wms; + +/// +/// WMS 状态/类型枚举。列以字符串存储(见约定 HasConversion<string>),成员名即库内合法值。 +/// +public enum AreaLayoutMode +{ + Flat, + Grid +} + +public enum LocationKind +{ + Grid, + Station +} + +public enum StorageStatus +{ + Empty, + EmptyContainer, + FullContainer, + Disabled +} + +public enum ContainerStatus +{ + EmptyMaterial, + FullMaterial +} + +public enum ContainerLocationType +{ + Storage, + Car +} + +public enum ContainerLocationStatus +{ + Active, + Locked, + Exception +} + +public enum ContainerMaterialStatus +{ + Bound, + Loaded, + Unloaded, + Adjusted, + Frozen +} + +public enum MaterialLifecycle +{ + Active, + Archived +} + +public enum StockEventType +{ + Bind, + Unbind, + ContainerMove +} diff --git a/MiGu.DB/Domains/Wms/WmsStatusHelpers.cs b/MiGu.DB/Domains/Wms/WmsStatusHelpers.cs new file mode 100644 index 0000000..5961b1d --- /dev/null +++ b/MiGu.DB/Domains/Wms/WmsStatusHelpers.cs @@ -0,0 +1,107 @@ +namespace MiGu.DB.Domains.Wms; + +/// +/// 枚举辅助类(复数命名):承接 API/DTO 的字符串入参,解析为实体上的枚举。 +/// 与同名枚举分离,避免「属性 LocationKind 初始值引用实例属性」一类编译冲突。 +/// Server 侧通过 global using 别名(如 StorageStatuses)引用本文件类型。 +/// +public static class AreaLayoutModes +{ + public static readonly HashSet All = new() { AreaLayoutMode.Flat, AreaLayoutMode.Grid }; + + public static AreaLayoutMode ParseOr(string? value, AreaLayoutMode fallback = AreaLayoutMode.Flat) => + Enum.TryParse(value, true, out var e) && All.Contains(e) ? e : fallback; +} + +public static class LocationKinds +{ + public const LocationKind Grid = LocationKind.Grid; + public const LocationKind Station = LocationKind.Station; + public static readonly HashSet All = new() { Grid, Station }; + + public static LocationKind ParseOr(string? value, LocationKind fallback = LocationKind.Station) => + Enum.TryParse(value, true, out var e) && All.Contains(e) ? e : fallback; +} + +public static class StorageStatuses +{ + public const StorageStatus Empty = StorageStatus.Empty; + public const StorageStatus EmptyContainer = StorageStatus.EmptyContainer; + public const StorageStatus FullContainer = StorageStatus.FullContainer; + public const StorageStatus Disabled = StorageStatus.Disabled; + + public static readonly HashSet All = new() + { Empty, EmptyContainer, FullContainer, Disabled }; + + public static StorageStatus ParseOr(string? value, StorageStatus fallback = StorageStatus.Empty) => + Enum.TryParse(value, true, out var e) && All.Contains(e) ? e : fallback; +} + +public static class ContainerStatuses +{ + public const ContainerStatus EmptyMaterial = ContainerStatus.EmptyMaterial; + public const ContainerStatus FullMaterial = ContainerStatus.FullMaterial; + public static readonly HashSet All = new() { EmptyMaterial, FullMaterial }; + + public static ContainerStatus ParseOr(string? value, ContainerStatus fallback = ContainerStatus.EmptyMaterial) => + Enum.TryParse(value, true, out var e) && All.Contains(e) ? e : fallback; +} + +public static class ContainerLocationTypes +{ + public const ContainerLocationType Storage = ContainerLocationType.Storage; + public const ContainerLocationType Car = ContainerLocationType.Car; + public static readonly HashSet All = new() { Storage, Car }; + + public static bool IsDefined(string? value) => + Enum.TryParse(value, true, out var e) && All.Contains(e); + + public static ContainerLocationType ParseOr(string? value, ContainerLocationType fallback = ContainerLocationType.Storage) => + Enum.TryParse(value, true, out var e) && All.Contains(e) ? e : fallback; + + public static bool EqualsString(ContainerLocationType value, string? other) => + Enum.TryParse(other, true, out var e) && e == value; +} + +public static class ContainerLocationStatuses +{ + public const ContainerLocationStatus Active = ContainerLocationStatus.Active; + public const ContainerLocationStatus Locked = ContainerLocationStatus.Locked; + public const ContainerLocationStatus Exception = ContainerLocationStatus.Exception; + public static readonly HashSet All = new() { Active, Locked, Exception }; + + public static bool IsDefined(string? value) => + Enum.TryParse(value, true, out var e) && All.Contains(e); + + public static ContainerLocationStatus ParseOr(string? value, ContainerLocationStatus fallback = ContainerLocationStatus.Active) => + Enum.TryParse(value, true, out var e) && All.Contains(e) ? e : fallback; +} + +public static class ContainerMaterialStatuses +{ + public const ContainerMaterialStatus Bound = ContainerMaterialStatus.Bound; + public const ContainerMaterialStatus Loaded = ContainerMaterialStatus.Loaded; + public const ContainerMaterialStatus Unloaded = ContainerMaterialStatus.Unloaded; + public const ContainerMaterialStatus Adjusted = ContainerMaterialStatus.Adjusted; + public const ContainerMaterialStatus Frozen = ContainerMaterialStatus.Frozen; + + public static readonly HashSet ActiveBind = new() + { Bound, Loaded, Adjusted, Frozen }; +} + +public static class MaterialLifecycles +{ + public const MaterialLifecycle Active = MaterialLifecycle.Active; + public const MaterialLifecycle Archived = MaterialLifecycle.Archived; + public static readonly HashSet All = new() { Active, Archived }; + + public static MaterialLifecycle ParseOr(string? value, MaterialLifecycle fallback = MaterialLifecycle.Active) => + Enum.TryParse(value, true, out var e) && All.Contains(e) ? e : fallback; +} + +public static class StockEventTypes +{ + public const StockEventType Bind = StockEventType.Bind; + public const StockEventType Unbind = StockEventType.Unbind; + public const StockEventType ContainerMove = StockEventType.ContainerMove; +} diff --git a/MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs b/MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs new file mode 100644 index 0000000..f938089 --- /dev/null +++ b/MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using MiGu.DB.Domains.Dashboard; +using MiGu.DB.Domains.Fleet; +using MiGu.DB.Domains.SimpleFields; +using MiGu.DB.Domains.Transport; +using MiGu.DB.Domains.Wms; + +namespace MiGu.DB.Kernel.Context; + +public partial class MiGuDbContext +{ + public DbSet Warehouses => Set(); + public DbSet WarehouseAreas => Set(); + public DbSet Storages => Set(); + public DbSet Containers => Set(); + public DbSet MaterialTypes => Set(); + public DbSet Materials => Set(); + public DbSet ContainerLocations => Set(); + public DbSet ContainerMaterials => Set(); + public DbSet StockEvents => Set(); + public DbSet ContainerLocationHistories => Set(); + public DbSet ContainerMaterialHistories => Set(); + public DbSet WmsTransportRules => Set(); + public DbSet WmsTransportTasks => Set(); + public DbSet WmsTransportReservations => Set(); + public DbSet WmsTransportTaskHistories => Set(); + public DbSet SimpleFields => Set(); + public DbSet UserDashboardShortcuts => Set(); + public DbSet CdmTasks => Set(); + public DbSet VehicleAlarms => Set(); +} diff --git a/MiGu.DB/Kernel/Context/MiGuDbContext.cs b/MiGu.DB/Kernel/Context/MiGuDbContext.cs new file mode 100644 index 0000000..3487d8d --- /dev/null +++ b/MiGu.DB/Kernel/Context/MiGuDbContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using MiGu.DB.Abstractions.Modules; +using MiGu.DB.Kernel.Conventions; + +namespace MiGu.DB.Kernel.Context; + +public partial class MiGuDbContext : DbContext +{ + private readonly IEnumerable _modules; + + public MiGuDbContext(DbContextOptions options, IEnumerable? modules = null) + : base(options) + { + _modules = modules ?? Array.Empty(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // 先约定、后 Configuration,让实体级配置覆盖全局 Guid/时间转换(如 SimpleField) + ModelConventionExtensions.ApplyMiGuConventions(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(MiGuDbContext).Assembly); + foreach (var module in _modules) + module.ConfigureModel(modelBuilder); + } +} diff --git a/MiGu.DB/Kernel/Conventions/Conventions.cs b/MiGu.DB/Kernel/Conventions/Conventions.cs new file mode 100644 index 0000000..51a25b8 --- /dev/null +++ b/MiGu.DB/Kernel/Conventions/Conventions.cs @@ -0,0 +1,167 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MiGu.DB.Abstractions.Entities; +using MiGu.DB.Abstractions.Runtime; + +namespace MiGu.DB.Kernel.Conventions; + +/// +/// 全局模型约定:Guid/DateTimeOffset 字符串化、枚举存字符串、软删过滤、Version 并发令牌等。 +/// +public static class ModelConventionExtensions +{ + public static void ApplyMiGuConventions(ModelBuilder modelBuilder) + { + // 与历史 Sqlite TEXT 列兼容:Guid / DateTimeOffset 均以字符串落库 + var guid = new ValueConverter(v => v.ToString("D"), v => Guid.Parse(v)); + var nullableGuid = new ValueConverter( + v => v.HasValue ? v.Value.ToString("D") : null, + v => string.IsNullOrWhiteSpace(v) ? null : Guid.Parse(v)); + var dto = new ValueConverter( + v => v.UtcDateTime.ToString("O"), + v => DateTimeOffset.Parse(v)); + var nullableDto = new ValueConverter( + v => v.HasValue ? v.Value.UtcDateTime.ToString("O") : null, + v => string.IsNullOrWhiteSpace(v) ? null : DateTimeOffset.Parse(v)); + + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var clr = entityType.ClrType; + + foreach (var p in clr.GetProperties().Where(p => p.PropertyType == typeof(Guid))) + modelBuilder.Entity(clr).Property(p.Name).HasConversion(guid).HasMaxLength(36); + foreach (var p in clr.GetProperties().Where(p => p.PropertyType == typeof(Guid?))) + modelBuilder.Entity(clr).Property(p.Name).HasConversion(nullableGuid).HasMaxLength(36); + foreach (var p in clr.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset))) + modelBuilder.Entity(clr).Property(p.Name).HasConversion(dto).HasMaxLength(40); + foreach (var p in clr.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset?))) + modelBuilder.Entity(clr).Property(p.Name).HasConversion(nullableDto).HasMaxLength(40); + + // 严格 1:1 字符串转换;旧值兼容靠 IDataMigrator,禁止在 converter 读侧 Normalize + foreach (var p in clr.GetProperties().Where(p => p.PropertyType.IsEnum)) + modelBuilder.Entity(clr).Property(p.Name).HasConversion().HasMaxLength(32); + + if (typeof(ISoftDeletable).IsAssignableFrom(clr)) + { + var method = typeof(ModelConventionExtensions) + .GetMethod(nameof(SetSoftDeleteFilter), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! + .MakeGenericMethod(clr); + method.Invoke(null, new object[] { modelBuilder }); + } + + // Version:业务乐观锁(非数据库 rowversion 字节数组) + if (typeof(IVersioned).IsAssignableFrom(clr)) + modelBuilder.Entity(clr).Property(nameof(IVersioned.Version)).IsConcurrencyToken(); + + if (typeof(ILockable).IsAssignableFrom(clr)) + modelBuilder.Entity(clr).Property(nameof(ILockable.IsLock)).HasColumnName("IsLock"); + + if (typeof(IAuditable).IsAssignableFrom(clr)) + { + modelBuilder.Entity(clr).Property(nameof(IAuditable.CreatedBy)).HasMaxLength(128); + modelBuilder.Entity(clr).Property(nameof(IAuditable.UpdatedBy)).HasMaxLength(128); + } + + if (typeof(ISoftDeletable).IsAssignableFrom(clr)) + modelBuilder.Entity(clr).Property(nameof(ISoftDeletable.DeletedBy)).HasMaxLength(128); + + if (typeof(IRemarkable).IsAssignableFrom(clr)) + modelBuilder.Entity(clr).Property(nameof(IRemarkable.Remark)).HasMaxLength(1000); + + if (typeof(IExtendable).IsAssignableFrom(clr)) + modelBuilder.Entity(clr).Property(nameof(IExtendable.Extend)).HasColumnType("text"); + } + } + + private static void SetSoftDeleteFilter(ModelBuilder modelBuilder) + where TEntity : class, ISoftDeletable + => modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); +} + +public sealed class ActorContextAccessor : IActorContextAccessor +{ + private static readonly AsyncLocal CurrentContext = new(); + + public IActorContext Current + { + get => CurrentContext.Value ?? SystemActorContext.Instance; + set => CurrentContext.Value = value; + } +} + +public sealed class SystemActorContext : IActorContext +{ + public static readonly SystemActorContext Instance = new(); + public string Name => "system"; +} + +public sealed class AuditSaveChangesInterceptor : SaveChangesInterceptor +{ + private readonly IActorContextAccessor _actors; + + public AuditSaveChangesInterceptor(IActorContextAccessor actors) => _actors = actors; + + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + Stamp(eventData.Context); + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + Stamp(eventData.Context); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private void Stamp(DbContext? db) + { + if (db is null) return; + var now = DateTimeOffset.UtcNow; + var actor = _actors.Current.Name; + + foreach (var entry in db.ChangeTracker.Entries()) + { + if (entry.Entity is IAuditable auditable) + { + if (entry.State == EntityState.Added) + { + if (entry.Entity is IEntity { Id: var id } && id == Guid.Empty) + ((IEntity)entry.Entity).Id = Guid.NewGuid(); + auditable.CreatedAt = now; + auditable.UpdatedAt = now; + if (string.IsNullOrWhiteSpace(auditable.CreatedBy)) auditable.CreatedBy = actor; + if (string.IsNullOrWhiteSpace(auditable.UpdatedBy)) auditable.UpdatedBy = actor; + } + else if (entry.State == EntityState.Modified) + { + auditable.UpdatedAt = now; + auditable.UpdatedBy = actor; + } + } + + if (entry.Entity is IVersioned versioned) + { + if (entry.State == EntityState.Added) + versioned.Version = Math.Max(1, versioned.Version); + else if (entry.State == EntityState.Modified) + versioned.Version += 1; + } + + if (entry.Entity is IExtendable extendable && string.IsNullOrWhiteSpace(extendable.Extend)) + extendable.Extend = "{}"; + + if (entry.Entity is ISoftDeletable soft + && entry.State == EntityState.Modified + && entry.Property(nameof(ISoftDeletable.IsDeleted)).IsModified + && soft.IsDeleted) + { + soft.DeletedAt ??= now; + if (string.IsNullOrWhiteSpace(soft.DeletedBy)) soft.DeletedBy = actor; + } + } + } +} diff --git a/MiGu.DB/Kernel/Design/MiGuDbContextFactory.cs b/MiGu.DB/Kernel/Design/MiGuDbContextFactory.cs new file mode 100644 index 0000000..eb60390 --- /dev/null +++ b/MiGu.DB/Kernel/Design/MiGuDbContextFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using MiGu.DB.Kernel.Context; + +namespace MiGu.DB.Kernel.Design; + +/// +/// design-time 工厂(dotnet ef migrations add)。指定 MigrationsAssembly,与运行时 SqliteProviderSetup 一致。 +/// +public sealed class MiGuDbContextFactory : IDesignTimeDbContextFactory +{ + public MiGuDbContext CreateDbContext(string[] args) + { + var options = new DbContextOptionsBuilder() + .UseSqlite("Data Source=platform.db", o => + o.MigrationsAssembly(typeof(MiGuDbContext).Assembly.GetName().Name)) + .Options; + return new MiGuDbContext(options); + } +} diff --git a/MiGu.DB/Kernel/Entities/EntityBases.cs b/MiGu.DB/Kernel/Entities/EntityBases.cs new file mode 100644 index 0000000..116515c --- /dev/null +++ b/MiGu.DB/Kernel/Entities/EntityBases.cs @@ -0,0 +1,46 @@ +using MiGu.DB.Abstractions.Entities; + +namespace MiGu.DB.Kernel.Entities; + +public abstract class Entity : IEntity +{ + public TKey Id { get; set; } = default!; +} + +public abstract class AuditedEntity : Entity, IAuditable +{ + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + public string CreatedBy { get; set; } = ""; + public string UpdatedBy { get; set; } = ""; +} + +public abstract class SoftDeletableEntity : AuditedEntity, ISoftDeletable, IVersioned, ILockable +{ + 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 abstract class AggregateRoot : SoftDeletableEntity, IRemarkable, IExtendable +{ + public string Remark { get; set; } = ""; + public string Extend { get; set; } = "{}"; +} + +public abstract class HistoryEntity : Entity, IHistoryEntry +{ + 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; } = "{}"; +} diff --git a/MiGu.DB/Kernel/Hosting/ServiceCollectionExtensions.cs b/MiGu.DB/Kernel/Hosting/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..67f82b1 --- /dev/null +++ b/MiGu.DB/Kernel/Hosting/ServiceCollectionExtensions.cs @@ -0,0 +1,224 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using MiGu.DB.Abstractions.Modules; +using MiGu.DB.Abstractions.Persistence; +using MiGu.DB.Abstractions.Providers; +using MiGu.DB.Abstractions.Runtime; +using MiGu.DB.Kernel.Context; +using MiGu.DB.Kernel.Conventions; +using MiGu.DB.Kernel.Providers; +using MiGu.DB.Kernel.Repositories; +using MiGu.DB.Domains.Migrators; + +namespace MiGu.DB.Kernel.Hosting; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMiGuDb( + this IServiceCollection services, + IConfiguration configuration, + Action? configure = null) + { + var options = new MiGuDbOptions + { + Provider = configuration["Database:Provider"] ?? "sqlite", + ConnectionString = configuration.GetConnectionString("Platform") + ?? configuration.GetConnectionString(ConnectionKey(configuration["Database:Provider"] ?? "sqlite")) + ?? "", + SchemaMode = ParseSchemaMode(configuration["Database:SchemaMode"]), + ApplyDataMigratorsOnStartup = configuration.GetValue("Database:ApplyDataMigratorsOnStartup", true) + }; + configure?.Invoke(options); + services.AddSingleton(options); + + services.TryAddSingleton(); + services.TryAddScoped(sp => sp.GetRequiredService().Current); + services.AddSingleton(); + + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + services.AddDbContext((sp, builder) => + { + var opt = sp.GetRequiredService(); + if (string.IsNullOrWhiteSpace(opt.ContentRootPath)) + { + var env = sp.GetService(); + opt.ContentRootPath = env?.ContentRootPath ?? AppContext.BaseDirectory; + } + + var providerName = NormalizeProviderName(opt.Provider); + var setup = sp.GetServices() + .FirstOrDefault(p => string.Equals(p.Name, providerName, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidOperationException($"未知数据库 Provider: {opt.Provider}"); + + setup.Configure(builder, ResolveConnectionString(opt, providerName)); + builder.AddInterceptors(sp.GetRequiredService()); + }); + + services.AddScoped(); + services.AddScoped(typeof(IRepository<,>), typeof(Repository<,>)); + services.AddScoped(typeof(IEditableRepository<>), typeof(EditableRepository<>)); + services.AddScoped(typeof(IHistoryRepository<>), typeof(HistoryRepository<>)); + + services.AddMiGuDataMigrators(); + return services; + } + + public static IServiceCollection AddMiGuEntityModule(this IServiceCollection services) + where TModule : class, IEntityModule, new() + { + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + new TModule().RegisterServices(services); + return services; + } + + private static string NormalizeProviderName(string provider) => provider.Trim().ToLowerInvariant() switch + { + "postgres" or "postgresql" => "npgsql", + "mssql" => "sqlserver", + var x => x + }; + + private static MiGuSchemaMode ParseSchemaMode(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return MiGuSchemaMode.Migrate; + if (Enum.TryParse(value.Trim(), ignoreCase: true, out var mode)) + return mode; + // 兼容简写 + return value.Trim().ToLowerInvariant() switch + { + "ensure" or "created" or "ensure-created" => MiGuSchemaMode.EnsureCreated, + "migrations" or "ef" => MiGuSchemaMode.Migrate, + _ => throw new InvalidOperationException( + $"未知 Database:SchemaMode '{value}',允许值:Migrate、EnsureCreated") + }; + } + + private static string ConnectionKey(string provider) => NormalizeProviderName(provider) switch + { + "npgsql" => "PostgreSQL", + "sqlserver" => "SqlServer", + "mysql" => "MySql", + _ => "Sqlite" + }; + + private static string ResolveConnectionString(MiGuDbOptions options, string providerName) + { + if (!string.IsNullOrWhiteSpace(options.ConnectionString)) + { + return providerName == "sqlite" + ? NormalizeSqliteConnection(options.ConnectionString, options.ContentRootPath) + : options.ConnectionString; + } + + var dataDir = Path.Combine(options.ContentRootPath, "data"); + Directory.CreateDirectory(dataDir); + return $"Data Source={Path.Combine(dataDir, "platform.db")}"; + } + + private static string NormalizeSqliteConnection(string connection, string contentRoot) + { + var builder = new SqliteConnectionStringBuilder(connection); + if (string.IsNullOrWhiteSpace(builder.DataSource) || builder.DataSource is ":memory:") + return connection; + if (!Path.IsPathRooted(builder.DataSource)) + builder.DataSource = Path.Combine(contentRoot, builder.DataSource); + var dir = Path.GetDirectoryName(builder.DataSource); + if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); + return builder.ToString(); + } +} + +public static class DatabaseInitializer +{ + /// + /// 启动期数据库初始化:按 SchemaMode 建库/迁移,再可选执行 DataMigrator。 + /// + public static async Task MigrateMiGuDbAsync(this IServiceProvider services, CancellationToken ct = default) + { + using var scope = services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetService()?.CreateLogger("MiGu.DB.DatabaseInitializer"); + + if (options.SchemaMode == MiGuSchemaMode.EnsureCreated) + { + logger?.LogWarning( + "Database:SchemaMode=EnsureCreated:仅按当前模型建库,不应用 Migrations。" + + "改实体后若结构未更新,请删除平台库文件后重启。发版请改用 Migrate。"); + await db.Database.EnsureCreatedAsync(ct); + } + else + { + var applied = (await db.Database.GetAppliedMigrationsAsync(ct)).ToList(); + var pending = (await db.Database.GetPendingMigrationsAsync(ct)).ToList(); + + if (applied.Count == 0 && pending.Count == 0) + { + logger?.LogWarning("程序集内无 Migration,回退 EnsureCreated。"); + await db.Database.EnsureCreatedAsync(ct); + } + else + { + // 旧库(EnsureCreated)无历史表行时写入基线,再 Migrate + await BaselineExistingDatabaseAsync(db, logger, ct); + await db.Database.MigrateAsync(ct); + } + } + + if (!options.ApplyDataMigratorsOnStartup) return; + + foreach (var migrator in scope.ServiceProvider.GetServices().OrderBy(m => m.Order)) + await migrator.MigrateAsync(db, ct); + } + + /// + /// 已有表结构但无 __EFMigrationsHistory 时:将全部 pending Migration 记为已应用。 + /// 前提:库由 EnsureCreated 按「当前模型 tip」建成,与最新 Snapshot 一致;否则应删库后走 Migrate,或手工对齐。 + /// + private static async Task BaselineExistingDatabaseAsync( + MiGuDbContext db, ILogger? logger, CancellationToken ct) + { + var applied = await db.Database.GetAppliedMigrationsAsync(ct); + if (applied.Any()) return; + + var pending = (await db.Database.GetPendingMigrationsAsync(ct)).ToList(); + if (pending.Count == 0) return; + + var creator = db.GetService(); + if (!await creator.ExistsAsync(ct) || !await creator.HasTablesAsync(ct)) + return; + + var history = db.GetService(); + var productVersion = ProductInfo.GetEFCoreVersion(); + logger?.LogWarning( + "检测到已有表且无迁移历史,将 {Count} 个 pending Migration 全部写入基线(假定库结构已对齐模型 tip):{Ids}", + pending.Count, string.Join(", ", pending)); + + foreach (var migrationId in pending) + { + var sql = history.GetInsertScript(new HistoryRow(migrationId, productVersion)); + await db.Database.ExecuteSqlRawAsync(sql, ct); + } + } +} + +internal static class ProductInfo +{ + public static string GetEFCoreVersion() + { + var asm = typeof(DbContext).Assembly.GetName().Version; + return asm?.ToString(3) ?? "8.0.10"; + } +} diff --git a/MiGu.DB/Kernel/Providers/ProviderSetups.cs b/MiGu.DB/Kernel/Providers/ProviderSetups.cs new file mode 100644 index 0000000..364247f --- /dev/null +++ b/MiGu.DB/Kernel/Providers/ProviderSetups.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using MiGu.DB.Abstractions.Providers; +using MiGu.DB.Kernel.Context; + +namespace MiGu.DB.Kernel.Providers; + +public sealed class SqliteProviderSetup : IDbProviderSetup +{ + public string Name => "sqlite"; + public string MigrationsAssemblyName => typeof(MiGuDbContext).Assembly.GetName().Name!; + public string MigrationsNamespace => "MiGu.DB.Migrations.Sqlite"; + + public void Configure(DbContextOptionsBuilder builder, string connectionString) + => builder.UseSqlite(connectionString, o => o.MigrationsAssembly(MigrationsAssemblyName) + .MigrationsHistoryTable("__EFMigrationsHistory")); +} + +public sealed class MySqlProviderSetup : IDbProviderSetup +{ + public string Name => "mysql"; + public string MigrationsAssemblyName => typeof(MiGuDbContext).Assembly.GetName().Name!; + public string MigrationsNamespace => "MiGu.DB.Migrations.MySql"; + + public void Configure(DbContextOptionsBuilder builder, string connectionString) + => builder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), + o => o.MigrationsAssembly(MigrationsAssemblyName)); +} + +public sealed class NpgsqlProviderSetup : IDbProviderSetup +{ + public string Name => "npgsql"; + public string MigrationsAssemblyName => typeof(MiGuDbContext).Assembly.GetName().Name!; + public string MigrationsNamespace => "MiGu.DB.Migrations.Npgsql"; + + public void Configure(DbContextOptionsBuilder builder, string connectionString) + => builder.UseNpgsql(connectionString, o => o.MigrationsAssembly(MigrationsAssemblyName)); +} + +public sealed class SqlServerProviderSetup : IDbProviderSetup +{ + public string Name => "sqlserver"; + public string MigrationsAssemblyName => typeof(MiGuDbContext).Assembly.GetName().Name!; + public string MigrationsNamespace => "MiGu.DB.Migrations.SqlServer"; + + public void Configure(DbContextOptionsBuilder builder, string connectionString) + => builder.UseSqlServer(connectionString, o => o.MigrationsAssembly(MigrationsAssemblyName)); +} diff --git a/MiGu.DB/Kernel/Repositories/Repositories.cs b/MiGu.DB/Kernel/Repositories/Repositories.cs new file mode 100644 index 0000000..e2ab9b5 --- /dev/null +++ b/MiGu.DB/Kernel/Repositories/Repositories.cs @@ -0,0 +1,116 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using MiGu.DB.Abstractions.Entities; +using MiGu.DB.Abstractions.Exceptions; +using MiGu.DB.Abstractions.Persistence; +using MiGu.DB.Abstractions.Runtime; +using MiGu.DB.Kernel.Context; + +namespace MiGu.DB.Kernel.Repositories; + +public class Repository : IRepository + where TEntity : class, IEntity +{ + protected readonly MiGuDbContext Db; + protected DbSet Set => Db.Set(); + + public Repository(MiGuDbContext db) => Db = db; + + public virtual IQueryable Query(bool asNoTracking = true) + => asNoTracking ? Set.AsNoTracking() : Set.AsQueryable(); + + public virtual Task FindAsync(TKey id, CancellationToken ct = default) + => Set.FindAsync([id], ct).AsTask(); + + public virtual Task AddAsync(TEntity entity, CancellationToken ct = default) + => Set.AddAsync(entity, ct).AsTask(); + + public virtual void Update(TEntity entity) => Set.Update(entity); +} + +public sealed class EditableRepository : Repository, IEditableRepository + where TEntity : class, IEntity, ISoftDeletable, IVersioned, ILockable +{ + private readonly IActorContextAccessor _actors; + + public EditableRepository(MiGuDbContext db, IActorContextAccessor actors) : base(db) + => _actors = actors; + + public async Task GetEditableAsync(Guid id, long? expectedVersion, CancellationToken ct = default) + { + var entity = await Set.FirstOrDefaultAsync(x => x.Id.Equals(id), ct) + ?? throw new EntityNotFoundException(typeof(TEntity).Name, id); + if (entity.IsLock) + throw new EntityLockedException(typeof(TEntity).Name, id); + if (expectedVersion.HasValue && entity.Version != expectedVersion.Value) + throw new ConcurrencyConflictException(typeof(TEntity).Name, id, expectedVersion); + return entity; + } + + public async Task SoftDeleteAsync(Guid id, long? expectedVersion, CancellationToken ct = default) + { + var entity = await GetEditableAsync(id, expectedVersion, ct); + entity.IsDeleted = true; + entity.DeletedAt = DateTimeOffset.UtcNow; + entity.DeletedBy = _actors.Current.Name; + if (entity is IAuditable auditable) + auditable.UpdatedBy = _actors.Current.Name; + } + + public async Task EnsureUniqueAsync(Expression> predicate, string errorMessage, CancellationToken ct = default) + { + if (await Set.AnyAsync(predicate, ct)) + throw new InvalidOperationException(errorMessage); + } +} + +public sealed class HistoryRepository : Repository, IHistoryRepository + where TEntity : class, IEntity, IHistoryEntry +{ + public HistoryRepository(MiGuDbContext db) : base(db) { } + + public Task AppendAsync(TEntity entry, CancellationToken ct = default) + => AddAsync(entry, ct); +} + +public sealed class UnitOfWork : IUnitOfWork +{ + private readonly MiGuDbContext _db; + + public UnitOfWork(MiGuDbContext db) => _db = db; + + public async Task SaveChangesAsync(CancellationToken ct = default) + { + try + { + return await _db.SaveChangesAsync(ct); + } + catch (DbUpdateConcurrencyException ex) + { + var entry = ex.Entries.FirstOrDefault(); + throw new ConcurrencyConflictException( + entry?.Entity.GetType().Name ?? "Unknown", + entry?.Property("Id")?.CurrentValue); + } + } + + public Task ExecuteInTransactionAsync(Func action, CancellationToken ct = default) + { + var strategy = _db.Database.CreateExecutionStrategy(); + return strategy.ExecuteAsync(async () => + { + await using var tx = await _db.Database.BeginTransactionAsync(ct); + try + { + await action(ct); + await _db.SaveChangesAsync(ct); + await tx.CommitAsync(ct); + } + catch + { + await tx.RollbackAsync(ct); + throw; + } + }); + } +} diff --git a/MiGu.DB/MIGRATIONS.md b/MiGu.DB/MIGRATIONS.md new file mode 100644 index 0000000..2ca301d --- /dev/null +++ b/MiGu.DB/MIGRATIONS.md @@ -0,0 +1,221 @@ +# MiGu.DB Migrations 使用手册 + +面向日常改表、发版与排错。当前默认 Provider 为 **Sqlite**,迁移目录为 `Migrations/Sqlite/`。 + +--- + +## 1. 概念速览 + +| 概念 | 说明 | +|------|------| +| Migration | 一次 Schema 变更(建表/加列/索引等),对应一对 `*_Name.cs` + `*_Name.Designer.cs` | +| ModelSnapshot | `MiGuDbContextModelSnapshot.cs`,当前模型总快照;下次 `add` 时与代码模型做 diff | +| `__EFMigrationsHistory` | 数据库内表,记录已应用的 Migration Id | +| DataMigrator | **数据**修补(刷旧状态、回填列),不是 Schema;启动时在 Migrate 之后执行 | + +**原则:Schema 只走 Migrations;业务代码禁止手写 CREATE/ALTER。** + +三个文件职责: + +- `YYYYMMDDHHMMSS_Name.cs` → `Up()`/`Down()`,真正改库 +- `YYYYMMDDHHMMSS_Name.Designer.cs` → 该次迁移的目标模型元数据(勿手改) +- `MiGuDbContextModelSnapshot.cs` → 全库最新快照(勿手改,除非处理合并冲突) + +--- + +## 2. 环境准备 + +### 2.1 工具 + +仓库根目录(`Migu2.0`)执行: + +```powershell +# 全局工具(任选) +dotnet tool install --global dotnet-ef --version 8.0.10 + +# 或本地工具目录(本仓库曾用此方式) +dotnet tool install dotnet-ef --version 8.0.10 --tool-path .\.tools +.\.tools\dotnet-ef --version +``` + +版本需与项目 EF Core **8.0.x** 对齐。 + +### 2.2 工程关系 + +| 参数 | 值 | +|------|-----| +| 迁移所在工程 | `MiGu.DB` | +| 启动工程 | `MiGu.Server`(提供配置与 Design 包) | +| DbContext | `MiGu.DB.Kernel.Context.MiGuDbContext` | +| Design-time 工厂 | `MiGu.DB.Kernel.Design.MiGuDbContextFactory` | + +`MiGu.Server.csproj` 已引用 `Microsoft.EntityFrameworkCore.Design`;`MiGu.DB` 含 Sqlite 等 Provider 包。 + +--- + +## 3. 生成 Migration(日常流程) + +### 3.1 改模型 + +在 `MiGu.DB/Domains` 改实体或 `IEntityTypeConfiguration`,保存后编译通过: + +```powershell +dotnet build .\MiGu.DB\MiGu.DB.csproj +``` + +### 3.2 添加迁移 + +在**解决方案根目录**执行(PowerShell): + +```powershell +dotnet ef migrations add <迁移名称> ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --output-dir Migrations/Sqlite ` + --namespace MiGu.DB.Migrations.Sqlite ` + --context MiGuDbContext +``` + +命名建议(PascalCase,无空格): + +| 场景 | 示例名 | +|------|--------| +| 首库 | `InitialPlatform`(已存在,勿重复) | +| 加表 | `AddWmsXxxTable` | +| 加列 | `AddStoragePriorityColumn` | +| 加索引 | `AddStockEventOperatedAtIndex` | + +### 3.3 生成后检查(必做) + +1. 新文件应在:`MiGu.DB/Migrations/Sqlite/` +2. 打开 `*_Name.cs`,确认 `Up()` 只包含**本次预期**变更(无误删表、无多余重建) +3. 确认 `MiGuDbContextModelSnapshot.cs` 仍在 `Migrations/Sqlite/` + - 若出现在 `MiGu.DB/MiGu/DB/Migrations/Sqlite/` 等错误路径:把 Snapshot **移回**正确目录并删掉空目录(`--namespace` 偶发路径问题) + +### 3.4 应用到本地库 + +启动 `MiGu.Server` 即可(`Program.cs` 直接调用 `MigrateMiGuDbAsync`),或: + +```powershell +dotnet ef database update ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --context MiGuDbContext +``` + +--- + +## 4. 常用命令 + +```powershell +# 列出迁移 +dotnet ef migrations list ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --context MiGuDbContext + +# 生成 SQL 脚本(发版/DBA 审阅,不直接连库执行也可) +dotnet ef migrations script ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --context MiGuDbContext ` + --output .\MiGu.DB\Migrations\Sqlite\script.sql + +# 从某迁移到最新(含幂等脚本时加 --idempotent) +dotnet ef migrations script FromMigration ToMigration ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --context MiGuDbContext ` + --idempotent + +# 删除「尚未应用到任何重要库」的最后一次迁移(仅开发) +dotnet ef migrations remove ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --context MiGuDbContext + +# 回滚到指定迁移(会执行 Down,生产慎用) +dotnet ef database update <目标迁移名> ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --context MiGuDbContext +``` + +使用本地工具时,将 `dotnet ef` 换成 `.\.tools\dotnet-ef`。 + +--- + +## 5. 启动时发生了什么 + +**完整说明(顺序、配置、SchemaMode、开发注意)见:** + +→ **[MiGu.Server/README.md · 数据库启动流程](../MiGu.Server/README.md#数据库启动流程)** + +摘要:`MigrateMiGuDbAsync`;`Database:SchemaMode` 为 `EnsureCreated` 或 `Migrate`;之后按需跑 `IDataMigrator`。 + +**Migrate 基线:** 已有表、无 History 时,会把**全部** pending Migration 写入 `__EFMigrationsHistory`(假定 EnsureCreated 库已对齐模型 tip)。发版前若开发期改过模型,须先 `migrations add` 再切 `Migrate`。 + +--- + +## 6. Schema 变更 vs 数据修补 + +| 需求 | 做法 | +|------|------| +| 新表/新列/索引/改列类型 | `migrations add` → 提交迁移文件 | +| 刷旧枚举字符串、回填列 | 新增 `IDataMigrator`,注册到 `AddMiGuDataMigrators` | +| 开发机整库清空重来 | 删 `data/platform.db*` 后启动(等同空库 Migrate);**不要**在生产用 EnsureDeleted | + +注意:带 `HasConversion` 的枚举列,用 `ExecuteUpdate` + 原始字符串比较可能触发转换异常;空 LayoutMode 等特例用 raw UPDATE(参见 `AreaLayoutModeDefaultMigrator`)。 + +--- + +## 7. 协作与发版 + +1. **迁移文件必须入库**(含 Designer、Snapshot) +2. 多人同时改模型易冲突 Snapshot:保留一方迁移,另一方 `remove` 后基于最新代码重新 `add` +3. 已合并到主分支并可能已应用到共享库的迁移:**不要** `migrations remove` 或改写历史 `Up()` +4. 发版包随程序集带上 Migration;现场首次升级靠启动 Migrate(或预执行 `migrations script`) + +--- + +## 8. 其他 Provider(预留) + +`IDbProviderSetup` 已预留 MySql / Npgsql / SqlServer。首轮只有 Sqlite 迁移套。 + +将来为企业库生成独立套时: + +```powershell +# 示例:输出到 Migrations/MySql,namespace 同步修改 +dotnet ef migrations add InitialPlatform ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --output-dir Migrations/MySql ` + --namespace MiGu.DB.Migrations.MySql ` + --context MiGuDbContext +``` + +并确保对应 Provider 的 `MigrationsAssembly` / 运行时能发现该套迁移(按需拆程序集或过滤命名空间)。 + +--- + +## 9. 常见问题 + +| 现象 | 处理 | +|------|------| +| `dotnet ef` 找不到 | 安装 8.0.10 工具,或用 `.\.tools\dotnet-ef` | +| Design-time 连错库 | 检查 `MiGuDbContextFactory`(默认 `platform.db`);运行时以 Server 配置为准 | +| Snapshot 生成到奇怪目录 | 移回 `Migrations/Sqlite/` | +| 旧库启动重复建表失败 | 确认基线逻辑是否写入 History;备份后必要时手工插入 Initial 行 | +| 改完实体 `add` 生成空迁移 | 模型无差异或未编译;先 `dotnet build` | +| 想撤销未提交的迁移 | `migrations remove`(确认未被他人/现场应用) | + +--- + +## 10. 检查清单(每次提 PR) + +- [ ] `dotnet build` 通过 +- [ ] 新迁移仅含预期 DDL +- [ ] 文件在 `Migrations/Sqlite/`,Snapshot 路径正确 +- [ ] 本地启动一次,确认 Migrate 成功 +- [ ] 若有数据刷库,已加幂等 `IDataMigrator` 并注明 Order +- [ ] 未改写已发布的历史 Migration diff --git a/MiGu.DB/MiGu.DB.csproj b/MiGu.DB/MiGu.DB.csproj new file mode 100644 index 0000000..3b8066c --- /dev/null +++ b/MiGu.DB/MiGu.DB.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + MiGu.DB + MiGu.DB + latest + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/MiGu.DB/Migrations/Sqlite/.gitkeep b/MiGu.DB/Migrations/Sqlite/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/MiGu.DB/Migrations/Sqlite/20260727011546_InitialPlatform.Designer.cs b/MiGu.DB/Migrations/Sqlite/20260727011546_InitialPlatform.Designer.cs new file mode 100644 index 0000000..689baf4 --- /dev/null +++ b/MiGu.DB/Migrations/Sqlite/20260727011546_InitialPlatform.Designer.cs @@ -0,0 +1,1686 @@ +// +using System; +using MiGu.DB.Kernel.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MiGu.DB.Migrations.Sqlite +{ + [DbContext(typeof(MiGuDbContext))] + [Migration("20260727011546_InitialPlatform")] + partial class InitialPlatform + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("MiGu.DB.Domains.Dashboard.UserDashboardShortcut", b => + { + b.Property("UserId") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Scope") + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("scope"); + + b.Property("KeysJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("keys_json"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId", "Scope"); + + b.ToTable("user_dashboard_shortcuts", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.SimpleFields.SimpleField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CarType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("car_type"); + + b.Property("Chinese") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("chinese"); + + b.Property("CreateTime") + .IsRequired() + .HasMaxLength(19) + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("data_type"); + + b.Property("English") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("english"); + + b.Property("FieldType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("field_type"); + + b.Property("IsDefault") + .HasColumnType("INTEGER") + .HasColumnName("is_default"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("Other") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("other"); + + b.Property("UpdateTime") + .IsRequired() + .HasMaxLength(19) + .HasColumnType("TEXT") + .HasColumnName("update_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("value"); + + b.HasKey("Id"); + + b.HasIndex("CarType", "FieldType", "Key") + .IsUnique(); + + b.ToTable("simple_fields", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SourceStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("TargetStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId", "Status"); + + b.HasIndex("TargetStorageId", "Status"); + + b.ToTable("wms_transport_reservations", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SourceSelectorJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetSelectorJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskOptionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("TriggerType", "Enabled", "Priority"); + + b.ToTable("wms_transport_rules", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BusinessType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeliveryId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DispatchMissionId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DispatchStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RuleId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("TargetStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("TaskPriority") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("Status"); + + b.HasIndex("TargetStorageId"); + + b.ToTable("wms_transport_tasks", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportTaskHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OperatedAt"); + + b.HasIndex("TaskId"); + + b.ToTable("wms_transport_task_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Container", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AreaId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("REAL"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Length") + .HasColumnType("REAL"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_containers", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("EnteredAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LocationCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LocationName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId") + .IsUnique(); + + b.HasIndex("StorageId"); + + b.HasIndex("LocationType", "LocationId"); + + b.ToTable("wms_container_locations", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerLocationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AfterJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("BeforeJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("FromLocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RelationId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ToLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ToLocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_container_location_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerMaterial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("BoundAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LoadedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("MaterialId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SerialNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UnloadedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("MaterialId") + .IsUnique(); + + b.ToTable("wms_container_materials", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerMaterialHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AfterJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("BeforeJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("QuantityDelta") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RelationId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_container_material_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Material", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LifecycleStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Spec") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TypeCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UnboundAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Barcode"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("TypeCode"); + + b.HasIndex("LifecycleStatus", "UpdatedAt"); + + b.ToTable("wms_materials", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.MaterialType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BarcodePrefix") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Spec") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_material_types", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.StockEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AreaCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("FromStorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("FromStorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("FromStorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialBarcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("MaterialName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialTypeCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RefCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RefId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("RefType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("StorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ToStorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ToStorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ToStorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("MaterialId"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_stock_events", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Storage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AllowInbound") + .HasColumnType("INTEGER"); + + b.Property("AllowOutbound") + .HasColumnType("INTEGER"); + + b.Property("AreaId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Capacity") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ColumnNo") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DepthNo") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LevelNo") + .HasColumnType("INTEGER"); + + b.Property("LocationKind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SiteCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SiteId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("StorageType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ZoneCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("Barcode"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_storages", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_warehouses", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.WarehouseArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LayoutMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("WarehouseId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("WarehouseId"); + + b.ToTable("wms_areas", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MiGu.DB/Migrations/Sqlite/20260727011546_InitialPlatform.cs b/MiGu.DB/Migrations/Sqlite/20260727011546_InitialPlatform.cs new file mode 100644 index 0000000..e962ba2 --- /dev/null +++ b/MiGu.DB/Migrations/Sqlite/20260727011546_InitialPlatform.cs @@ -0,0 +1,754 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MiGu.DB.Migrations.Sqlite +{ + /// + /// Sqlite 初始 Schema(平台库全量表)。旧 EnsureCreated 库启动时会先基线本迁移名,再应用后续增量。 + /// + public partial class InitialPlatform : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "simple_fields", + columns: table => new + { + id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + car_type = table.Column(type: "TEXT", maxLength: 64, nullable: false), + field_type = table.Column(type: "TEXT", maxLength: 64, nullable: false), + key = table.Column(type: "TEXT", maxLength: 128, nullable: false), + value = table.Column(type: "TEXT", nullable: false), + data_type = table.Column(type: "TEXT", maxLength: 128, nullable: false), + chinese = table.Column(type: "TEXT", maxLength: 256, nullable: true), + english = table.Column(type: "TEXT", maxLength: 256, nullable: true), + other = table.Column(type: "TEXT", maxLength: 512, nullable: false), + is_default = table.Column(type: "INTEGER", nullable: false), + create_time = table.Column(type: "TEXT", maxLength: 19, nullable: false), + update_time = table.Column(type: "TEXT", maxLength: 19, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_simple_fields", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "user_dashboard_shortcuts", + columns: table => new + { + user_id = table.Column(type: "TEXT", maxLength: 64, nullable: false), + scope = table.Column(type: "TEXT", maxLength: 32, nullable: false), + keys_json = table.Column(type: "text", nullable: false), + updated_at = table.Column(type: "TEXT", maxLength: 40, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_dashboard_shortcuts", x => new { x.user_id, x.scope }); + }); + + migrationBuilder.CreateTable( + name: "wms_areas", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + WarehouseId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Code = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Type = table.Column(type: "TEXT", maxLength: 64, nullable: false), + LayoutMode = table.Column(type: "TEXT", maxLength: 32, nullable: false), + State = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_areas", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_container_location_history", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + FromLocationType = table.Column(type: "TEXT", maxLength: 32, nullable: false), + FromLocationId = table.Column(type: "TEXT", maxLength: 64, nullable: false), + ToLocationType = table.Column(type: "TEXT", maxLength: 32, nullable: false), + ToLocationId = table.Column(type: "TEXT", maxLength: 64, nullable: false), + RelationId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + EventType = table.Column(type: "TEXT", maxLength: 64, nullable: false), + BeforeJson = table.Column(type: "text", nullable: false), + AfterJson = table.Column(type: "text", nullable: false), + ContainerId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + Operator = table.Column(type: "TEXT", maxLength: 128, nullable: false), + OperatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + Source = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Reason = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_container_location_history", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_container_locations", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + ContainerId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + LocationType = table.Column(type: "TEXT", maxLength: 32, nullable: false), + LocationId = table.Column(type: "TEXT", maxLength: 64, nullable: false), + StorageId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + LocationCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + LocationName = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 32, nullable: false), + EnteredAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_container_locations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_container_material_history", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + MaterialId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + QuantityDelta = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + RelationId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + EventType = table.Column(type: "TEXT", maxLength: 64, nullable: false), + BeforeJson = table.Column(type: "text", nullable: false), + AfterJson = table.Column(type: "text", nullable: false), + ContainerId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + Operator = table.Column(type: "TEXT", maxLength: 128, nullable: false), + OperatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + Source = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Reason = table.Column(type: "TEXT", maxLength: 500, nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_container_material_history", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_container_materials", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + ContainerId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + MaterialId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Quantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: false), + BatchNo = table.Column(type: "TEXT", maxLength: 64, nullable: false), + SerialNo = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 32, nullable: false), + BoundAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + LoadedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UnloadedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_container_materials", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_containers", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + AreaId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + Code = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ContainerType = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Barcode = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Length = table.Column(type: "REAL", nullable: false), + Width = table.Column(type: "REAL", nullable: false), + Height = table.Column(type: "REAL", nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_containers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_material_types", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Code = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Spec = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Unit = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Category = table.Column(type: "TEXT", maxLength: 64, nullable: false), + BarcodePrefix = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_material_types", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_materials", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Code = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + TypeCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Barcode = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Spec = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Unit = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Category = table.Column(type: "TEXT", maxLength: 64, nullable: false), + LifecycleStatus = table.Column(type: "TEXT", maxLength: 32, nullable: false), + UnboundAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + Enabled = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_materials", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_stock_events", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + EventType = table.Column(type: "TEXT", maxLength: 32, nullable: false), + MaterialId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + MaterialCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + MaterialName = table.Column(type: "TEXT", maxLength: 128, nullable: false), + MaterialBarcode = table.Column(type: "TEXT", maxLength: 128, nullable: false), + MaterialTypeCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + ContainerId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + ContainerCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + ContainerName = table.Column(type: "TEXT", maxLength: 128, nullable: false), + StorageId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + StorageCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + StorageName = table.Column(type: "TEXT", maxLength: 128, nullable: false), + AreaCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + FromStorageId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + FromStorageCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + FromStorageName = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ToStorageId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + ToStorageCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + ToStorageName = table.Column(type: "TEXT", maxLength: 128, nullable: false), + RefType = table.Column(type: "TEXT", maxLength: 64, nullable: false), + RefId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + RefCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Operator = table.Column(type: "TEXT", maxLength: 128, nullable: false), + OperatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + Reason = table.Column(type: "TEXT", maxLength: 500, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_stock_events", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_storages", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + AreaId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Code = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + StorageType = table.Column(type: "TEXT", maxLength: 64, nullable: false), + LocationKind = table.Column(type: "TEXT", maxLength: 32, nullable: false), + ColumnNo = table.Column(type: "INTEGER", nullable: false), + LevelNo = table.Column(type: "INTEGER", nullable: false), + DepthNo = table.Column(type: "INTEGER", nullable: false), + SiteId = table.Column(type: "TEXT", maxLength: 64, nullable: false), + SiteCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Barcode = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Capacity = table.Column(type: "INTEGER", nullable: false), + Status = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Usage = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Priority = table.Column(type: "INTEGER", nullable: false), + ZoneCode = table.Column(type: "TEXT", maxLength: 64, nullable: false), + AllowInbound = table.Column(type: "INTEGER", nullable: false), + AllowOutbound = table.Column(type: "INTEGER", nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_storages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_transport_reservations", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + TaskId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + ContainerId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + SourceStorageId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + TargetStorageId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 32, nullable: false), + ExpiresAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_transport_reservations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_transport_rules", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Code = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + TriggerType = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false), + Priority = table.Column(type: "INTEGER", nullable: false), + SourceSelectorJson = table.Column(type: "text", nullable: false), + TargetSelectorJson = table.Column(type: "text", nullable: false), + TaskOptionsJson = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_transport_rules", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_transport_task_history", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + TaskId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + FromStatus = table.Column(type: "TEXT", maxLength: 32, nullable: false), + ToStatus = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Operator = table.Column(type: "TEXT", maxLength: 128, nullable: false), + OperatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + Reason = table.Column(type: "TEXT", maxLength: 500, nullable: false), + ErrorMessage = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + SnapshotJson = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_transport_task_history", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_transport_tasks", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + BusinessType = table.Column(type: "TEXT", maxLength: 64, nullable: false), + RuleId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + SourceStorageId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + TargetStorageId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + ContainerId = table.Column(type: "TEXT", maxLength: 36, nullable: false), + MaterialId = table.Column(type: "TEXT", maxLength: 36, nullable: true), + Quantity = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + Status = table.Column(type: "TEXT", maxLength: 32, nullable: false), + DispatchMissionId = table.Column(type: "TEXT", maxLength: 64, nullable: false), + DeliveryId = table.Column(type: "TEXT", maxLength: 64, nullable: false), + DispatchStatus = table.Column(type: "TEXT", maxLength: 32, nullable: false), + Reason = table.Column(type: "TEXT", maxLength: 500, nullable: false), + SnapshotJson = table.Column(type: "text", nullable: false), + ErrorMessage = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + TaskPriority = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_transport_tasks", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "wms_warehouses", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + Code = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Type = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + UpdatedAt = table.Column(type: "TEXT", maxLength: 40, nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UpdatedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", maxLength: 40, nullable: true), + DeletedBy = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Version = table.Column(type: "INTEGER", nullable: false), + IsLock = table.Column(type: "INTEGER", nullable: false), + Remark = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Extend = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_wms_warehouses", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_simple_fields_car_type_field_type_key", + table: "simple_fields", + columns: new[] { "car_type", "field_type", "key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_areas_Code", + table: "wms_areas", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_areas_WarehouseId", + table: "wms_areas", + column: "WarehouseId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_location_history_ContainerId", + table: "wms_container_location_history", + column: "ContainerId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_location_history_EventType", + table: "wms_container_location_history", + column: "EventType"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_location_history_OperatedAt", + table: "wms_container_location_history", + column: "OperatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_locations_ContainerId", + table: "wms_container_locations", + column: "ContainerId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_locations_LocationType_LocationId", + table: "wms_container_locations", + columns: new[] { "LocationType", "LocationId" }); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_locations_StorageId", + table: "wms_container_locations", + column: "StorageId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_material_history_ContainerId", + table: "wms_container_material_history", + column: "ContainerId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_material_history_EventType", + table: "wms_container_material_history", + column: "EventType"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_material_history_OperatedAt", + table: "wms_container_material_history", + column: "OperatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_materials_ContainerId", + table: "wms_container_materials", + column: "ContainerId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_container_materials_MaterialId", + table: "wms_container_materials", + column: "MaterialId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_containers_Code", + table: "wms_containers", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_material_types_Code", + table: "wms_material_types", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_materials_Barcode", + table: "wms_materials", + column: "Barcode"); + + migrationBuilder.CreateIndex( + name: "IX_wms_materials_Code", + table: "wms_materials", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_materials_LifecycleStatus_UpdatedAt", + table: "wms_materials", + columns: new[] { "LifecycleStatus", "UpdatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_wms_materials_TypeCode", + table: "wms_materials", + column: "TypeCode"); + + migrationBuilder.CreateIndex( + name: "IX_wms_stock_events_ContainerId", + table: "wms_stock_events", + column: "ContainerId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_stock_events_EventType", + table: "wms_stock_events", + column: "EventType"); + + migrationBuilder.CreateIndex( + name: "IX_wms_stock_events_MaterialId", + table: "wms_stock_events", + column: "MaterialId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_stock_events_OperatedAt", + table: "wms_stock_events", + column: "OperatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_wms_storages_AreaId", + table: "wms_storages", + column: "AreaId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_storages_Barcode", + table: "wms_storages", + column: "Barcode"); + + migrationBuilder.CreateIndex( + name: "IX_wms_storages_Code", + table: "wms_storages", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_reservations_ContainerId_Status", + table: "wms_transport_reservations", + columns: new[] { "ContainerId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_reservations_TargetStorageId_Status", + table: "wms_transport_reservations", + columns: new[] { "TargetStorageId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_rules_Code", + table: "wms_transport_rules", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_rules_TriggerType_Enabled_Priority", + table: "wms_transport_rules", + columns: new[] { "TriggerType", "Enabled", "Priority" }); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_task_history_OperatedAt", + table: "wms_transport_task_history", + column: "OperatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_task_history_TaskId", + table: "wms_transport_task_history", + column: "TaskId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_tasks_ContainerId", + table: "wms_transport_tasks", + column: "ContainerId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_tasks_Status", + table: "wms_transport_tasks", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_wms_transport_tasks_TargetStorageId", + table: "wms_transport_tasks", + column: "TargetStorageId"); + + migrationBuilder.CreateIndex( + name: "IX_wms_warehouses_Code", + table: "wms_warehouses", + column: "Code", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "simple_fields"); + + migrationBuilder.DropTable( + name: "user_dashboard_shortcuts"); + + migrationBuilder.DropTable( + name: "wms_areas"); + + migrationBuilder.DropTable( + name: "wms_container_location_history"); + + migrationBuilder.DropTable( + name: "wms_container_locations"); + + migrationBuilder.DropTable( + name: "wms_container_material_history"); + + migrationBuilder.DropTable( + name: "wms_container_materials"); + + migrationBuilder.DropTable( + name: "wms_containers"); + + migrationBuilder.DropTable( + name: "wms_material_types"); + + migrationBuilder.DropTable( + name: "wms_materials"); + + migrationBuilder.DropTable( + name: "wms_stock_events"); + + migrationBuilder.DropTable( + name: "wms_storages"); + + migrationBuilder.DropTable( + name: "wms_transport_reservations"); + + migrationBuilder.DropTable( + name: "wms_transport_rules"); + + migrationBuilder.DropTable( + name: "wms_transport_task_history"); + + migrationBuilder.DropTable( + name: "wms_transport_tasks"); + + migrationBuilder.DropTable( + name: "wms_warehouses"); + } + } +} diff --git a/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.Designer.cs b/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.Designer.cs new file mode 100644 index 0000000..628dd18 --- /dev/null +++ b/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.Designer.cs @@ -0,0 +1,1882 @@ +// +using System; +using MiGu.DB.Kernel.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MiGu.DB.Migrations.Sqlite +{ + [DbContext(typeof(MiGuDbContext))] + [Migration("20260727070415_AddFleetTables")] + partial class AddFleetTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("MiGu.DB.Domains.Dashboard.UserDashboardShortcut", b => + { + b.Property("UserId") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Scope") + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("scope"); + + b.Property("KeysJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("keys_json"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId", "Scope"); + + b.ToTable("user_dashboard_shortcuts", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Fleet.CdmTaskRecord", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CarId") + .HasColumnType("INTEGER") + .HasColumnName("car_id"); + + b.Property("CarName") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("car_name"); + + b.Property("CreateTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("DstLabel") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("dst_label"); + + b.Property("DstSiteId") + .HasColumnType("INTEGER") + .HasColumnName("dst_site_id"); + + b.Property("FinishTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("finish_time"); + + b.Property("FirstSeenAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("first_seen_at"); + + b.Property("LastSeenAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("last_seen_at"); + + b.Property("MissionId") + .HasColumnType("INTEGER") + .HasColumnName("mission_id"); + + b.Property("MissionName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("mission_name"); + + b.Property("MissionTypeName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("mission_type"); + + b.Property("Overdue") + .HasColumnType("INTEGER") + .HasColumnName("overdue"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("SrcLabel") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("src_label"); + + b.Property("SrcSiteId") + .HasColumnType("INTEGER") + .HasColumnName("src_site_id"); + + b.Property("StartTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("start_time"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("status_code"); + + b.Property("StuckReason") + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("stuck_reason"); + + b.Property("TaskId") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.HasKey("Id"); + + b.HasIndex("CreateTime"); + + b.HasIndex("StatusCode"); + + b.ToTable("cdm_tasks", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Fleet.VehicleAlarmRecord", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Acknowledged") + .HasColumnType("INTEGER") + .HasColumnName("acknowledged"); + + b.Property("AcknowledgedAt") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("acknowledged_at"); + + b.Property("AcknowledgedBy") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("acknowledged_by"); + + b.Property("CarId") + .HasColumnType("INTEGER") + .HasColumnName("car_id"); + + b.Property("CarName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("car_name"); + + b.Property("DurationSecs") + .HasColumnType("INTEGER") + .HasColumnName("duration_secs"); + + b.Property("FirstAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("first_at"); + + b.Property("Info") + .IsRequired() + .HasColumnType("text") + .HasColumnName("info"); + + b.Property("LastAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("last_at"); + + b.Property("Level") + .HasColumnType("INTEGER") + .HasColumnName("level"); + + b.Property("ResolvedAt") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("FirstAt"); + + b.HasIndex("Status"); + + b.HasIndex("CarId", "Status"); + + b.ToTable("vehicle_alarms", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.SimpleFields.SimpleField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CarType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("car_type"); + + b.Property("Chinese") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("chinese"); + + b.Property("CreateTime") + .IsRequired() + .HasMaxLength(19) + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("data_type"); + + b.Property("English") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("english"); + + b.Property("FieldType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("field_type"); + + b.Property("IsDefault") + .HasColumnType("INTEGER") + .HasColumnName("is_default"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("Other") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("other"); + + b.Property("UpdateTime") + .IsRequired() + .HasMaxLength(19) + .HasColumnType("TEXT") + .HasColumnName("update_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("value"); + + b.HasKey("Id"); + + b.HasIndex("CarType", "FieldType", "Key") + .IsUnique(); + + b.ToTable("simple_fields", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SourceStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("TargetStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId", "Status"); + + b.HasIndex("TargetStorageId", "Status"); + + b.ToTable("wms_transport_reservations", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SourceSelectorJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetSelectorJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskOptionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("TriggerType", "Enabled", "Priority"); + + b.ToTable("wms_transport_rules", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BusinessType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeliveryId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DispatchMissionId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DispatchStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RuleId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("TargetStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("TaskPriority") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("Status"); + + b.HasIndex("TargetStorageId"); + + b.ToTable("wms_transport_tasks", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportTaskHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OperatedAt"); + + b.HasIndex("TaskId"); + + b.ToTable("wms_transport_task_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Container", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AreaId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("REAL"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Length") + .HasColumnType("REAL"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_containers", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("EnteredAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LocationCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LocationName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId") + .IsUnique(); + + b.HasIndex("StorageId"); + + b.HasIndex("LocationType", "LocationId"); + + b.ToTable("wms_container_locations", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerLocationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AfterJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("BeforeJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("FromLocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RelationId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ToLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ToLocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_container_location_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerMaterial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("BoundAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LoadedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("MaterialId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SerialNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UnloadedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("MaterialId") + .IsUnique(); + + b.ToTable("wms_container_materials", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerMaterialHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AfterJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("BeforeJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("QuantityDelta") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RelationId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_container_material_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Material", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LifecycleStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Spec") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TypeCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UnboundAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Barcode"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("TypeCode"); + + b.HasIndex("LifecycleStatus", "UpdatedAt"); + + b.ToTable("wms_materials", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.MaterialType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BarcodePrefix") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Spec") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_material_types", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.StockEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AreaCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("FromStorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("FromStorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("FromStorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialBarcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("MaterialName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialTypeCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RefCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RefId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("RefType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("StorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ToStorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ToStorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ToStorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("MaterialId"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_stock_events", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Storage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AllowInbound") + .HasColumnType("INTEGER"); + + b.Property("AllowOutbound") + .HasColumnType("INTEGER"); + + b.Property("AreaId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Capacity") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ColumnNo") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DepthNo") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LevelNo") + .HasColumnType("INTEGER"); + + b.Property("LocationKind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SiteCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SiteId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("StorageType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ZoneCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("Barcode"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_storages", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_warehouses", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.WarehouseArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LayoutMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("WarehouseId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("WarehouseId"); + + b.ToTable("wms_areas", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.cs b/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.cs new file mode 100644 index 0000000..32e050a --- /dev/null +++ b/MiGu.DB/Migrations/Sqlite/20260727070415_AddFleetTables.cs @@ -0,0 +1,103 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MiGu.DB.Migrations.Sqlite +{ + /// + public partial class AddFleetTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "cdm_tasks", + columns: table => new + { + id = table.Column(type: "TEXT", maxLength: 64, nullable: false), + task_id = table.Column(type: "TEXT", maxLength: 128, nullable: true), + mission_id = table.Column(type: "INTEGER", nullable: false), + mission_name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + mission_type = table.Column(type: "TEXT", maxLength: 128, nullable: false), + src_site_id = table.Column(type: "INTEGER", nullable: false), + src_label = table.Column(type: "TEXT", maxLength: 256, nullable: false), + dst_site_id = table.Column(type: "INTEGER", nullable: false), + dst_label = table.Column(type: "TEXT", maxLength: 256, nullable: false), + status = table.Column(type: "TEXT", maxLength: 32, nullable: false), + status_code = table.Column(type: "TEXT", maxLength: 32, nullable: false), + car_id = table.Column(type: "INTEGER", nullable: true), + car_name = table.Column(type: "TEXT", maxLength: 128, nullable: true), + priority = table.Column(type: "INTEGER", nullable: false), + create_time = table.Column(type: "TEXT", maxLength: 40, nullable: true), + start_time = table.Column(type: "TEXT", maxLength: 40, nullable: true), + finish_time = table.Column(type: "TEXT", maxLength: 40, nullable: true), + stuck_reason = table.Column(type: "TEXT", maxLength: 512, nullable: true), + overdue = table.Column(type: "INTEGER", nullable: false), + first_seen_at = table.Column(type: "TEXT", maxLength: 40, nullable: false), + last_seen_at = table.Column(type: "TEXT", maxLength: 40, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cdm_tasks", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "vehicle_alarms", + columns: table => new + { + id = table.Column(type: "TEXT", maxLength: 36, nullable: false), + car_id = table.Column(type: "INTEGER", nullable: false), + car_name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + info = table.Column(type: "text", nullable: false), + level = table.Column(type: "INTEGER", nullable: false), + status = table.Column(type: "TEXT", maxLength: 16, nullable: false), + first_at = table.Column(type: "TEXT", maxLength: 40, nullable: false), + last_at = table.Column(type: "TEXT", maxLength: 40, nullable: false), + resolved_at = table.Column(type: "TEXT", maxLength: 40, nullable: true), + duration_secs = table.Column(type: "INTEGER", nullable: true), + acknowledged = table.Column(type: "INTEGER", nullable: false), + acknowledged_at = table.Column(type: "TEXT", maxLength: 40, nullable: true), + acknowledged_by = table.Column(type: "TEXT", maxLength: 128, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_vehicle_alarms", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "IX_cdm_tasks_create_time", + table: "cdm_tasks", + column: "create_time"); + + migrationBuilder.CreateIndex( + name: "IX_cdm_tasks_status_code", + table: "cdm_tasks", + column: "status_code"); + + migrationBuilder.CreateIndex( + name: "IX_vehicle_alarms_car_id_status", + table: "vehicle_alarms", + columns: new[] { "car_id", "status" }); + + migrationBuilder.CreateIndex( + name: "IX_vehicle_alarms_first_at", + table: "vehicle_alarms", + column: "first_at"); + + migrationBuilder.CreateIndex( + name: "IX_vehicle_alarms_status", + table: "vehicle_alarms", + column: "status"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "cdm_tasks"); + + migrationBuilder.DropTable( + name: "vehicle_alarms"); + } + } +} diff --git a/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs b/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs new file mode 100644 index 0000000..6182c9f --- /dev/null +++ b/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs @@ -0,0 +1,1879 @@ +// +using System; +using MiGu.DB.Kernel.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MiGu.DB.Migrations.Sqlite +{ + [DbContext(typeof(MiGuDbContext))] + partial class MiGuDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("MiGu.DB.Domains.Dashboard.UserDashboardShortcut", b => + { + b.Property("UserId") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Scope") + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("scope"); + + b.Property("KeysJson") + .IsRequired() + .HasColumnType("text") + .HasColumnName("keys_json"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId", "Scope"); + + b.ToTable("user_dashboard_shortcuts", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Fleet.CdmTaskRecord", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CarId") + .HasColumnType("INTEGER") + .HasColumnName("car_id"); + + b.Property("CarName") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("car_name"); + + b.Property("CreateTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("DstLabel") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("dst_label"); + + b.Property("DstSiteId") + .HasColumnType("INTEGER") + .HasColumnName("dst_site_id"); + + b.Property("FinishTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("finish_time"); + + b.Property("FirstSeenAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("first_seen_at"); + + b.Property("LastSeenAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("last_seen_at"); + + b.Property("MissionId") + .HasColumnType("INTEGER") + .HasColumnName("mission_id"); + + b.Property("MissionName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("mission_name"); + + b.Property("MissionTypeName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("mission_type"); + + b.Property("Overdue") + .HasColumnType("INTEGER") + .HasColumnName("overdue"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("SrcLabel") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("src_label"); + + b.Property("SrcSiteId") + .HasColumnType("INTEGER") + .HasColumnName("src_site_id"); + + b.Property("StartTime") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("start_time"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("status_code"); + + b.Property("StuckReason") + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("stuck_reason"); + + b.Property("TaskId") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.HasKey("Id"); + + b.HasIndex("CreateTime"); + + b.HasIndex("StatusCode"); + + b.ToTable("cdm_tasks", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Fleet.VehicleAlarmRecord", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Acknowledged") + .HasColumnType("INTEGER") + .HasColumnName("acknowledged"); + + b.Property("AcknowledgedAt") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("acknowledged_at"); + + b.Property("AcknowledgedBy") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("acknowledged_by"); + + b.Property("CarId") + .HasColumnType("INTEGER") + .HasColumnName("car_id"); + + b.Property("CarName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("car_name"); + + b.Property("DurationSecs") + .HasColumnType("INTEGER") + .HasColumnName("duration_secs"); + + b.Property("FirstAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("first_at"); + + b.Property("Info") + .IsRequired() + .HasColumnType("text") + .HasColumnName("info"); + + b.Property("LastAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("last_at"); + + b.Property("Level") + .HasColumnType("INTEGER") + .HasColumnName("level"); + + b.Property("ResolvedAt") + .HasMaxLength(40) + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.HasKey("Id"); + + b.HasIndex("FirstAt"); + + b.HasIndex("Status"); + + b.HasIndex("CarId", "Status"); + + b.ToTable("vehicle_alarms", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.SimpleFields.SimpleField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CarType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("car_type"); + + b.Property("Chinese") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("chinese"); + + b.Property("CreateTime") + .IsRequired() + .HasMaxLength(19) + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("data_type"); + + b.Property("English") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("english"); + + b.Property("FieldType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("field_type"); + + b.Property("IsDefault") + .HasColumnType("INTEGER") + .HasColumnName("is_default"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("Other") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("other"); + + b.Property("UpdateTime") + .IsRequired() + .HasMaxLength(19) + .HasColumnType("TEXT") + .HasColumnName("update_time"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("value"); + + b.HasKey("Id"); + + b.HasIndex("CarType", "FieldType", "Key") + .IsUnique(); + + b.ToTable("simple_fields", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportReservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SourceStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("TargetStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId", "Status"); + + b.HasIndex("TargetStorageId", "Status"); + + b.ToTable("wms_transport_reservations", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SourceSelectorJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetSelectorJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskOptionsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("TriggerType", "Enabled", "Priority"); + + b.ToTable("wms_transport_rules", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BusinessType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeliveryId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DispatchMissionId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DispatchStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RuleId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("TargetStorageId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("TaskPriority") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("Status"); + + b.HasIndex("TargetStorageId"); + + b.ToTable("wms_transport_tasks", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Transport.WmsTransportTaskHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SnapshotJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OperatedAt"); + + b.HasIndex("TaskId"); + + b.ToTable("wms_transport_task_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Container", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AreaId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("REAL"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Length") + .HasColumnType("REAL"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_containers", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("EnteredAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LocationCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LocationName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("LocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId") + .IsUnique(); + + b.HasIndex("StorageId"); + + b.HasIndex("LocationType", "LocationId"); + + b.ToTable("wms_container_locations", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerLocationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AfterJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("BeforeJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("FromLocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RelationId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ToLocationId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ToLocationType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_container_location_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerMaterial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("BoundAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LoadedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("MaterialId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SerialNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UnloadedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("MaterialId") + .IsUnique(); + + b.ToTable("wms_container_materials", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.ContainerMaterialHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AfterJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("BeforeJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("QuantityDelta") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RelationId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_container_material_history", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Material", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LifecycleStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Spec") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TypeCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UnboundAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Barcode"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("TypeCode"); + + b.HasIndex("LifecycleStatus", "UpdatedAt"); + + b.ToTable("wms_materials", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.MaterialType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("BarcodePrefix") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Spec") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_material_types", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.StockEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AreaCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ContainerId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ContainerName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("FromStorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("FromStorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("FromStorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialBarcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("MaterialId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("MaterialName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("MaterialTypeCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("OperatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Operator") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RefCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RefId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("RefType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("StorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("StorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ToStorageCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ToStorageId") + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("ToStorageName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("EventType"); + + b.HasIndex("MaterialId"); + + b.HasIndex("OperatedAt"); + + b.ToTable("wms_stock_events", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Storage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("AllowInbound") + .HasColumnType("INTEGER"); + + b.Property("AllowOutbound") + .HasColumnType("INTEGER"); + + b.Property("AreaId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Barcode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Capacity") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ColumnNo") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DepthNo") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LevelNo") + .HasColumnType("INTEGER"); + + b.Property("LocationKind") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SiteCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SiteId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("StorageType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ZoneCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("Barcode"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_storages", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("wms_warehouses", (string)null); + }); + + modelBuilder.Entity("MiGu.DB.Domains.Wms.WarehouseArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("DeletedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Extend") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsLock") + .HasColumnType("INTEGER") + .HasColumnName("IsLock"); + + b.Property("LayoutMode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Remark") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("WarehouseId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("WarehouseId"); + + b.ToTable("wms_areas", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MiGu.DB/README.md b/MiGu.DB/README.md new file mode 100644 index 0000000..debc756 --- /dev/null +++ b/MiGu.DB/README.md @@ -0,0 +1,56 @@ +# MiGu.DB + +迷毂平台持久化框架(EF Core 8)。由 `MiGu.Server` 引用;本工程不依赖 ASP.NET。 + +## 分层 + +| 目录 | 职责 | +|------|------| +| `Abstractions/` | 对外契约:能力接口、仓储/UoW、Provider、Module、Actor、异常 | +| `Kernel/` | 框架实现:实体基类族、MiGuDbContext、约定/Interceptor、仓储、Hosting | +| `Domains/` | 业务实体、枚举、辅助类与 `IEntityTypeConfiguration` | +| `Migrations/Sqlite/` | Schema 版本化来源(发版用;开发可用 EnsureCreated 不跑迁移) | + +## 使用 + +由 `MiGu.Server` 调用: + +```csharp +builder.Services.AddPlatformPersistence(builder.Configuration); // 内部 AddMiGuDb +await app.Services.MigrateMiGuDbAsync(); +``` + +**启动流程、SchemaMode、开发/发版配置说明见 [MiGu.Server/README.md · 数据库启动流程](../MiGu.Server/README.md#数据库启动流程)。** + +配置键(`Database:*` / `ConnectionStrings:Platform`)由 Server 的 `appsettings*.json` 提供;本库通过 `AddMiGuDb(IConfiguration)` 读取。 + +## 实体与枚举 + +- 业务状态字段为 **enum**,约定自动 `HasConversion()`(严格 1:1,不做读侧归一)。 +- `Version` 为乐观并发令牌;软删走全局 `HasQueryFilter`。 +- 复数辅助类(`StorageStatuses`、`LocationKinds`…)负责 API 字符串解析与集合校验。 + +## 扩展 + +- **新 Provider**:实现 `IDbProviderSetup` + `Migrations/{Name}/` +- **新领域**:`Domains/Xxx` 实体 + Configuration;可选 `IEntityModule` +- **数据修补**:实现 `IDataMigrator`(优先 LINQ;枚举旧值刷库等特例可定点 raw UPDATE) + +## 生成 Migration(Sqlite) + +完整步骤、命令说明与排错见 **[MIGRATIONS.md](./MIGRATIONS.md)**。 + +快速命令: + +```powershell +dotnet ef migrations add ` + --project .\MiGu.DB\MiGu.DB.csproj ` + --startup-project .\MiGu.Server\MiGu.Server.csproj ` + --output-dir Migrations/Sqlite ` + --namespace MiGu.DB.Migrations.Sqlite ` + --context MiGuDbContext +``` + +生成后请确认 `MiGuDbContextModelSnapshot.cs` 落在 `Migrations/Sqlite/`。 + +开发期使用 `EnsureCreated` 时仍可保留/继续提交 Migration 文件,启动不会应用它们,直到 `SchemaMode` 改回 `Migrate`(见 Server README)。 diff --git a/MiGu.DB/build-out.txt b/MiGu.DB/build-out.txt new file mode 100644 index 0000000..b2ca9cc Binary files /dev/null and b/MiGu.DB/build-out.txt differ diff --git a/MiGu.Server.sln b/MiGu.Server.sln index b37b3eb..3c2a734 100644 --- a/MiGu.Server.sln +++ b/MiGu.Server.sln @@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiGu.Server", "MiGu.Server\MiGu.Server.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiGu.DB", "MiGu.DB\MiGu.DB.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -13,5 +15,9 @@ Global {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/MiGu.Server/Controllers/FleetController.cs b/MiGu.Server/Controllers/FleetController.cs index cea75a9..1ada520 100644 --- a/MiGu.Server/Controllers/FleetController.cs +++ b/MiGu.Server/Controllers/FleetController.cs @@ -15,9 +15,9 @@ public sealed class FleetController : ControllerBase private readonly FleetHealthService _health; private readonly CdmTaskSyncer _cdmSyncer; private readonly AlarmCollector _alarmCollector; - private readonly PlatformDbContext _db; + private readonly MiGuDbContext _db; - public FleetController(FleetHealthService health, CdmTaskSyncer cdmSyncer, AlarmCollector alarmCollector, PlatformDbContext db) + public FleetController(FleetHealthService health, CdmTaskSyncer cdmSyncer, AlarmCollector alarmCollector, MiGuDbContext db) { _health = health; _cdmSyncer = cdmSyncer; diff --git a/MiGu.Server/Controllers/OtaController.cs b/MiGu.Server/Controllers/OtaController.cs index f2f4185..04924bb 100644 --- a/MiGu.Server/Controllers/OtaController.cs +++ b/MiGu.Server/Controllers/OtaController.cs @@ -48,7 +48,10 @@ public class OtaController : ControllerBase if (string.Equals(Scope, "Platform", StringComparison.OrdinalIgnoreCase)) return true; var ops = User.FindFirst("ops")?.Value ?? ""; var set = ops.Split(' ', StringSplitOptions.RemoveEmptyEntries); - return set.Contains("*") || set.Any(o => o.StartsWith("ops.ota", StringComparison.OrdinalIgnoreCase)); + return set.Contains("*") + || set.Contains("ops.ota") + || set.Contains("ops.ota.write") + || set.Any(o => o.StartsWith("ops.ota.", StringComparison.OrdinalIgnoreCase)); } private bool DenyWrite(out ActionResult denied) @@ -147,12 +150,32 @@ public class OtaController : ControllerBase var receiveBase = $"{baseUrl.TrimEnd('/')}/api/ota/receive"; var time = DateTime.Now.ToString("yyyyMMddHHmmss"); await _wd.TriggerPullAsync(car.Ip, receiveBase, time, ct); - // 等待文件落盘 + // 等待文件落盘:绝对超时 + 收到文件后的空闲窗口,避免首个组件到达就清会话导致半包 await Task.Delay(1500, ct); - for (var i = 0; i < 40; i++) + var deadline = DateTime.UtcNow.AddSeconds(90); + var idleAfterReceive = TimeSpan.FromSeconds(8); + var lastCount = 0; + DateTime? lastProgressAt = null; + while (DateTime.UtcNow < deadline) { + ct.ThrowIfCancellationRequested(); var info = _store.ScanPackage(pkgId); - if (info.Components.Count > 0) break; + if (info.Components.Count > lastCount) + { + lastCount = info.Components.Count; + lastProgressAt = DateTime.UtcNow; + } + else if (_store.TryGetLastReceiveAt(car.Ip, out var recvAt) && + (lastProgressAt == null || recvAt > lastProgressAt.Value)) + { + lastProgressAt = recvAt; + } + + if (lastCount > 0 && + lastProgressAt != null && + DateTime.UtcNow - lastProgressAt.Value >= idleAfterReceive) + break; + await Task.Delay(500, ct); } _store.ClearActivePull(car.Ip); diff --git a/MiGu.Server/Controllers/OtaReceiveController.cs b/MiGu.Server/Controllers/OtaReceiveController.cs index 17a1d43..79dab2c 100644 --- a/MiGu.Server/Controllers/OtaReceiveController.cs +++ b/MiGu.Server/Controllers/OtaReceiveController.cs @@ -8,11 +8,14 @@ namespace MiGu.Server.Controllers; /// WatchDog 回传包接收端。 /// WatchDog 写死 POST 到 http://{config.serverIP}:8000/upload-mdcs/{routeKey}, /// 必须与参考 Electron Express :8000 路径一致;/api/ota/receive/* 仅作兼容别名。 +/// 会话校验使用 TCP 对端 IP(见 Program 中 TcpRemoteIp),忽略可伪造的 X-Forwarded-For。 /// [ApiController] [AllowAnonymous] public class OtaReceiveController : ControllerBase { + public const string TcpRemoteIpItemKey = "TcpRemoteIp"; + private readonly OtaStore _store; private readonly ILogger _log; @@ -40,15 +43,15 @@ public class OtaReceiveController : ControllerBase [RequestSizeLimit(512_000_000)] public async Task UploadHistory(string routeKey, CancellationToken ct) { - var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + var ip = ResolveTcpRemoteIp(); if (!_store.TryGetActivePullId(ip, out _)) { - _log.LogWarning("OTA history rejected without active pull session from {Ip}", ip); + _log.LogWarning("OTA history rejected without active pull session from {Ip}", ip ?? "unknown"); return BadRequest("no active pull session"); } var day = DateTime.Now.ToString("yyyy-MM-dd"); - var dir = Path.Combine(_store.HistoryDir, day, SafeFileName(ip, "unknown")); + var dir = Path.Combine(_store.HistoryDir, day, SafeFileName(ip ?? "unknown", "unknown")); Directory.CreateDirectory(dir); var file = await ReadFirstFileAsync(ct); if (file == null || file.Length == 0) return BadRequest("empty"); @@ -58,6 +61,7 @@ public class OtaReceiveController : ControllerBase var path = Path.Combine(dir, safeName); await using var fs = System.IO.File.Create(path); await file.CopyToAsync(fs, ct); + _store.NotePullReceive(ip); _log.LogInformation("OTA history receive {Route} -> {Path} ({Len})", routeKey, path, file.Length); return Ok(new { ok = true }); } @@ -66,7 +70,7 @@ public class OtaReceiveController : ControllerBase { try { - var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString(); + var clientIp = ResolveTcpRemoteIp(); if (!_store.TryGetActivePullId(clientIp, out _)) { _log.LogWarning("OTA mdcs rejected without active pull session from {Ip}", clientIp ?? "unknown"); @@ -80,8 +84,9 @@ public class OtaReceiveController : ControllerBase Directory.CreateDirectory(Path.GetDirectoryName(dest)!); await using (var fs = System.IO.File.Create(dest)) await file.CopyToAsync(fs, ct); + _store.NotePullReceive(clientIp); _log.LogInformation("OTA mdcs receive {Route} -> {Dest} ({Len})", routeKey, dest, file.Length); - return Ok(new { ok = true, path = dest }); + return Ok(new { ok = true }); } catch (Exception ex) { @@ -90,6 +95,14 @@ public class OtaReceiveController : ControllerBase } } + /// 优先取 ForwardedHeaders 之前写入的 TCP 对端 IP,避免 X-Forwarded-For 投毒。 + private string? ResolveTcpRemoteIp() + { + if (HttpContext.Items.TryGetValue(TcpRemoteIpItemKey, out var boxed) && boxed is string s && !string.IsNullOrWhiteSpace(s)) + return s; + return HttpContext.Connection.RemoteIpAddress?.ToString(); + } + private async Task ReadFirstFileAsync(CancellationToken ct) { if (!Request.HasFormContentType) return null; diff --git a/MiGu.Server/Controllers/RbacController.cs b/MiGu.Server/Controllers/RbacController.cs index 09f864b..4ac4ac7 100644 --- a/MiGu.Server/Controllers/RbacController.cs +++ b/MiGu.Server/Controllers/RbacController.cs @@ -33,6 +33,8 @@ public class RbacController : ControllerBase new("ops.task.cancel", "任务 · 取消"), new("ops.task.reassign", "任务 · 改派"), new("ops.task.boostPriority", "任务 · 提升优先级"), + new("ops.ota", "OTA · 运维读写"), + new("ops.ota.write", "OTA · 写操作"), new("monitor.note.write", "监控 · 写运营备注"), new("auth.manage", "系统 · 权限与角色管理"), }; diff --git a/MiGu.Server/Controllers/WmsController.cs b/MiGu.Server/Controllers/WmsController.cs index 7c02199..feccc1a 100644 --- a/MiGu.Server/Controllers/WmsController.cs +++ b/MiGu.Server/Controllers/WmsController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using MiGu.DB.Abstractions.Exceptions; using MiGu.Server.Wms; namespace MiGu.Server.Controllers; @@ -191,10 +192,6 @@ public sealed class WmsController : ControllerBase public Task> ContainerMaterials([FromQuery] string? q, [FromQuery] Guid? containerId) => _service.ContainerMaterials(q, containerId); - [HttpPost("container-materials")] - public async Task SaveContainerMaterial([FromBody] BindMaterialRequest req) => - Ok(await _service.BindMaterial(req, User.ActorName())); - [HttpPost("container-materials/bind")] public async Task BindMaterial([FromBody] BindMaterialRequest req) => Ok(await _service.BindMaterial(req, User.ActorName())); @@ -276,15 +273,31 @@ 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 }); + switch (context.Exception) + { + case ConcurrencyConflictException ex: + context.Result = new ObjectResult(new { message = ex.Message }) { StatusCode = StatusCodes.Status409Conflict }; + break; + case EntityLockedException ex: + context.Result = new ObjectResult(new { message = ex.Message }) { StatusCode = StatusCodes.Status409Conflict }; + break; + case EntityNotFoundException ex: + context.Result = new NotFoundObjectResult(new { message = ex.Message }); + break; + case InvalidOperationException ex: + context.Result = new BadRequestObjectResult(new { message = ex.Message }); + break; + default: + return; + } + context.ExceptionHandled = true; } } public sealed class TransportRequestQuery { - public string TriggerType { get; set; } = WmsTransportTriggerTypes.MaterialCall; + public string TriggerType { get; set; } = nameof(WmsTransportTriggerType.MaterialCall); public Guid? RuleId { get; set; } public string? RequestSiteId { get; set; } public Guid? MaterialId { get; set; } diff --git a/MiGu.Server/Dashboard/DashboardShortcutService.cs b/MiGu.Server/Dashboard/DashboardShortcutService.cs index 7e3f668..9343b1e 100644 --- a/MiGu.Server/Dashboard/DashboardShortcutService.cs +++ b/MiGu.Server/Dashboard/DashboardShortcutService.cs @@ -7,7 +7,7 @@ namespace MiGu.Server.Dashboard; public sealed class DashboardShortcutService { - private readonly PlatformDbContext _db; + private readonly MiGuDbContext _db; private readonly RbacStore _rbac; private static readonly JsonSerializerOptions JsonOpts = new() @@ -15,7 +15,7 @@ public sealed class DashboardShortcutService PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - public DashboardShortcutService(PlatformDbContext db, RbacStore rbac) + public DashboardShortcutService(MiGuDbContext db, RbacStore rbac) { _db = db; _rbac = rbac; diff --git a/MiGu.Server/Fleet/AlarmCollector.cs b/MiGu.Server/Fleet/AlarmCollector.cs index 6d527fa..6defd45 100644 --- a/MiGu.Server/Fleet/AlarmCollector.cs +++ b/MiGu.Server/Fleet/AlarmCollector.cs @@ -76,7 +76,7 @@ public sealed class AlarmCollector try { using var scope = _scopeFactory.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); await ReconcileAsync(db, current, ct); } catch (Exception ex) @@ -161,36 +161,43 @@ public sealed class AlarmCollector } } - private static async Task ReconcileAsync(PlatformDbContext db, Dictionary current, CancellationToken ct) + private static async Task ReconcileAsync(MiGuDbContext db, Dictionary current, CancellationToken ct) { var now = DateTimeOffset.UtcNow; var active = await db.VehicleAlarms.Where(a => a.Status == "active").ToListAsync(ct); var activeByCar = new Dictionary(); foreach (var a in active) activeByCar[a.CarId] = a; // 每车取一条 active - // 出现 / 更新 + // 出现 / 更新:文案或级别变化时先 clear 旧记录再开新 active,保留分段历史 foreach (var cur in current.Values) { if (activeByCar.TryGetValue(cur.CarId, out var rec)) { - rec.Info = cur.Info; - rec.Level = cur.Level; - rec.CarName = cur.CarName; - rec.LastAt = now; - } - else - { - db.VehicleAlarms.Add(new VehicleAlarmRecord + var same = + string.Equals(rec.Info, cur.Info, StringComparison.Ordinal) && + rec.Level == cur.Level; + if (same) { - CarId = cur.CarId, - CarName = cur.CarName, - Info = cur.Info, - Level = cur.Level, - Status = "active", - FirstAt = now, - LastAt = now - }); + rec.CarName = cur.CarName; + rec.LastAt = now; + continue; + } + + rec.Status = "cleared"; + rec.ResolvedAt = now; + rec.DurationSecs = (long)Math.Max(0, (now - rec.FirstAt).TotalSeconds); } + + db.VehicleAlarms.Add(new VehicleAlarmRecord + { + CarId = cur.CarId, + CarName = cur.CarName, + Info = cur.Info, + Level = cur.Level, + Status = "active", + FirstAt = now, + LastAt = now + }); } // 消失 → 恢复 diff --git a/MiGu.Server/Fleet/CdmTaskSync.cs b/MiGu.Server/Fleet/CdmTaskSync.cs index 1609426..7c7342a 100644 --- a/MiGu.Server/Fleet/CdmTaskSync.cs +++ b/MiGu.Server/Fleet/CdmTaskSync.cs @@ -80,7 +80,7 @@ public sealed class CdmTaskSyncer try { using var scope = _scopeFactory.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); await UpsertAsync(db, dtos, ct); } catch (Exception ex) @@ -126,7 +126,7 @@ public sealed class CdmTaskSyncer } } - private static async Task UpsertAsync(PlatformDbContext db, IReadOnlyList dtos, CancellationToken ct) + private static async Task UpsertAsync(MiGuDbContext db, IReadOnlyList dtos, CancellationToken ct) { var valid = dtos.Where(d => !string.IsNullOrWhiteSpace(d.id)).ToList(); if (valid.Count == 0) return; diff --git a/MiGu.Server/GlobalUsings.Db.cs b/MiGu.Server/GlobalUsings.Db.cs new file mode 100644 index 0000000..c3811e3 --- /dev/null +++ b/MiGu.Server/GlobalUsings.Db.cs @@ -0,0 +1,21 @@ +global using MiGu.DB.Kernel.Context; +global using MiGu.DB.Kernel.Entities; +global using SimpleField = MiGu.DB.Domains.SimpleFields.SimpleField; +global using UserDashboardShortcut = MiGu.DB.Domains.Dashboard.UserDashboardShortcut; +global using Warehouse = MiGu.DB.Domains.Wms.Warehouse; +global using WarehouseArea = MiGu.DB.Domains.Wms.WarehouseArea; +global using Storage = MiGu.DB.Domains.Wms.Storage; +global using Container = MiGu.DB.Domains.Wms.Container; +global using MaterialType = MiGu.DB.Domains.Wms.MaterialType; +global using Material = MiGu.DB.Domains.Wms.Material; +global using ContainerLocation = MiGu.DB.Domains.Wms.ContainerLocation; +global using ContainerMaterial = MiGu.DB.Domains.Wms.ContainerMaterial; +global using StockEvent = MiGu.DB.Domains.Wms.StockEvent; +global using ContainerLocationHistory = MiGu.DB.Domains.Wms.ContainerLocationHistory; +global using ContainerMaterialHistory = MiGu.DB.Domains.Wms.ContainerMaterialHistory; +global using WmsTransportRule = MiGu.DB.Domains.Transport.WmsTransportRule; +global using WmsTransportTask = MiGu.DB.Domains.Transport.WmsTransportTask; +global using WmsTransportReservation = MiGu.DB.Domains.Transport.WmsTransportReservation; +global using WmsTransportTaskHistory = MiGu.DB.Domains.Transport.WmsTransportTaskHistory; +global using CdmTaskRecord = MiGu.DB.Domains.Fleet.CdmTaskRecord; +global using VehicleAlarmRecord = MiGu.DB.Domains.Fleet.VehicleAlarmRecord; diff --git a/MiGu.Server/MiGu.Server.csproj b/MiGu.Server/MiGu.Server.csproj index 5a3e57d..8b27a3a 100644 --- a/MiGu.Server/MiGu.Server.csproj +++ b/MiGu.Server/MiGu.Server.csproj @@ -16,11 +16,14 @@ - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/MiGu.Server/Ota/OtaJobRunner.cs b/MiGu.Server/Ota/OtaJobRunner.cs index 32fbffd..e8b0fba 100644 --- a/MiGu.Server/Ota/OtaJobRunner.cs +++ b/MiGu.Server/Ota/OtaJobRunner.cs @@ -16,6 +16,35 @@ public sealed class OtaJobRunner _wd = wd; _vehicles = vehicles; _log = log; + RecoverInterruptedJobs(); + } + + /// 进程重启后把落盘中非终态 job 标为 failed,避免 UI 永久显示 running。 + private void RecoverInterruptedJobs() + { + try + { + foreach (var job in _store.ListJobs(500)) + { + if (job.Status is not ("running" or "pending")) continue; + job.Status = "failed"; + job.Message = string.IsNullOrWhiteSpace(job.Message) + ? "进程重启,任务中断" + : job.Message; + job.FinishedAt = DateTimeOffset.UtcNow; + foreach (var step in job.Steps.Where(s => s.Status is "pending" or "running")) + { + step.Status = "failed"; + step.Error ??= "进程重启,任务中断"; + } + _store.SaveJob(job); + _log.LogWarning("OTA job {Id} marked failed after process restart", job.Id); + } + } + catch (Exception ex) + { + _log.LogWarning(ex, "OTA interrupted job recovery failed"); + } } public OtaJob EnqueueSync(CreateSyncJobRequest req, string? user) diff --git a/MiGu.Server/Ota/OtaModels.cs b/MiGu.Server/Ota/OtaModels.cs index 24fdc4f..02738f0 100644 --- a/MiGu.Server/Ota/OtaModels.cs +++ b/MiGu.Server/Ota/OtaModels.cs @@ -6,9 +6,11 @@ public sealed class OtaSettings public int MaxCar { get; set; } = 2; public bool LatencyEnabled { get; set; } public int RttThresholdMs { get; set; } = 200; - /// skip | confirm + /// skip | allow(历史值 confirm 视为 allow) public string OverThreshold { get; set; } = "skip"; + /// 保留字段:备份尚未实现,仅反序列化兼容。 public int BackupPeriodMinutes { get; set; } = 60; + /// 保留字段:备份尚未实现,仅反序列化兼容。 public bool BackupExe { get; set; } public string? NewVersionName { get; set; } } diff --git a/MiGu.Server/Ota/OtaStore.cs b/MiGu.Server/Ota/OtaStore.cs index 55b22eb..d891ee3 100644 --- a/MiGu.Server/Ota/OtaStore.cs +++ b/MiGu.Server/Ota/OtaStore.cs @@ -24,6 +24,8 @@ public sealed class OtaStore private string? _lastPullId; private readonly System.Collections.Concurrent.ConcurrentDictionary _pullByIp = new(StringComparer.OrdinalIgnoreCase); + private readonly System.Collections.Concurrent.ConcurrentDictionary _pullLastReceiveUtc = + new(StringComparer.OrdinalIgnoreCase); private long _jobSeq; public OtaStore(IWebHostEnvironment env, IOptions options, ILogger log) @@ -144,29 +146,44 @@ public sealed class OtaStore lock (_gate) { var ip = NormalizeIp(sourceIp); + // 必须匹配会话 IP;禁止无 IP 时回退到最近一次拉包(可被伪造/误写)。 if (ip != null && _pullByIp.TryGetValue(ip, out var byIp)) { id = byIp; return true; } - if (ip == null && _lastPullId != null) - { - id = _lastPullId; - return true; - } - id = null; return false; } } + public void NotePullReceive(string? sourceIp) + { + var ip = NormalizeIp(sourceIp); + if (ip == null) return; + _pullLastReceiveUtc[ip] = DateTime.UtcNow; + } + + public bool TryGetLastReceiveAt(string? sourceIp, out DateTime utc) + { + var ip = NormalizeIp(sourceIp); + if (ip != null && _pullLastReceiveUtc.TryGetValue(ip, out utc)) + return true; + utc = default; + return false; + } + public void ClearActivePull(string? sourceIp = null) { lock (_gate) { var ip = NormalizeIp(sourceIp); - if (ip != null) _pullByIp.TryRemove(ip, out _); + if (ip != null) + { + _pullByIp.TryRemove(ip, out _); + _pullLastReceiveUtc.TryRemove(ip, out _); + } if (_pullByIp.IsEmpty) _lastPullId = null; } } diff --git a/MiGu.Server/Persistence/EntityBase.cs b/MiGu.Server/Persistence/EntityBase.cs deleted file mode 100644 index 8d5e51e..0000000 --- a/MiGu.Server/Persistence/EntityBase.cs +++ /dev/null @@ -1,17 +0,0 @@ -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; } = "{}"; -} diff --git a/MiGu.Server/Persistence/HttpActorContextMiddleware.cs b/MiGu.Server/Persistence/HttpActorContextMiddleware.cs new file mode 100644 index 0000000..422ea78 --- /dev/null +++ b/MiGu.Server/Persistence/HttpActorContextMiddleware.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using MiGu.DB.Abstractions.Runtime; +using MiGu.DB.Kernel.Conventions; + +namespace MiGu.Server.Persistence; + +/// 从 JWT Claims 解析当前操作者,写入 IActorContextAccessor。 +public sealed class HttpActorContextMiddleware +{ + private readonly RequestDelegate _next; + + public HttpActorContextMiddleware(RequestDelegate next) => _next = next; + + public async Task InvokeAsync(HttpContext http, IActorContextAccessor actors) + { + var name = http.User?.Identity?.Name + ?? http.User?.FindFirst("sub")?.Value + ?? http.User?.FindFirst("unique_name")?.Value + ?? "system"; + actors.Current = new HttpActorContext(name); + try + { + await _next(http); + } + finally + { + actors.Current = SystemActorContext.Instance; + } + } + + private sealed class HttpActorContext : IActorContext + { + public HttpActorContext(string name) => Name = name; + public string Name { get; } + } +} diff --git a/MiGu.Server/Persistence/PlatformDbContext.cs b/MiGu.Server/Persistence/PlatformDbContext.cs deleted file mode 100644 index 1e2d345..0000000 --- a/MiGu.Server/Persistence/PlatformDbContext.cs +++ /dev/null @@ -1,316 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using MiGu.Server.Dashboard; -using MiGu.Server.Wms; -using MiGu.Server.SimpleFields; - -namespace MiGu.Server.Persistence; - -public sealed class PlatformDbContext : DbContext -{ - public PlatformDbContext(DbContextOptions options) : base(options) { } - - public DbSet Warehouses => Set(); - public DbSet WarehouseAreas => Set(); - public DbSet Storages => Set(); - public DbSet Containers => Set(); - public DbSet MaterialTypes => Set(); - public DbSet Materials => Set(); - public DbSet ContainerLocations => Set(); - public DbSet ContainerMaterials => Set(); - public DbSet StockEvents => Set(); - public DbSet ContainerLocationHistories => Set(); - public DbSet ContainerMaterialHistories => Set(); - public DbSet WmsTransportRules => Set(); - public DbSet WmsTransportTasks => Set(); - public DbSet WmsTransportReservations => Set(); - public DbSet WmsTransportTaskHistories => Set(); - public DbSet SimpleFields => Set(); - public DbSet UserDashboardShortcuts => Set(); - public DbSet CdmTasks => Set(); - public DbSet VehicleAlarms => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - var guid = new ValueConverter( - v => v.ToString("D"), - v => Guid.Parse(v)); - var nullableGuid = new ValueConverter( - v => v.HasValue ? v.Value.ToString("D") : null, - v => string.IsNullOrWhiteSpace(v) ? null : Guid.Parse(v)); - var dateTimeOffset = new ValueConverter( - v => v.UtcDateTime.ToString("O"), - v => DateTimeOffset.Parse(v)); - var nullableDateTimeOffset = new ValueConverter( - v => v.HasValue ? v.Value.UtcDateTime.ToString("O") : null, - v => string.IsNullOrWhiteSpace(v) ? null : DateTimeOffset.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); - foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset))) - modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(dateTimeOffset).HasMaxLength(40); - foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset?))) - modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(nullableDateTimeOffset).HasMaxLength(40); - } - - ConfigureEntityBase(modelBuilder, "wms_warehouses"); - ConfigureEntityBase(modelBuilder, "wms_areas"); - ConfigureEntityBase(modelBuilder, "wms_storages"); - ConfigureEntityBase(modelBuilder, "wms_containers"); - ConfigureEntityBase(modelBuilder, "wms_material_types"); - ConfigureEntityBase(modelBuilder, "wms_materials"); - ConfigureEntityBase(modelBuilder, "wms_container_locations"); - ConfigureEntityBase(modelBuilder, "wms_container_materials"); - ConfigureEntityBase(modelBuilder, "wms_transport_rules"); - ConfigureEntityBase(modelBuilder, "wms_transport_tasks"); - ConfigureEntityBase(modelBuilder, "wms_transport_reservations"); - - ConfigureHistory(modelBuilder, "wms_container_location_history"); - ConfigureHistory(modelBuilder, "wms_container_material_history"); - ConfigureStockEvent(modelBuilder); - ConfigureTransportTaskHistory(modelBuilder); - - modelBuilder.Entity().HasIndex(x => x.Code).IsUnique(); - modelBuilder.Entity().HasIndex(x => x.Code).IsUnique(); - modelBuilder.Entity().HasIndex(x => x.WarehouseId); - modelBuilder.Entity().HasIndex(x => x.Code).IsUnique(); - modelBuilder.Entity().HasIndex(x => x.AreaId); - modelBuilder.Entity().HasIndex(x => x.Barcode); - modelBuilder.Entity().HasIndex(x => x.Code).IsUnique(); - modelBuilder.Entity().HasIndex(x => x.Code).IsUnique(); - modelBuilder.Entity().HasIndex(x => x.Code).IsUnique(); - modelBuilder.Entity().HasIndex(x => x.Barcode); - modelBuilder.Entity().HasIndex(x => new { x.LifecycleStatus, x.UpdatedAt }); - modelBuilder.Entity().HasIndex(x => x.TypeCode); - modelBuilder.Entity().HasIndex(x => x.ContainerId).IsUnique(); - // 库位占用 1:1:同一 Storage LocationId 同时只能有一条未删除记录(Car 等其它类型不限) - modelBuilder.Entity() - .HasIndex(x => x.LocationId) - .IsUnique() - .HasFilter("LocationType = 'Storage' AND IsDeleted = 0") - .HasDatabaseName("IX_wms_container_locations_StorageLocationId"); - modelBuilder.Entity().HasIndex(x => new { x.LocationType, x.LocationId }); - modelBuilder.Entity().HasIndex(x => x.MaterialId).IsUnique(); - modelBuilder.Entity().HasIndex(x => x.ContainerId); - - modelBuilder.Entity().HasIndex(x => x.Code).IsUnique(); - modelBuilder.Entity().HasIndex(x => new { x.TriggerType, x.Enabled, x.Priority }); - modelBuilder.Entity().HasIndex(x => x.Status); - modelBuilder.Entity().HasIndex(x => x.ContainerId); - modelBuilder.Entity().HasIndex(x => x.TargetStorageId); - modelBuilder.Entity().HasIndex(x => new { x.ContainerId, x.Status }); - modelBuilder.Entity().HasIndex(x => new { x.TargetStorageId, x.Status }); - modelBuilder.Entity().Property(x => x.Quantity).HasPrecision(18, 4); - - modelBuilder.Entity().Property(x => x.Quantity).HasPrecision(18, 4); - modelBuilder.Entity().Property(x => x.QuantityDelta).HasPrecision(18, 4); - - ConfigureSimpleField(modelBuilder); - ConfigureUserDashboardShortcut(modelBuilder); - ConfigureCdmTask(modelBuilder); - ConfigureVehicleAlarm(modelBuilder); - } - - private static void ConfigureVehicleAlarm(ModelBuilder modelBuilder) - { - var e = modelBuilder.Entity(); - 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); - } - - private static void ConfigureCdmTask(ModelBuilder modelBuilder) - { - var e = modelBuilder.Entity(); - 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); - } - - private static void ConfigureUserDashboardShortcut(ModelBuilder modelBuilder) - { - var e = modelBuilder.Entity(); - e.ToTable("user_dashboard_shortcuts"); - e.HasKey(x => new { x.UserId, x.Scope }); - e.Property(x => x.UserId).HasColumnName("user_id").HasMaxLength(64); - e.Property(x => x.Scope).HasColumnName("scope").HasMaxLength(32); - e.Property(x => x.KeysJson).HasColumnName("keys_json").HasColumnType("text"); - var dateTime = new ValueConverter( - v => v.UtcDateTime.ToString("O"), - v => DateTimeOffset.Parse(v)); - e.Property(x => x.UpdatedAt).HasColumnName("updated_at").HasConversion(dateTime).HasMaxLength(40); - } - - private static void ConfigureSimpleField(ModelBuilder modelBuilder) - { - var e = modelBuilder.Entity(); - e.ToTable("simple_fields"); - e.HasKey(x => x.Id); - e.Property(x => x.Id).HasColumnName("id"); - e.Property(x => x.CarType).HasColumnName("car_type").HasMaxLength(64); - e.Property(x => x.FieldType).HasColumnName("field_type").HasMaxLength(64); - e.Property(x => x.Key).HasColumnName("key").HasMaxLength(128); - e.Property(x => x.Value).HasColumnName("value"); - e.Property(x => x.DataType).HasColumnName("data_type").HasMaxLength(128); - e.Property(x => x.Chinese).HasColumnName("chinese").HasMaxLength(256).IsRequired(false); - e.Property(x => x.English).HasColumnName("english").HasMaxLength(256).IsRequired(false); - e.Property(x => x.Other).HasColumnName("other").HasMaxLength(512); - e.Property(x => x.IsDefault).HasColumnName("is_default"); - var dateTime = new ValueConverter( - v => SimpleFieldDateTime.ToStorage(v), - v => SimpleFieldDateTime.FromStorage(v)); - e.Property(x => x.CreateTime).HasColumnName("create_time").HasConversion(dateTime).HasMaxLength(19); - e.Property(x => x.UpdateTime).HasColumnName("update_time").HasConversion(dateTime).HasMaxLength(19); - e.HasIndex(x => new { x.CarType, x.FieldType, x.Key }).IsUnique(); - } - - public override int SaveChanges(bool acceptAllChangesOnSuccess) - { - StampEntities(); - return base.SaveChanges(acceptAllChangesOnSuccess); - } - - public override Task 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()) - { - 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(ModelBuilder modelBuilder, string table) where T : EntityBase - { - var e = modelBuilder.Entity(); - 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(ModelBuilder modelBuilder, string table) where T : WarehouseHistoryBase - { - var e = modelBuilder.Entity(); - 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); - } - - private static void ConfigureStockEvent(ModelBuilder modelBuilder) - { - var e = modelBuilder.Entity(); - e.ToTable("wms_stock_events"); - e.HasKey(x => x.Id); - e.Property(x => x.EventType).HasMaxLength(32); - e.Property(x => x.MaterialCode).HasMaxLength(64); - e.Property(x => x.MaterialName).HasMaxLength(128); - e.Property(x => x.MaterialBarcode).HasMaxLength(128); - e.Property(x => x.MaterialTypeCode).HasMaxLength(64); - e.Property(x => x.ContainerCode).HasMaxLength(64); - e.Property(x => x.ContainerName).HasMaxLength(128); - e.Property(x => x.StorageCode).HasMaxLength(64); - e.Property(x => x.StorageName).HasMaxLength(128); - e.Property(x => x.AreaCode).HasMaxLength(64); - e.Property(x => x.FromStorageCode).HasMaxLength(64); - e.Property(x => x.FromStorageName).HasMaxLength(128); - e.Property(x => x.ToStorageCode).HasMaxLength(64); - e.Property(x => x.ToStorageName).HasMaxLength(128); - e.Property(x => x.RefType).HasMaxLength(64); - e.Property(x => x.RefCode).HasMaxLength(64); - e.Property(x => x.Operator).HasMaxLength(128); - e.Property(x => x.Reason).HasMaxLength(500); - e.HasIndex(x => x.OperatedAt); - e.HasIndex(x => x.EventType); - e.HasIndex(x => x.MaterialId); - e.HasIndex(x => x.ContainerId); - } - - private static void ConfigureTransportTaskHistory(ModelBuilder modelBuilder) - { - var e = modelBuilder.Entity(); - e.ToTable("wms_transport_task_history"); - e.HasKey(x => x.Id); - e.Property(x => x.FromStatus).HasMaxLength(32); - e.Property(x => x.ToStatus).HasMaxLength(32); - e.Property(x => x.Operator).HasMaxLength(128); - e.Property(x => x.Reason).HasMaxLength(500); - e.Property(x => x.ErrorMessage).HasMaxLength(1000); - e.Property(x => x.SnapshotJson).HasColumnType("text"); - e.HasIndex(x => x.TaskId); - e.HasIndex(x => x.OperatedAt); - } -} diff --git a/MiGu.Server/Persistence/PlatformPersistence.cs b/MiGu.Server/Persistence/PlatformPersistence.cs index 774c2f7..a176778 100644 --- a/MiGu.Server/Persistence/PlatformPersistence.cs +++ b/MiGu.Server/Persistence/PlatformPersistence.cs @@ -1,566 +1,24 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Data.Sqlite; -using MiGu.Server.Wms; -using MiGu.Server.SimpleFields; +using MiGu.DB.Kernel.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace MiGu.Server.Persistence; +/// 兼容入口:转调 MiGu.DB Hosting,并注册本进程业务服务。 public static class PlatformPersistence { public static IServiceCollection AddPlatformPersistence(this IServiceCollection services, IConfiguration configuration) { - services.AddDbContext((sp, options) => - { - var env = sp.GetRequiredService(); - var provider = configuration["Database:Provider"] ?? "sqlite"; - var connection = ResolveConnectionString(configuration, env, provider); + services.AddMiGuDb(configuration); - 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(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } - - public static async Task EnsurePlatformDatabaseAsync(this IServiceProvider services) - { - using var scope = services.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - await db.Database.EnsureCreatedAsync(); - // EnsureCreated 只在「库文件不存在」时建表;已有 platform.db 时新增实体不会自动补表。 - await EnsureSimpleFieldsTableAsync(db); - await EnsureUserDashboardShortcutsTableAsync(db); - await EnsureWmsTransportSchemaAsync(db); - await EnsureWmsStructureSchemaAsync(db); - await EnsureCdmTasksTableAsync(db); - await EnsureVehicleAlarmsTableAsync(db); - await MigrateWmsLegacyAsync(scope.ServiceProvider); - } - - private static async Task MigrateWmsLegacyAsync(IServiceProvider sp) - { - try - { - var wms = sp.GetRequiredService(); - await wms.MigrateLegacyAsync("system"); - } - catch - { - // 首次建库或缺列时忽略,后续请求可再触发 - } - } - - /// 补建仓库/物料类型/库存事件及结构扩展列(幂等,SQLite)。 - private static async Task EnsureWmsStructureSchemaAsync(PlatformDbContext db) - { - if (!db.Database.IsSqlite()) return; - - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS wms_warehouses ( - Id TEXT NOT NULL CONSTRAINT PK_wms_warehouses PRIMARY KEY, - Code TEXT NOT NULL, - Name TEXT NOT NULL, - Type TEXT NOT NULL, - Enabled INTEGER NOT NULL, - SortOrder INTEGER NOT NULL, - CreatedAt TEXT NOT NULL, - UpdatedAt TEXT NOT NULL, - IsDeleted INTEGER NOT NULL, - DeletedAt TEXT, - DeletedBy TEXT NOT NULL DEFAULT '', - Version INTEGER NOT NULL, - IsLock INTEGER NOT NULL, - CreatedBy TEXT NOT NULL DEFAULT '', - UpdatedBy TEXT NOT NULL DEFAULT '', - Remark TEXT NOT NULL DEFAULT '', - Extend TEXT NOT NULL DEFAULT '{{}}' - ); - """); - await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_warehouses_Code ON wms_warehouses (Code);"); - - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS wms_material_types ( - Id TEXT NOT NULL CONSTRAINT PK_wms_material_types PRIMARY KEY, - Code TEXT NOT NULL, - Name TEXT NOT NULL, - Spec TEXT NOT NULL DEFAULT '', - Unit TEXT NOT NULL DEFAULT 'pcs', - Category TEXT NOT NULL DEFAULT '', - BarcodePrefix TEXT NOT NULL DEFAULT '', - Enabled INTEGER NOT NULL, - CreatedAt TEXT NOT NULL, - UpdatedAt TEXT NOT NULL, - IsDeleted INTEGER NOT NULL, - DeletedAt TEXT, - DeletedBy TEXT NOT NULL DEFAULT '', - Version INTEGER NOT NULL, - IsLock INTEGER NOT NULL, - CreatedBy TEXT NOT NULL DEFAULT '', - UpdatedBy TEXT NOT NULL DEFAULT '', - Remark TEXT NOT NULL DEFAULT '', - Extend TEXT NOT NULL DEFAULT '{{}}' - ); - """); - await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_material_types_Code ON wms_material_types (Code);"); - - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS wms_stock_events ( - Id TEXT NOT NULL CONSTRAINT PK_wms_stock_events PRIMARY KEY, - EventType TEXT NOT NULL, - MaterialId TEXT, - MaterialCode TEXT NOT NULL DEFAULT '', - MaterialName TEXT NOT NULL DEFAULT '', - MaterialBarcode TEXT NOT NULL DEFAULT '', - MaterialTypeCode TEXT NOT NULL DEFAULT '', - ContainerId TEXT, - ContainerCode TEXT NOT NULL DEFAULT '', - ContainerName TEXT NOT NULL DEFAULT '', - StorageId TEXT, - StorageCode TEXT NOT NULL DEFAULT '', - StorageName TEXT NOT NULL DEFAULT '', - AreaCode TEXT NOT NULL DEFAULT '', - FromStorageId TEXT, - FromStorageCode TEXT NOT NULL DEFAULT '', - FromStorageName TEXT NOT NULL DEFAULT '', - ToStorageId TEXT, - ToStorageCode TEXT NOT NULL DEFAULT '', - ToStorageName TEXT NOT NULL DEFAULT '', - RefType TEXT NOT NULL DEFAULT 'Manual', - RefId TEXT, - RefCode TEXT NOT NULL DEFAULT '', - Operator TEXT NOT NULL DEFAULT '', - OperatedAt TEXT NOT NULL, - Reason TEXT NOT NULL DEFAULT '' - ); - """); - await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_wms_stock_events_OperatedAt ON wms_stock_events (OperatedAt);"); - - await EnsureSqliteColumnAsync(db, "wms_areas", "WarehouseId", "TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'"); - await EnsureSqliteColumnAsync(db, "wms_areas", "LayoutMode", "TEXT NOT NULL DEFAULT 'Flat'"); - await EnsureSqliteColumnAsync(db, "wms_areas", "State", "TEXT NOT NULL DEFAULT 'Default'"); - - await EnsureSqliteColumnAsync(db, "wms_storages", "LocationKind", "TEXT NOT NULL DEFAULT 'Station'"); - await EnsureSqliteColumnAsync(db, "wms_storages", "ColumnNo", "INTEGER NOT NULL DEFAULT 0"); - await EnsureSqliteColumnAsync(db, "wms_storages", "LevelNo", "INTEGER NOT NULL DEFAULT 1"); - await EnsureSqliteColumnAsync(db, "wms_storages", "DepthNo", "INTEGER NOT NULL DEFAULT 1"); - await EnsureSqliteColumnAsync(db, "wms_storages", "SiteCode", "TEXT NOT NULL DEFAULT ''"); - await EnsureSqliteColumnAsync(db, "wms_storages", "Barcode", "TEXT NOT NULL DEFAULT ''"); - - await EnsureSqliteColumnAsync(db, "wms_containers", "AreaId", "TEXT"); - await EnsureSqliteColumnAsync(db, "wms_containers", "Barcode", "TEXT NOT NULL DEFAULT ''"); - await EnsureSqliteColumnAsync(db, "wms_containers", "Length", "REAL NOT NULL DEFAULT 0"); - await EnsureSqliteColumnAsync(db, "wms_containers", "Width", "REAL NOT NULL DEFAULT 0"); - await EnsureSqliteColumnAsync(db, "wms_containers", "Height", "REAL NOT NULL DEFAULT 0"); - - await EnsureSqliteColumnAsync(db, "wms_materials", "TypeCode", "TEXT NOT NULL DEFAULT ''"); - await EnsureSqliteColumnAsync(db, "wms_materials", "Barcode", "TEXT NOT NULL DEFAULT ''"); - await EnsureSqliteColumnAsync(db, "wms_materials", "LifecycleStatus", "TEXT NOT NULL DEFAULT 'Active'"); - await EnsureSqliteColumnAsync(db, "wms_materials", "UnboundAt", "TEXT"); - - await EnsureSqliteColumnAsync(db, "wms_container_materials", "BoundAt", "TEXT NOT NULL DEFAULT ''"); - } - - /// 为已存在的数据库补建 vehicle_alarms 表(车辆报警平台侧记录,幂等)。 - private static async Task EnsureVehicleAlarmsTableAsync(PlatformDbContext db) - { - if (db.Database.IsSqlite()) - { - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS vehicle_alarms ( - id TEXT NOT NULL CONSTRAINT PK_vehicle_alarms PRIMARY KEY, - car_id INTEGER NOT NULL DEFAULT 0, - car_name TEXT NOT NULL DEFAULT '', - info TEXT NOT NULL DEFAULT '', - level INTEGER NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'active', - first_at TEXT NOT NULL, - last_at TEXT NOT NULL, - resolved_at TEXT, - duration_secs INTEGER, - acknowledged INTEGER NOT NULL DEFAULT 0, - acknowledged_at TEXT, - acknowledged_by TEXT - ); - """); - await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_vehicle_alarms_car_status ON vehicle_alarms (car_id, status);"); - await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_vehicle_alarms_status ON vehicle_alarms (status);"); - await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_vehicle_alarms_first_at ON vehicle_alarms (first_at);"); - return; - } - - if (!await TableExistsAsync(db, "vehicle_alarms")) - { - var creator = db.GetService(); - await creator.CreateTablesAsync(); - } - } - - /// 为已存在的数据库补建 cdm_tasks 表(CDM 搬运任务平台侧快照,幂等)。 - private static async Task EnsureCdmTasksTableAsync(PlatformDbContext db) - { - if (db.Database.IsSqlite()) - { - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS cdm_tasks ( - id TEXT NOT NULL CONSTRAINT PK_cdm_tasks PRIMARY KEY, - task_id TEXT, - mission_id INTEGER NOT NULL DEFAULT 0, - mission_name TEXT NOT NULL DEFAULT '', - mission_type TEXT NOT NULL DEFAULT '', - src_site_id INTEGER NOT NULL DEFAULT 0, - src_label TEXT NOT NULL DEFAULT '', - dst_site_id INTEGER NOT NULL DEFAULT 0, - dst_label TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL DEFAULT '', - status_code TEXT NOT NULL DEFAULT '', - car_id INTEGER, - car_name TEXT, - priority INTEGER NOT NULL DEFAULT 0, - create_time TEXT, - start_time TEXT, - finish_time TEXT, - stuck_reason TEXT, - overdue INTEGER NOT NULL DEFAULT 0, - first_seen_at TEXT NOT NULL, - last_seen_at TEXT NOT NULL - ); - """); - await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_cdm_tasks_status_code ON cdm_tasks (status_code);"); - await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_cdm_tasks_create_time ON cdm_tasks (create_time);"); - return; - } - - if (!await TableExistsAsync(db, "cdm_tasks")) - { - var creator = db.GetService(); - await creator.CreateTablesAsync(); - } - } - - /// 为已存在的数据库补建 simple_fields 表(幂等)。 - private static async Task EnsureSimpleFieldsTableAsync(PlatformDbContext db) - { - if (db.Database.IsSqlite()) - { - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS simple_fields ( - id TEXT NOT NULL CONSTRAINT PK_simple_fields PRIMARY KEY, - car_type TEXT NOT NULL DEFAULT '', - field_type TEXT NOT NULL, - "key" TEXT NOT NULL, - value TEXT NOT NULL DEFAULT '', - data_type TEXT NOT NULL DEFAULT '', - chinese TEXT, - english TEXT, - other TEXT NOT NULL DEFAULT '', - is_default INTEGER NOT NULL, - create_time TEXT NOT NULL, - update_time TEXT NOT NULL - ); - """); - // 须先删旧索引 (field_type, other, key):把 other 清空为「其他语言」后会与旧唯一约束冲突。 - await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS IX_simple_fields_field_type_other_key;"); - await db.Database.ExecuteSqlRawAsync(""" - UPDATE simple_fields SET car_type = other - WHERE (car_type IS NULL OR car_type = '') AND other <> ''; - """); - await db.Database.ExecuteSqlRawAsync(""" - UPDATE simple_fields SET other = '' - WHERE other <> '' AND other = car_type; - """); - await db.Database.ExecuteSqlRawAsync(""" - CREATE UNIQUE INDEX IF NOT EXISTS IX_simple_fields_car_type_field_type_key - ON simple_fields (car_type, field_type, "key"); - """); - return; - } - - // 非 SQLite:表不存在时尝试按当前模型创建(已有库不会走 EnsureCreated)。 - if (!await TableExistsAsync(db, "simple_fields")) - { - var creator = db.GetService(); - await creator.CreateTablesAsync(); - } - } - - /// 为已存在的数据库补建 WMS 搬运规则/任务相关表及库位扩展列(幂等)。 - private static async Task EnsureWmsTransportSchemaAsync(PlatformDbContext db) - { - if (!db.Database.IsSqlite()) return; - - await EnsureSqliteColumnAsync(db, "wms_storages", "Status", "TEXT NOT NULL DEFAULT 'Available'"); - await EnsureSqliteColumnAsync(db, "wms_storages", "Usage", "TEXT NOT NULL DEFAULT ''"); - await EnsureSqliteColumnAsync(db, "wms_storages", "Priority", "INTEGER NOT NULL DEFAULT 0"); - await EnsureSqliteColumnAsync(db, "wms_storages", "ZoneCode", "TEXT NOT NULL DEFAULT ''"); - await EnsureSqliteColumnAsync(db, "wms_storages", "AllowInbound", "INTEGER NOT NULL DEFAULT 1"); - await EnsureSqliteColumnAsync(db, "wms_storages", "AllowOutbound", "INTEGER NOT NULL DEFAULT 1"); - - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS wms_transport_rules ( - Id TEXT NOT NULL CONSTRAINT PK_wms_transport_rules PRIMARY KEY, - CreatedAt TEXT NOT NULL, - UpdatedAt TEXT NOT NULL, - IsDeleted INTEGER NOT NULL, - DeletedAt TEXT, - DeletedBy TEXT, - Version INTEGER NOT NULL, - IsLock INTEGER NOT NULL, - CreatedBy TEXT NOT NULL, - UpdatedBy TEXT NOT NULL, - Remark TEXT NOT NULL, - Extend TEXT NOT NULL, - Code TEXT NOT NULL, - Name TEXT NOT NULL, - TriggerType TEXT NOT NULL, - Enabled INTEGER NOT NULL, - Priority INTEGER NOT NULL, - SourceSelectorJson TEXT NOT NULL, - TargetSelectorJson TEXT NOT NULL, - TaskOptionsJson TEXT NOT NULL - ); - """); - await db.Database.ExecuteSqlRawAsync(""" - CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_transport_rules_Code ON wms_transport_rules (Code); - """); - - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS wms_transport_tasks ( - Id TEXT NOT NULL CONSTRAINT PK_wms_transport_tasks PRIMARY KEY, - CreatedAt TEXT NOT NULL, - UpdatedAt TEXT NOT NULL, - IsDeleted INTEGER NOT NULL, - DeletedAt TEXT, - DeletedBy TEXT, - Version INTEGER NOT NULL, - IsLock INTEGER NOT NULL, - CreatedBy TEXT NOT NULL, - UpdatedBy TEXT NOT NULL, - Remark TEXT NOT NULL, - Extend TEXT NOT NULL, - BusinessType TEXT NOT NULL, - RuleId TEXT, - SourceStorageId TEXT NOT NULL, - TargetStorageId TEXT NOT NULL, - ContainerId TEXT NOT NULL, - MaterialId TEXT, - Quantity REAL, - Status TEXT NOT NULL, - DispatchMissionId TEXT NOT NULL DEFAULT '', - DeliveryId TEXT NOT NULL DEFAULT '', - DispatchStatus TEXT NOT NULL DEFAULT '', - Reason TEXT NOT NULL DEFAULT '', - SnapshotJson TEXT NOT NULL, - ErrorMessage TEXT NOT NULL DEFAULT '', - TaskPriority INTEGER NOT NULL DEFAULT 0 - ); - """); - - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS wms_transport_reservations ( - Id TEXT NOT NULL CONSTRAINT PK_wms_transport_reservations PRIMARY KEY, - CreatedAt TEXT NOT NULL, - UpdatedAt TEXT NOT NULL, - IsDeleted INTEGER NOT NULL, - DeletedAt TEXT, - DeletedBy TEXT, - Version INTEGER NOT NULL, - IsLock INTEGER NOT NULL, - CreatedBy TEXT NOT NULL, - UpdatedBy TEXT NOT NULL, - Remark TEXT NOT NULL, - Extend TEXT NOT NULL, - TaskId TEXT NOT NULL, - ContainerId TEXT NOT NULL, - SourceStorageId TEXT NOT NULL, - TargetStorageId TEXT NOT NULL, - Status TEXT NOT NULL, - ExpiresAt TEXT - ); - """); - - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS wms_transport_task_history ( - Id TEXT NOT NULL CONSTRAINT PK_wms_transport_task_history PRIMARY KEY, - TaskId TEXT NOT NULL, - FromStatus TEXT NOT NULL, - ToStatus TEXT NOT NULL, - Operator TEXT NOT NULL, - OperatedAt TEXT NOT NULL, - Reason TEXT NOT NULL, - ErrorMessage TEXT NOT NULL, - SnapshotJson TEXT NOT NULL - ); - """); - - // 库位占用唯一约束(幂等);若库内已有重复占用会创建失败,不阻断启动 - try - { - await db.Database.ExecuteSqlRawAsync(""" - CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_container_locations_StorageLocationId - ON wms_container_locations (LocationId) - WHERE LocationType = 'Storage' AND IsDeleted = 0; - """); - } - catch (Exception ex) - { - Console.Error.WriteLine( - $"[WMS] 无法创建库位占用唯一索引 IX_wms_container_locations_StorageLocationId(可能已有重复占用): {ex.Message}"); - } - } - - private static async Task EnsureSqliteColumnAsync(PlatformDbContext db, string table, string column, string definition) - { - // 仅允许内部迁移调用方传入的标识符;definition 可含 GUID 默认值连字符。 - static bool IsSafeIdent(string s) => - !string.IsNullOrEmpty(s) && s.All(ch => char.IsAsciiLetterOrDigit(ch) || ch == '_'); - if (!IsSafeIdent(table) || !IsSafeIdent(column) - || definition.Contains(';') || definition.Contains("--") - || !System.Text.RegularExpressions.Regex.IsMatch(definition, @"^[A-Za-z0-9_()'.,\s\-]+$")) - throw new ArgumentException("unsafe sqlite migration identifier"); - - var conn = db.Database.GetDbConnection(); - if (conn.State != System.Data.ConnectionState.Open) - await conn.OpenAsync(); - await using var cmd = conn.CreateCommand(); - cmd.CommandText = $"PRAGMA table_info({table});"; - await using var reader = await cmd.ExecuteReaderAsync(); - while (await reader.ReadAsync()) - { - var name = reader.GetString(1); - if (string.Equals(name, column, StringComparison.OrdinalIgnoreCase)) - return; - } - - await db.Database.ExecuteSqlRawAsync($"ALTER TABLE {table} ADD COLUMN {column} {definition};"); - } - - /// 为已存在的数据库补建 user_dashboard_shortcuts 表(幂等)。 - private static async Task EnsureUserDashboardShortcutsTableAsync(PlatformDbContext db) - { - if (db.Database.IsSqlite()) - { - await db.Database.ExecuteSqlRawAsync(""" - CREATE TABLE IF NOT EXISTS user_dashboard_shortcuts ( - user_id TEXT NOT NULL, - scope TEXT NOT NULL, - keys_json TEXT NOT NULL DEFAULT '[]', - updated_at TEXT NOT NULL, - CONSTRAINT PK_user_dashboard_shortcuts PRIMARY KEY (user_id, scope) - ); - """); - return; - } - - if (!await TableExistsAsync(db, "user_dashboard_shortcuts")) - { - var creator = db.GetService(); - await creator.CreateTablesAsync(); - } - } - - /// - /// 检查表是否存在 - /// - /// 数据库上下文 - /// 表名 - /// 表是否存在 - private static async Task TableExistsAsync(PlatformDbContext db, string table) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != System.Data.ConnectionState.Open) - await conn.OpenAsync(); - try - { - await using var cmd = conn.CreateCommand(); - if (db.Database.IsSqlServer()) - { - cmd.CommandText = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @t"; - var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); - } - else if (db.Database.IsNpgsql()) - { - cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_name = @t"; - var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); - } - else if (db.Database.IsMySql()) - { - cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = @t"; - var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); - } - else - { - return false; - } - var result = await cmd.ExecuteScalarAsync(); - return result != null; - } - finally - { - if (conn.State == System.Data.ConnectionState.Open) - await conn.CloseAsync(); - } - } - - 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(); - } } diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs index ecdd69d..e0d93cc 100644 --- a/MiGu.Server/Program.cs +++ b/MiGu.Server/Program.cs @@ -8,6 +8,7 @@ using MiGu.Server.Configs; using MiGu.Server.Launcher; using MiGu.Server.OpenApi; using MiGu.Server.Ota; +using MiGu.DB.Kernel.Hosting; using MiGu.Server.Persistence; using Yarp.ReverseProxy.Transforms; @@ -279,7 +280,7 @@ builder.Services.AddHostedService(); builder.Services.AddHttpClient(nameof(WatchDogClient)); var app = builder.Build(); -await app.Services.EnsurePlatformDatabaseAsync(); +await app.Services.MigrateMiGuDbAsync(); // 启动期主动构造 JwtIssuer / InternalTokenStore:让 secret 校验日志在请求来之前打印。 _ = app.Services.GetRequiredService(); @@ -316,6 +317,14 @@ if (app.Environment.IsDevelopment()) app.UseSwaggerUI(); } +// OTA 回传会话按 TCP 对端 IP 校验;必须在 ForwardedHeaders 改写 RemoteIpAddress 之前捕获。 +app.Use(async (ctx, next) => +{ + ctx.Items[MiGu.Server.Controllers.OtaReceiveController.TcpRemoteIpItemKey] = + ctx.Connection.RemoteIpAddress?.ToString(); + await next(); +}); + // M6:在反向代理 / 负载均衡后运行时,根据 X-Forwarded-Proto 还原真实 scheme,让 // Request.IsHttps 正确 → 登录 Cookie 的 Secure 标志在生产 HTTPS 下能正确置位。 // 默认(未配置 KnownProxies):清空 Known* 表 = 信任所有前置转发头,适合「反代与本服务同机 / @@ -357,6 +366,7 @@ app.UseStaticFiles(); // 鉴权 / 授权管道必须放在 MapControllers 之前;CORS 之后。 app.UseAuthentication(); +app.UseMiddleware(); app.UseAuthorization(); app.MapControllers(); diff --git a/MiGu.Server/README.md b/MiGu.Server/README.md index 87216c1..6a94aef 100644 --- a/MiGu.Server/README.md +++ b/MiGu.Server/README.md @@ -97,6 +97,88 @@ $env:SimpleLite__Enabled = "true" > 如果只想跑 MiGu.Server 单进程调试(不拉 SimpleLite),把 `appsettings.json:SimpleLite.Enabled` 改为 `false` 即可。 +## 数据库启动流程 + +持久层在独立工程 **[MiGu.DB](../MiGu.DB/README.md)**;Server 通过 `PlatformPersistence` 接入。 + +### 启动顺序(`Program.cs`) + +```text +AddPlatformPersistence(configuration) # 注册 AddMiGuDb + Wms/SimpleFields 等业务服务 + │ + ▼ +builder.Build() + │ + ▼ +MigrateMiGuDbAsync() # Schema 初始化 + 可选 IDataMigrator + │ + ▼ +… 其余中间件 … +UseMiddleware # 请求级写入 IActorContext(审计戳) +``` + +### 配置(`appsettings*.json`) + +```json +"Database": { + "Provider": "sqlite", + "SchemaMode": "Migrate", + "ApplyDataMigratorsOnStartup": true +}, +"ConnectionStrings": { + "Platform": "Data Source=data/platform.db" +} +``` + +| 配置项 | 含义 | 取值 | +|--------|------|------| +| `Provider` | 数据库种类 | `sqlite`(默认)/ `mysql` / `npgsql` / `sqlserver` | +| `SchemaMode` | 启动时如何初始化 Schema | 见下表 | +| `ApplyDataMigratorsOnStartup` | Schema 完成后是否跑数据修补 | `true`(默认)/ `false` | + +当前仓库约定: + +- `appsettings.json` → `"SchemaMode": "Migrate"`(发版/默认) +- `appsettings.Development.json` → `"SchemaMode": "EnsureCreated"`(本地开发) + +也可用环境变量覆盖,例如:`$env:Database__SchemaMode = "Migrate"`。 + +### SchemaMode 行为 + +| 值 | 行为 | 适用 | +|----|------|------| +| **`Migrate`** | 旧库无 `__EFMigrationsHistory` 时先写 Initial 基线 → `MigrateAsync` →(可选)DataMigrator | 测试 / 预发 / 生产 | +| **`EnsureCreated`** | 仅 `EnsureCreated`(按**当前模型**建空库),**不**跑 Migrations →(可选)DataMigrator | 开发期改实体、暂不生成迁移 | + +流程图: + +```text +SchemaMode = EnsureCreated + → EnsureCreatedAsync(库已存在则不改结构) + → IDataMigrator(ApplyDataMigratorsOnStartup=true 时) + +SchemaMode = Migrate + → 程序集无 Migration → EnsureCreated 兜底 + → 否则:基线(如需)→ MigrateAsync + → IDataMigrator(可选) +``` + +**开发注意(EnsureCreated):** + +- 改实体后结构不会自动升级;请删除 `MiGu.Server/data/platform.db*` 再启动。 +- 不会写入迁移历史;以后改回 `Migrate` 时,对已有库会走基线再应用 Migrations。 + +**发版注意:** + +- 现场务必使用 `Migrate`,并随包带上 `MiGu.DB/Migrations/Sqlite/`。 +- 开发期若长期 `EnsureCreated` 改模型,发版前必须对**当前模型**执行 `dotnet ef migrations add`,再切回 `Migrate` 验证;否则空库只会落到旧 Migration,结构落后于代码。 +- 从 EnsureCreated 库切到 `Migrate`:若无 History,启动会把**全部** pending Migration 写入基线(假定库已对齐模型 tip)。结构不对齐时应删库重建或手工处理。 + +### 相关文档 + +- 持久层总览与 SchemaMode 摘要:[MiGu.DB/README.md](../MiGu.DB/README.md) +- 如何生成/管理 Migration:[MiGu.DB/MIGRATIONS.md](../MiGu.DB/MIGRATIONS.md) + ## 安全须知(生产部署必读) > **重要:以下默认值仅供本地开发,切勿原样用于生产环境。** @@ -136,7 +218,7 @@ $env:SimpleLite__Enabled = "true" - 健康检查:`/api/health`; - Swagger:开发环境下 `/swagger`。 -> 不在本轮范围:真实 SimpleLite WebAPI、SystemMission 拉起、真实 JWT/RBAC、SQLite/EF Core 持久层。 +> 能力补充:平台库由 **MiGu.DB**(EF Core)承载,启动流程见「[数据库启动流程](#数据库启动流程)」。 ## 目录结构 @@ -242,3 +324,5 @@ YARP 路由配置见 `appsettings.json`: - [ARCHITECTURE.md](ARCHITECTURE.md) — v1.6 总体架构(§4 进程拓扑、§6.3 YARP 配置、§9 配置中心、§10 交互序列、§17 视觉规范) - [(见 Simple 仓库)frontends/README.md]((见 Simple 仓库)frontends/README.md) — 前端启动与联调说明(含「迷毂」品牌与紫色主题约定) +- [MiGu.DB/README.md](../MiGu.DB/README.md) — 持久层框架;[MIGRATIONS.md](../MiGu.DB/MIGRATIONS.md) — 生成 Migration 手册 +- 本文「[数据库启动流程](#数据库启动流程)」— SchemaMode / EnsureCreated / Migrate diff --git a/MiGu.Server/SimpleFields/SimpleFieldDateTime.cs b/MiGu.Server/SimpleFields/SimpleFieldDateTime.cs deleted file mode 100644 index 1ab0bd1..0000000 --- a/MiGu.Server/SimpleFields/SimpleFieldDateTime.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Globalization; - -namespace MiGu.Server.SimpleFields; - -public static class SimpleFieldDateTime -{ - public const string StorageFormat = "yyyy-MM-dd HH:mm:ss"; - - public static DateTimeOffset Now => DateTimeOffset.Now; - - public static string ToStorage(DateTimeOffset value) => - value.LocalDateTime.ToString(StorageFormat, CultureInfo.InvariantCulture); - - public static DateTimeOffset FromStorage(string value) - { - if (DateTime.TryParseExact(value, StorageFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var local)) - return new DateTimeOffset(local); - - return DateTimeOffset.Parse(value, CultureInfo.InvariantCulture); - } -} diff --git a/MiGu.Server/SimpleFields/SimpleFieldModels.cs b/MiGu.Server/SimpleFields/SimpleFieldModels.cs index 1f2f2a5..4f91248 100644 --- a/MiGu.Server/SimpleFields/SimpleFieldModels.cs +++ b/MiGu.Server/SimpleFields/SimpleFieldModels.cs @@ -1,72 +1,5 @@ -using System.ComponentModel.DataAnnotations; - namespace MiGu.Server.SimpleFields; -public sealed class SimpleField -{ - public Guid Id { get; set; } = Guid.NewGuid(); - - /// 车型唯一标识:assemblyName.shortName,如 StandardScene.QrLidar.Forklift。 - [MaxLength(64)] - public string CarType { get; set; } = ""; - - /// - /// 字段类型唯一标识:assemblyName.shortName,如 StandardScene.QrLidar.Forklift。 - /// - [MaxLength(64)] - public string FieldType { get; set; } = ""; - - /// - /// 字段唯一标识:key,如 Forklift.PositionX。 - /// - [MaxLength(128)] - public string Key { get; set; } = ""; - - /// - /// 字段值,如 10.0。 - /// - public string Value { get; set; } = ""; - - /// - /// 数据类型,如 System.String。 - /// - [MaxLength(128)] - public string DataType { get; set; } = ""; - - /// - /// 中文名称,如 位置 X。 - /// - [MaxLength(256)] - public string? Chinese { get; set; } - - /// - /// 英文名称,如 Position X。 - /// - [MaxLength(256)] - public string? English { get; set; } - - /// - /// 其他语言名称,如 位置 X。 - /// - [MaxLength(512)] - public string Other { get; set; } = ""; - - /// - /// 是否内置默认字段,如 true。 - /// - public bool IsDefault { get; set; } - - /// - /// 创建时间,如 2021-01-01 12:00:00。 - /// - public DateTimeOffset CreateTime { get; set; } - - /// - /// 更新时间,如 2021-01-01 12:00:00。 - /// - public DateTimeOffset UpdateTime { get; set; } -} - public sealed record SimpleFieldRequest( Guid? Id, string CarType, diff --git a/MiGu.Server/SimpleFields/SimpleFieldService.cs b/MiGu.Server/SimpleFields/SimpleFieldService.cs index b9a23ea..47bb27b 100644 --- a/MiGu.Server/SimpleFields/SimpleFieldService.cs +++ b/MiGu.Server/SimpleFields/SimpleFieldService.cs @@ -1,13 +1,14 @@ using Microsoft.EntityFrameworkCore; using MiGu.Server.Persistence; +using MiGu.DB.Domains.SimpleFields; namespace MiGu.Server.SimpleFields; public sealed class SimpleFieldService { - private readonly PlatformDbContext _db; + private readonly MiGuDbContext _db; - public SimpleFieldService(PlatformDbContext db) => _db = db; + public SimpleFieldService(MiGuDbContext db) => _db = db; public async Task> ListAsync(string? fieldType = null, string? carType = null, string? q = null) { diff --git a/MiGu.Server/Wms/WmsModels.cs b/MiGu.Server/Wms/WmsModels.cs index 5a9d2f3..0bb0d60 100644 --- a/MiGu.Server/Wms/WmsModels.cs +++ b/MiGu.Server/Wms/WmsModels.cs @@ -1,358 +1,59 @@ -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; } = "{}"; -} +// API / 读模型 DTO(留在 Server,不进 MiGu.DB)。请求字段中的状态仍为 string,由 Service 解析为枚举后写入实体。 -public sealed class Warehouse : EntityBase -{ - [MaxLength(64)] public string Code { get; set; } = ""; - [MaxLength(128)] public string Name { get; set; } = ""; - [MaxLength(64)] public string Type { get; set; } = "Default"; - public bool Enabled { get; set; } = true; - public int SortOrder { get; set; } -} +public sealed record InventoryMaterialRow( + Guid MaterialId, string MaterialCode, string MaterialName, string MaterialBarcode, string MaterialTypeCode, + Guid ContainerId, string ContainerCode, string ContainerName, + Guid? StorageId, string StorageCode, string StorageName, + Guid? AreaId, string AreaCode, string AreaName, + string LocationType, DateTimeOffset BoundAt); -public sealed class WarehouseArea : EntityBase -{ - public Guid WarehouseId { get; set; } - [MaxLength(64)] public string Code { get; set; } = ""; - [MaxLength(128)] public string Name { get; set; } = ""; - [MaxLength(64)] public string Type { get; set; } = "Storage"; - [MaxLength(32)] public string LayoutMode { get; set; } = AreaLayoutModes.Flat; - [MaxLength(32)] public string State { get; set; } = "Default"; - public bool Enabled { get; set; } = true; - public int SortOrder { get; set; } -} +public sealed record ContainerLocationSnapshot( + Guid Id, Guid ContainerId, string LocationType, string LocationId, string LocationCode, string LocationName, + string Status, DateTimeOffset EnteredAt, long Version); -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(32)] public string LocationKind { get; set; } = LocationKinds.Station; - public int ColumnNo { get; set; } - public int LevelNo { get; set; } = 1; - public int DepthNo { get; set; } = 1; - [MaxLength(64)] public string SiteId { get; set; } = ""; - [MaxLength(64)] public string SiteCode { get; set; } = ""; - [MaxLength(128)] public string Barcode { get; set; } = ""; - /// 已废弃:一货位一容器,保留列兼容旧库。 - public int Capacity { get; set; } - [MaxLength(32)] public string Status { get; set; } = StorageStatuses.Empty; - [MaxLength(64)] public string Usage { get; set; } = ""; - public int Priority { get; set; } - [MaxLength(64)] public string ZoneCode { get; set; } = ""; - public bool AllowInbound { get; set; } = true; - public bool AllowOutbound { get; set; } = true; - public bool Enabled { get; set; } = true; -} +public sealed record ContainerMaterialSnapshot( + Guid Id, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo, string Status, + DateTimeOffset BoundAt, DateTimeOffset LoadedAt, DateTimeOffset? UnloadedAt, long Version); -public sealed class Container : EntityBase -{ - public Guid? AreaId { get; set; } - [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; } = ContainerStatuses.EmptyMaterial; - [MaxLength(128)] public string Barcode { get; set; } = ""; - public double Length { get; set; } - public double Width { get; set; } - public double Height { get; set; } - public bool Enabled { get; set; } = true; -} +public abstract record CommonRequest(Guid? Id, long? Version, bool IsLock, string Remark, string Extend); -public sealed class MaterialType : 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; } = ""; - [MaxLength(64)] public string BarcodePrefix { get; set; } = ""; - public bool Enabled { get; set; } = true; -} +public sealed record MasterDataRequest( + Guid? Id, long? Version, string Code, string Name, string Type, bool Enabled, int SortOrder, + bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend); -public sealed class Material : EntityBase -{ - [MaxLength(64)] public string Code { get; set; } = ""; - [MaxLength(128)] public string Name { get; set; } = ""; - [MaxLength(64)] public string TypeCode { get; set; } = ""; - [MaxLength(128)] public string Barcode { get; set; } = ""; - [MaxLength(128)] public string Spec { get; set; } = ""; - [MaxLength(32)] public string Unit { get; set; } = "pcs"; - [MaxLength(64)] public string Category { get; set; } = ""; - [MaxLength(32)] public string LifecycleStatus { get; set; } = MaterialLifecycles.Active; - public DateTimeOffset? UnboundAt { get; set; } - public bool Enabled { get; set; } = true; -} +public sealed record AreaRequest( + Guid? Id, long? Version, Guid? WarehouseId, string Code, string Name, string Type, string LayoutMode, string State, + bool Enabled, int SortOrder, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend); -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 record StorageRequest( + Guid? Id, long? Version, Guid AreaId, string Code, string Name, string StorageType, string LocationKind, + int ColumnNo, int LevelNo, int DepthNo, string SiteId, string SiteCode, string Barcode, int Capacity, + string Status, string Usage, int Priority, string ZoneCode, bool AllowInbound, bool AllowOutbound, bool Enabled, + bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend); -/// 容器-物料绑定(无数量语义;Quantity 列仅兼容旧库,固定为 1)。 -public sealed class ContainerMaterial : EntityBase -{ - public Guid ContainerId { get; set; } - public Guid MaterialId { get; set; } - public decimal Quantity { get; set; } = 1; - [MaxLength(64)] public string BatchNo { get; set; } = ""; - [MaxLength(64)] public string SerialNo { get; set; } = ""; - [MaxLength(32)] public string Status { get; set; } = ContainerMaterialStatuses.Bound; - public DateTimeOffset BoundAt { get; set; } - public DateTimeOffset LoadedAt { get; set; } - public DateTimeOffset? UnloadedAt { get; set; } -} +public sealed record GenerateBinsRequest(int ColumnFrom, int ColumnTo, int LevelFrom, int LevelTo, int DepthFrom, int DepthTo, string? CodePattern); -public sealed class StockEvent -{ - public Guid Id { get; set; } = Guid.NewGuid(); - [MaxLength(32)] public string EventType { get; set; } = ""; - public Guid? MaterialId { get; set; } - [MaxLength(64)] public string MaterialCode { get; set; } = ""; - [MaxLength(128)] public string MaterialName { get; set; } = ""; - [MaxLength(128)] public string MaterialBarcode { get; set; } = ""; - [MaxLength(64)] public string MaterialTypeCode { get; set; } = ""; - public Guid? ContainerId { get; set; } - [MaxLength(64)] public string ContainerCode { get; set; } = ""; - [MaxLength(128)] public string ContainerName { get; set; } = ""; - public Guid? StorageId { get; set; } - [MaxLength(64)] public string StorageCode { get; set; } = ""; - [MaxLength(128)] public string StorageName { get; set; } = ""; - [MaxLength(64)] public string AreaCode { get; set; } = ""; - public Guid? FromStorageId { get; set; } - [MaxLength(64)] public string FromStorageCode { get; set; } = ""; - [MaxLength(128)] public string FromStorageName { get; set; } = ""; - public Guid? ToStorageId { get; set; } - [MaxLength(64)] public string ToStorageCode { get; set; } = ""; - [MaxLength(128)] public string ToStorageName { get; set; } = ""; - [MaxLength(64)] public string RefType { get; set; } = "Manual"; - public Guid? RefId { get; set; } - [MaxLength(64)] public string RefCode { get; set; } = ""; - [MaxLength(128)] public string Operator { get; set; } = ""; - public DateTimeOffset OperatedAt { get; set; } - [MaxLength(500)] public string Reason { get; set; } = ""; -} +public sealed record ContainerRequest( + Guid? Id, long? Version, Guid? AreaId, string Code, string Name, string Type, string Status, string Barcode, + double Length, double Width, double Height, bool Enabled, bool IsLock, string Remark, string Extend) + : CommonRequest(Id, Version, IsLock, Remark, Extend); -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 record MaterialTypeRequest( + Guid? Id, long? Version, string Code, string Name, string Spec, string Unit, string Category, string BarcodePrefix, + bool Enabled, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend); -public sealed class ContainerMaterialHistory : WarehouseHistoryBase -{ - public Guid? MaterialId { get; set; } - public decimal QuantityDelta { get; set; } -} +public sealed record MaterialRequest( + Guid? Id, long? Version, string Code, string Name, string TypeCode, string Barcode, string Spec, string Unit, + string Category, string LifecycleStatus, bool Enabled, bool IsLock, string Remark, string Extend) + : CommonRequest(Id, Version, IsLock, Remark, Extend); -public static class AreaLayoutModes -{ - public const string Flat = "Flat"; - public const string Grid = "Grid"; - public static readonly HashSet All = new(StringComparer.OrdinalIgnoreCase) { Flat, Grid }; -} +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 static class LocationKinds -{ - public const string Grid = "Grid"; - public const string Station = "Station"; - public static readonly HashSet All = new(StringComparer.OrdinalIgnoreCase) { Grid, Station }; -} - -public static class ContainerLocationTypes -{ - public const string Storage = "Storage"; - public const string Car = "Car"; - public static readonly HashSet 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 All = new(StringComparer.OrdinalIgnoreCase) { Active, Locked, Exception }; -} - -public static class ContainerMaterialStatuses -{ - public const string Bound = "Bound"; - // 兼容旧数据 - public const string Loaded = "Loaded"; - public const string Unloaded = "Unloaded"; - public const string Adjusted = "Adjusted"; - public const string Frozen = "Frozen"; - public static readonly HashSet ActiveBind = new(StringComparer.OrdinalIgnoreCase) { Bound, Loaded, Adjusted, Frozen }; -} - -public static class ContainerStatuses -{ - public const string EmptyMaterial = "EmptyMaterial"; - public const string FullMaterial = "FullMaterial"; - public static readonly HashSet All = new(StringComparer.OrdinalIgnoreCase) { EmptyMaterial, FullMaterial }; -} - -public static class StorageStatuses -{ - public const string Empty = "Empty"; - public const string EmptyContainer = "EmptyContainer"; - public const string FullContainer = "FullContainer"; - public const string Disabled = "Disabled"; - // 兼容旧值(读时映射,写时用新值) - public const string Available = "Available"; - public const string Idle = "Idle"; - public const string Occupied = "Occupied"; - - public static readonly HashSet All = new(StringComparer.OrdinalIgnoreCase) - { Empty, EmptyContainer, FullContainer, Disabled }; - - public static string Normalize(string? status) => status switch - { - Available or Idle => Empty, - Occupied => FullContainer, - _ when string.IsNullOrWhiteSpace(status) => Empty, - _ => status - }; -} - -public static class StorageTypes -{ - public const string Storage = "Storage"; - public const string LineSide = "LineSide"; - public const string OfflinePoint = "OfflinePoint"; - public const string Buffer = "Buffer"; - public const string FinishedGoods = "FinishedGoods"; -} - -public static class MaterialLifecycles -{ - public const string Active = "Active"; - public const string Archived = "Archived"; - public static readonly HashSet All = new(StringComparer.OrdinalIgnoreCase) { Active, Archived }; -} - -public static class StockEventTypes -{ - public const string Bind = "Bind"; - public const string Unbind = "Unbind"; - public const string ContainerMove = "ContainerMove"; -} - -public static class WmsTransportTriggerTypes -{ - public const string MaterialCall = "MaterialCall"; - public const string FinishedGoodsOffline = "FinishedGoodsOffline"; - public const string AutoTransfer = "AutoTransfer"; - public static readonly HashSet All = new(StringComparer.OrdinalIgnoreCase) - { MaterialCall, FinishedGoodsOffline, AutoTransfer }; -} - -public static class WmsTransportTaskStatuses -{ - public const string Pending = "Pending"; - public const string Reserved = "Reserved"; - public const string Dispatched = "Dispatched"; - public const string InTransit = "InTransit"; - public const string Completed = "Completed"; - public const string Failed = "Failed"; - public const string Cancelled = "Cancelled"; - public static readonly HashSet Active = new(StringComparer.OrdinalIgnoreCase) - { Pending, Reserved, Dispatched, InTransit }; - public static readonly HashSet All = new(StringComparer.OrdinalIgnoreCase) - { Pending, Reserved, Dispatched, InTransit, Completed, Failed, Cancelled }; -} - -public static class WmsReservationStatuses -{ - public const string Active = "Active"; - public const string Released = "Released"; - public const string Expired = "Expired"; -} - -public sealed class WmsTransportRule : EntityBase -{ - [MaxLength(64)] public string Code { get; set; } = ""; - [MaxLength(128)] public string Name { get; set; } = ""; - [MaxLength(64)] public string TriggerType { get; set; } = WmsTransportTriggerTypes.MaterialCall; - public bool Enabled { get; set; } = true; - public int Priority { get; set; } - public string SourceSelectorJson { get; set; } = "{}"; - public string TargetSelectorJson { get; set; } = "{}"; - public string TaskOptionsJson { get; set; } = "{}"; -} - -public sealed class WmsTransportTask : EntityBase -{ - [MaxLength(64)] public string BusinessType { get; set; } = ""; - public Guid? RuleId { get; set; } - public Guid SourceStorageId { get; set; } - public Guid TargetStorageId { get; set; } - public Guid ContainerId { get; set; } - public Guid? MaterialId { get; set; } - /// 已废弃:单物料实体模型不再使用数量。 - public decimal? Quantity { get; set; } - [MaxLength(32)] public string Status { get; set; } = WmsTransportTaskStatuses.Pending; - [MaxLength(64)] public string DispatchMissionId { get; set; } = ""; - [MaxLength(64)] public string DeliveryId { get; set; } = ""; - [MaxLength(32)] public string DispatchStatus { get; set; } = ""; - [MaxLength(500)] public string Reason { get; set; } = ""; - public string SnapshotJson { get; set; } = "{}"; - [MaxLength(1000)] public string ErrorMessage { get; set; } = ""; - public int TaskPriority { get; set; } -} - -public sealed class WmsTransportReservation : EntityBase -{ - public Guid TaskId { get; set; } - public Guid ContainerId { get; set; } - public Guid SourceStorageId { get; set; } - public Guid TargetStorageId { get; set; } - [MaxLength(32)] public string Status { get; set; } = WmsReservationStatuses.Active; - public DateTimeOffset? ExpiresAt { get; set; } -} - -public sealed class WmsTransportTaskHistory -{ - public Guid Id { get; set; } = Guid.NewGuid(); - public Guid TaskId { get; set; } - [MaxLength(32)] public string FromStatus { get; set; } = ""; - [MaxLength(32)] public string ToStatus { get; set; } = ""; - [MaxLength(128)] public string Operator { get; set; } = ""; - public DateTimeOffset OperatedAt { get; set; } - [MaxLength(500)] public string Reason { get; set; } = ""; - [MaxLength(1000)] public string ErrorMessage { get; set; } = ""; - public string SnapshotJson { get; set; } = "{}"; -} - -public static class WmsDefaults -{ - public const string DefaultWarehouseCode = "DEFAULT"; - public const string DefaultWarehouseName = "默认仓库"; -} +public sealed record BindMaterialRequest( + Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, string Source, string Reason, + bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend); diff --git a/MiGu.Server/Wms/WmsReferenceValidator.cs b/MiGu.Server/Wms/WmsReferenceValidator.cs index b3ef8d4..72a8870 100644 --- a/MiGu.Server/Wms/WmsReferenceValidator.cs +++ b/MiGu.Server/Wms/WmsReferenceValidator.cs @@ -5,9 +5,9 @@ namespace MiGu.Server.Wms; public sealed class WmsReferenceValidator { - private readonly PlatformDbContext _db; + private readonly MiGuDbContext _db; - public WmsReferenceValidator(PlatformDbContext db) + public WmsReferenceValidator(MiGuDbContext db) { _db = db; } @@ -50,12 +50,12 @@ public sealed class WmsReferenceValidator public async Task<(string Code, string Name)> ResolveLocationSnapshotAsync(string locationType, string locationId) { - if (!ContainerLocationTypes.All.Contains(locationType)) + if (!ContainerLocationTypes.IsDefined(locationType)) throw new InvalidOperationException("位置类型无效"); if (string.IsNullOrWhiteSpace(locationId)) throw new InvalidOperationException("位置 ID 不能为空"); - if (string.Equals(locationType, ContainerLocationTypes.Storage, StringComparison.OrdinalIgnoreCase)) + if (ContainerLocationTypes.EqualsString(ContainerLocationTypes.Storage, locationType)) { if (!Guid.TryParse(locationId, out var id)) throw new InvalidOperationException("库位 ID 格式无效"); var s = await _db.Storages.FirstOrDefaultAsync(x => x.Id == id); diff --git a/MiGu.Server/Wms/WmsService.cs b/MiGu.Server/Wms/WmsService.cs index 9160a54..7e563ab 100644 --- a/MiGu.Server/Wms/WmsService.cs +++ b/MiGu.Server/Wms/WmsService.cs @@ -1,19 +1,30 @@ using System.Security.Claims; using System.Text.Json; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using MiGu.DB.Abstractions.Entities; +using MiGu.DB.Abstractions.Persistence; using MiGu.Server.Persistence; namespace MiGu.Server.Wms; public sealed class WmsService { - private readonly PlatformDbContext _db; + private readonly MiGuDbContext _db; + private readonly IUnitOfWork _uow; + private readonly IServiceProvider _services; private readonly WmsReferenceValidator _refs; private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web); - public WmsService(PlatformDbContext db, WmsReferenceValidator refs) + public WmsService( + MiGuDbContext db, + IUnitOfWork uow, + IServiceProvider services, + WmsReferenceValidator refs) { _db = db; + _uow = uow; + _services = services; _refs = refs; } @@ -31,7 +42,10 @@ public sealed class WmsService { var query = _db.Storages.AsNoTracking().AsQueryable(); if (areaId.HasValue) query = query.Where(x => x.AreaId == areaId.Value); - if (!string.IsNullOrWhiteSpace(locationKind)) query = query.Where(x => x.LocationKind == locationKind); + if (!string.IsNullOrWhiteSpace(locationKind) && + Enum.TryParse(locationKind.Trim(), true, out var kind) && + LocationKinds.All.Contains(kind)) + query = query.Where(x => x.LocationKind == kind); return FilterByKeyword(query.OrderBy(x => x.Code), q).ToListAsync(); } @@ -44,7 +58,7 @@ public sealed class WmsService public Task> Materials(string? q = null, string? lifecycle = null, bool? onlyUnbound = null, bool? onlyBound = null) { var query = _db.Materials.AsNoTracking().AsQueryable(); - var life = string.IsNullOrWhiteSpace(lifecycle) ? MaterialLifecycles.Active : lifecycle.Trim(); + var life = MaterialLifecycles.ParseOr(lifecycle); query = query.Where(x => x.LifecycleStatus == life); if (onlyUnbound == true || onlyBound == true) { @@ -58,7 +72,11 @@ public sealed class WmsService public Task> 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); + if (!string.IsNullOrWhiteSpace(locationType) && ContainerLocationTypes.IsDefined(locationType)) + { + var lt = ContainerLocationTypes.ParseOr(locationType); + query = query.Where(x => x.LocationType == lt); + } return FilterByKeyword(query, q).ToListAsync(); } @@ -69,75 +87,85 @@ public sealed class WmsService return FilterByKeyword(query.OrderBy(x => x.ContainerId), q).ToListAsync(); } + /// + /// 库存物料列表:筛选/排序下推到 SQL。 + /// 库位:优先 ContainerLocation.StorageId;未回填时用 LocationId 与库位 Id 的存储字符串(Guid "D")匹配。 + /// public async Task> InventoryMaterials(Guid? areaId = null, Guid? storageId = null, string? q = null) { - var binds = await _db.ContainerMaterials.AsNoTracking().ToListAsync(); - var materials = await _db.Materials.AsNoTracking().ToDictionaryAsync(x => x.Id); - var locations = await _db.ContainerLocations.AsNoTracking().ToDictionaryAsync(x => x.ContainerId); - var storages = await _db.Storages.AsNoTracking().ToDictionaryAsync(x => x.Id); - var areas = await _db.WarehouseAreas.AsNoTracking().ToDictionaryAsync(x => x.Id); - var containers = await _db.Containers.AsNoTracking().ToDictionaryAsync(x => x.Id); + var query = + from b in _db.ContainerMaterials.AsNoTracking() + join mat in _db.Materials.AsNoTracking() on b.MaterialId equals mat.Id + from ctn in _db.Containers.AsNoTracking().Where(c => c.Id == b.ContainerId).DefaultIfEmpty() + from loc in _db.ContainerLocations.AsNoTracking().Where(l => l.ContainerId == b.ContainerId).DefaultIfEmpty() + from st in _db.Storages.AsNoTracking().Where(s => + loc != null && + loc.LocationType == ContainerLocationType.Storage && + (loc.StorageId == s.Id || + (loc.StorageId == null && loc.LocationId == EF.Property(s, nameof(Storage.Id))))).DefaultIfEmpty() + from area in _db.WarehouseAreas.AsNoTracking().Where(a => st != null && a.Id == st.AreaId).DefaultIfEmpty() + select new { b, mat, ctn, loc, st, area }; - var rows = new List(); - foreach (var b in binds) + if (storageId.HasValue) + query = query.Where(x => x.st != null && x.st.Id == storageId.Value); + if (areaId.HasValue) + query = query.Where(x => x.area != null && x.area.Id == areaId.Value); + if (!string.IsNullOrWhiteSpace(q)) { - if (!materials.TryGetValue(b.MaterialId, out var mat)) continue; - locations.TryGetValue(b.ContainerId, out var loc); - Storage? storage = null; - WarehouseArea? area = null; - if (loc != null && loc.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(loc.LocationId, out var sid)) - storages.TryGetValue(sid, out storage); - if (storage != null) areas.TryGetValue(storage.AreaId, out area); - containers.TryGetValue(b.ContainerId, out var ctn); - - if (storageId.HasValue && storage?.Id != storageId.Value) continue; - if (areaId.HasValue && area?.Id != areaId.Value) continue; - if (!string.IsNullOrWhiteSpace(q)) - { - var s = q.Trim(); - if (!(mat.Code.Contains(s) || mat.Name.Contains(s) || (ctn?.Code.Contains(s) ?? false) || (storage?.Code.Contains(s) ?? false))) - continue; - } - - rows.Add(new InventoryMaterialRow( - mat.Id, mat.Code, mat.Name, mat.Barcode, mat.TypeCode, - b.ContainerId, ctn?.Code ?? "", ctn?.Name ?? "", - storage?.Id, storage?.Code ?? "", storage?.Name ?? "", - area?.Id, area?.Code ?? "", area?.Name ?? "", - loc?.LocationType ?? "", b.BoundAt)); + var s = q.Trim(); + query = query.Where(x => + x.mat.Code.Contains(s) || x.mat.Name.Contains(s) || + (x.ctn != null && (x.ctn.Code.Contains(s) || x.ctn.Name.Contains(s))) || + (x.st != null && x.st.Code.Contains(s))); } - return rows.OrderBy(x => x.MaterialCode).ToList(); + return await query + .OrderBy(x => x.mat.Code) + .Select(x => new InventoryMaterialRow( + x.mat.Id, x.mat.Code, x.mat.Name, x.mat.Barcode, x.mat.TypeCode, + x.b.ContainerId, x.ctn != null ? x.ctn.Code : "", x.ctn != null ? x.ctn.Name : "", + x.st != null ? x.st.Id : null, x.st != null ? x.st.Code : "", x.st != null ? x.st.Name : "", + x.area != null ? x.area.Id : null, x.area != null ? x.area.Code : "", x.area != null ? x.area.Name : "", + x.loc != null ? x.loc.LocationType.ToString() : "", x.b.BoundAt)) + .ToListAsync(); } + /// + /// 库存事件:条件与 OrderBy/Take(500) 均下推;非法 eventType 直接返回空,避免全表拉取后再过滤。 + /// public async Task> StockEvents(string? eventType = null, Guid? materialId = null, Guid? containerId = null) { var query = _db.StockEvents.AsNoTracking().AsQueryable(); - if (!string.IsNullOrWhiteSpace(eventType)) query = query.Where(x => x.EventType == eventType); + if (!string.IsNullOrWhiteSpace(eventType)) + { + if (Enum.TryParse(eventType.Trim(), true, out var et)) + query = query.Where(x => x.EventType == et); + else + return []; + } if (materialId.HasValue) query = query.Where(x => x.MaterialId == materialId.Value); if (containerId.HasValue) query = query.Where(x => x.ContainerId == containerId.Value); - var rows = await query.ToListAsync(); - return rows.OrderByDescending(x => x.OperatedAt).Take(500).ToList(); + return await query.OrderByDescending(x => x.OperatedAt).Take(500).ToListAsync(); } public async Task SaveWarehouse(MasterDataRequest req, string actor) { Warehouse entity; - if (req.Id.HasValue) entity = await FindEditable(_db.Warehouses, req.Id.Value, req.Version); + if (req.Id.HasValue) entity = await FindEditable(req.Id.Value, req.Version); else { entity = new Warehouse(); StampCreate(entity, actor); _db.Warehouses.Add(entity); } - await EnsureUnique(_db.Warehouses, x => x.Code == req.Code && x.Id != entity.Id, "仓库编码已存在"); + await Repo().EnsureUniqueAsync(x => x.Code == req.Code && x.Id != entity.Id, "仓库编码已存在"); entity.Code = req.Code.Trim(); entity.Name = req.Name.Trim(); entity.Type = req.Type.TrimOr("Default"); entity.Enabled = req.Enabled; entity.SortOrder = req.SortOrder; ApplyCommon(entity, req, actor); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return entity; } @@ -150,43 +178,43 @@ public sealed class WmsService await _refs.EnsureWarehouseAsync(warehouseId); WarehouseArea entity; - if (req.Id.HasValue) entity = await FindEditable(_db.WarehouseAreas, req.Id.Value, req.Version); + if (req.Id.HasValue) entity = await FindEditable(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, "库区编码已存在"); + await Repo().EnsureUniqueAsync(x => x.Code == req.Code && x.Id != entity.Id, "库区编码已存在"); entity.WarehouseId = warehouseId; entity.Code = req.Code.Trim(); entity.Name = req.Name.Trim(); entity.Type = req.Type.TrimOr("Storage"); - entity.LayoutMode = AreaLayoutModes.All.Contains(req.LayoutMode) ? req.LayoutMode : AreaLayoutModes.Flat; + entity.LayoutMode = AreaLayoutModes.ParseOr(req.LayoutMode); entity.State = req.State.TrimOr("Default"); entity.Enabled = req.Enabled; entity.SortOrder = req.SortOrder; ApplyCommon(entity, req, actor); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return entity; } public async Task SaveStorage(StorageRequest req, string actor) { await _refs.EnsureAreaAsync(req.AreaId); - var kind = LocationKinds.All.Contains(req.LocationKind) ? req.LocationKind : LocationKinds.Station; + var kind = LocationKinds.ParseOr(req.LocationKind); if (kind == LocationKinds.Grid) await EnsureGridCoordUnique(req.AreaId, req.ColumnNo, req.LevelNo, req.DepthNo, req.Id); Storage entity; - if (req.Id.HasValue) entity = await FindEditable(_db.Storages, req.Id.Value, req.Version); + if (req.Id.HasValue) entity = await FindEditable(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, "库位编码已存在"); + await Repo().EnsureUniqueAsync(x => x.Code == req.Code && x.Id != entity.Id, "库位编码已存在"); entity.AreaId = req.AreaId; entity.Code = req.Code.Trim(); entity.Name = req.Name.Trim(); @@ -199,7 +227,7 @@ public sealed class WmsService entity.SiteCode = req.SiteCode.TrimOr(entity.SiteId); entity.Barcode = req.Barcode.TrimOr(""); entity.Capacity = 1; - var status = StorageStatuses.Normalize(req.Status.TrimOr(StorageStatuses.Empty)); + var status = StorageStatuses.ParseOr(req.Status); if (status == StorageStatuses.Disabled || req.Enabled == false) entity.Status = req.Enabled ? status : StorageStatuses.Disabled; else if (!string.IsNullOrWhiteSpace(req.Status) && StorageStatuses.All.Contains(status)) @@ -211,7 +239,7 @@ public sealed class WmsService entity.AllowOutbound = req.AllowOutbound; entity.Enabled = req.Enabled; ApplyCommon(entity, req, actor); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); if (entity.Status != StorageStatuses.Disabled) await SyncOccupancyStatus(storageId: entity.Id); return entity; @@ -262,28 +290,28 @@ public sealed class WmsService _db.Storages.Add(entity); created++; } - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return created; } public async Task SetStorageLock(Guid id, bool isLock, long? version, string actor) { - var entity = await FindEditable(_db.Storages, id, version); + var entity = await FindEditable(id, version); entity.IsLock = isLock; entity.UpdatedBy = actor; - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return entity; } public async Task SetStorageEnabled(Guid id, bool enabled, long? version, string actor) { - var entity = await FindEditable(_db.Storages, id, version); + var entity = await FindEditable(id, version); entity.Enabled = enabled; entity.Status = enabled ? (entity.Status == StorageStatuses.Disabled ? StorageStatuses.Empty : entity.Status) : StorageStatuses.Disabled; entity.UpdatedBy = actor; - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); if (enabled) await SyncOccupancyStatus(storageId: id); return entity; } @@ -291,27 +319,26 @@ public sealed class WmsService public async Task SaveContainer(ContainerRequest req, string actor) { Container entity; - if (req.Id.HasValue) entity = await FindEditable(_db.Containers, req.Id.Value, req.Version); + if (req.Id.HasValue) entity = await FindEditable(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, "容器编码已存在"); + await Repo().EnsureUniqueAsync(x => x.Code == req.Code && x.Id != entity.Id, "容器编码已存在"); entity.AreaId = req.AreaId; entity.Code = req.Code.Trim(); entity.Name = req.Name.Trim(); entity.ContainerType = req.Type.TrimOr("Box"); - var status = req.Status.TrimOr(ContainerStatuses.EmptyMaterial); - entity.Status = ContainerStatuses.All.Contains(status) ? status : ContainerStatuses.EmptyMaterial; + entity.Status = ContainerStatuses.ParseOr(req.Status); entity.Barcode = req.Barcode.TrimOr(""); entity.Length = req.Length; entity.Width = req.Width; entity.Height = req.Height; entity.Enabled = req.Enabled; ApplyCommon(entity, req, actor); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); await SyncOccupancyStatus(containerId: entity.Id); return entity; } @@ -319,14 +346,14 @@ public sealed class WmsService public async Task SaveMaterialType(MaterialTypeRequest req, string actor) { MaterialType entity; - if (req.Id.HasValue) entity = await FindEditable(_db.MaterialTypes, req.Id.Value, req.Version); + if (req.Id.HasValue) entity = await FindEditable(req.Id.Value, req.Version); else { entity = new MaterialType(); StampCreate(entity, actor); _db.MaterialTypes.Add(entity); } - await EnsureUnique(_db.MaterialTypes, x => x.Code == req.Code && x.Id != entity.Id, "物料类型编码已存在"); + await Repo().EnsureUniqueAsync(x => x.Code == req.Code && x.Id != entity.Id, "物料类型编码已存在"); entity.Code = req.Code.Trim(); entity.Name = req.Name.Trim(); entity.Spec = req.Spec.TrimOr(""); @@ -335,7 +362,7 @@ public sealed class WmsService entity.BarcodePrefix = req.BarcodePrefix.TrimOr(""); entity.Enabled = req.Enabled; ApplyCommon(entity, req, actor); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return entity; } @@ -344,16 +371,16 @@ public sealed class WmsService if (!string.IsNullOrWhiteSpace(req.TypeCode)) await _refs.EnsureMaterialTypeCodeAsync(req.TypeCode); Material entity; - if (req.Id.HasValue) entity = await FindEditable(_db.Materials, req.Id.Value, req.Version); + if (req.Id.HasValue) entity = await FindEditable(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, "物料编码已存在"); + await Repo().EnsureUniqueAsync(x => x.Code == req.Code && x.Id != entity.Id, "物料编码已存在"); if (!string.IsNullOrWhiteSpace(req.Barcode)) - await EnsureUnique(_db.Materials, x => x.Barcode == req.Barcode && x.Id != entity.Id, "物料条码已存在"); + await Repo().EnsureUniqueAsync(x => x.Barcode == req.Barcode && x.Id != entity.Id, "物料条码已存在"); entity.Code = req.Code.Trim(); entity.Name = req.Name.Trim(); entity.TypeCode = req.TypeCode.TrimOr(""); @@ -361,23 +388,22 @@ public sealed class WmsService entity.Spec = req.Spec.TrimOr(""); entity.Unit = req.Unit.TrimOr("pcs"); entity.Category = req.Category.TrimOr(""); - var life = req.LifecycleStatus.TrimOr(MaterialLifecycles.Active); - entity.LifecycleStatus = MaterialLifecycles.All.Contains(life) ? life : MaterialLifecycles.Active; + entity.LifecycleStatus = MaterialLifecycles.ParseOr(req.LifecycleStatus); entity.Enabled = req.Enabled; ApplyCommon(entity, req, actor); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return entity; } public async Task ArchiveMaterial(Guid id, long? version, string actor) { - var entity = await FindEditable(_db.Materials, id, version); + var entity = await FindEditable(id, version); if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == id)) throw new InvalidOperationException("物料仍在绑定中,不能归档"); entity.LifecycleStatus = MaterialLifecycles.Archived; entity.UnboundAt ??= DateTimeOffset.UtcNow; entity.UpdatedBy = actor; - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return entity; } @@ -396,13 +422,12 @@ public sealed class WmsService entity.DeletedBy = actor; entity.UpdatedBy = actor; } - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); } - public async Task DeleteEntity(Guid id, long? version, string actor) where T : EntityBase + public async Task DeleteEntity(Guid id, long? version, string actor) + where T : class, IEntity, ISoftDeletable, IVersioned, ILockable, IAuditable { - var set = _db.Set(); - var entity = await FindEditable(set, id, version); if (typeof(T) == typeof(WarehouseArea)) { if (await _db.Storages.AnyAsync(x => x.AreaId == id)) @@ -419,23 +444,24 @@ public sealed class WmsService if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == id)) throw new InvalidOperationException("物料仍在绑定中,不能删除"); } - entity.IsDeleted = true; - entity.DeletedAt = DateTimeOffset.UtcNow; - entity.DeletedBy = actor; - entity.UpdatedBy = actor; - await _db.SaveChangesAsync(); + + await Repo().SoftDeleteAsync(id, version); + await _uow.SaveChangesAsync(); } public async Task 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)) + if (!ContainerLocationStatuses.IsDefined(req.Status)) throw new InvalidOperationException("容器位置状态无效"); + var locationType = ContainerLocationTypes.ParseOr(req.LocationType); + var locationStatus = ContainerLocationStatuses.ParseOr(req.Status); + Guid? fromStorageId = null; Guid? toStorageId = null; - if (string.Equals(req.LocationType, ContainerLocationTypes.Storage, StringComparison.OrdinalIgnoreCase) && + if (locationType == ContainerLocationTypes.Storage && Guid.TryParse(req.LocationId, out var targetStorageId)) { toStorageId = targetStorageId; @@ -455,7 +481,9 @@ public sealed class WmsService var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == req.ContainerId); var before = current == null ? null : Snapshot(current); - if (before != null && before.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(before.LocationId, out var fs)) + if (before != null && + ContainerLocationTypes.EqualsString(ContainerLocationTypes.Storage, before.LocationType) && + Guid.TryParse(before.LocationId, out var fs)) fromStorageId = fs; var now = DateTimeOffset.UtcNow; @@ -467,15 +495,15 @@ public sealed class WmsService } else { - EnsureVersion(current, req.Version); - EnsureUnlocked(current); + await Repo().GetEditableAsync(current.Id, req.Version); } - current.LocationType = req.LocationType; + current.LocationType = locationType; current.LocationId = req.LocationId.Trim(); + current.StorageId = toStorageId; current.LocationCode = code; current.LocationName = name; - current.Status = req.Status; + current.Status = locationStatus; current.EnteredAt = req.EnteredAt ?? now; ApplyCommon(current, req, actor); @@ -486,7 +514,7 @@ public sealed class WmsService EventType = before == null ? "Bind" : "Transfer", FromLocationType = before?.LocationType ?? "", FromLocationId = before?.LocationId ?? "", - ToLocationType = current.LocationType, + ToLocationType = current.LocationType.ToString(), ToLocationId = current.LocationId, BeforeJson = before == null ? "{}" : JsonSerializer.Serialize(before, _json), AfterJson = JsonSerializer.Serialize(Snapshot(current), _json), @@ -499,14 +527,7 @@ public sealed class WmsService }); await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now); - try - { - await _db.SaveChangesAsync(); - } - catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex)) - { - throw new InvalidOperationException("目标库位已被其他容器占用,库位与容器为 1 对 1"); - } + await _uow.SaveChangesAsync(); await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId); await SyncOccupancyStatus(storageId: toStorageId); return current; @@ -516,7 +537,7 @@ public sealed class WmsService { var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == containerId) ?? throw new InvalidOperationException("容器当前位置不存在"); - EnsureUnlocked(current); + await Repo().GetEditableAsync(current.Id, null); Guid? fromStorageId = null; if (current.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(current.LocationId, out var fs)) fromStorageId = fs; @@ -527,7 +548,7 @@ public sealed class WmsService RelationId = current.Id, ContainerId = current.ContainerId, EventType = "Unbind", - FromLocationType = current.LocationType, + FromLocationType = current.LocationType.ToString(), FromLocationId = current.LocationId, BeforeJson = JsonSerializer.Serialize(before, _json), AfterJson = "{}", @@ -538,7 +559,7 @@ public sealed class WmsService }); await AddContainerMoveEventAsync(containerId, fromStorageId, null, actor, reason, now); _db.ContainerLocations.Remove(current); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); await SyncOccupancyStatus(containerId: containerId, storageId: fromStorageId); } @@ -585,20 +606,14 @@ public sealed class WmsService material.UnboundAt = null; await AddBindUnbindEventAsync(StockEventTypes.Bind, material, req.ContainerId, actor, req.Reason.TrimOr(""), now); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); await SyncOccupancyStatus(containerId: req.ContainerId); return current; } - /// 兼容旧接口:忽略数量,按实体绑定。 - public Task SaveContainerMaterial(ContainerMaterialRequest req, string actor) => - BindMaterial(new BindMaterialRequest(req.Id, req.Version, req.ContainerId, req.MaterialId, req.Source, req.Reason, req.IsLock, req.Remark, req.Extend), actor); - public async Task UnbindMaterial(Guid bindingId, string actor, string reason = "", bool archive = false) { - var current = await _db.ContainerMaterials.FirstOrDefaultAsync(x => x.Id == bindingId) - ?? throw new InvalidOperationException("绑定不存在"); - EnsureUnlocked(current); + var current = await Repo().GetEditableAsync(bindingId, null); var material = await _db.Materials.FirstOrDefaultAsync(x => x.Id == current.MaterialId); var before = Snapshot(current); var now = DateTimeOffset.UtcNow; @@ -627,7 +642,7 @@ public sealed class WmsService } _db.ContainerMaterials.Remove(current); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); await SyncOccupancyStatus(containerId: containerId); } @@ -673,7 +688,7 @@ public sealed class WmsService } } - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); } public async Task EnsureDefaultWarehouseAsync(string actor = "system") @@ -690,68 +705,10 @@ public sealed class WmsService }; StampCreate(wh, actor); _db.Warehouses.Add(wh); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return wh; } - public async Task MigrateLegacyAsync(string actor = "system") - { - var wh = await EnsureDefaultWarehouseAsync(actor); - var areas = await _db.WarehouseAreas.Where(x => x.WarehouseId == Guid.Empty).ToListAsync(); - foreach (var a in areas) - { - a.WarehouseId = wh.Id; - if (string.IsNullOrWhiteSpace(a.LayoutMode)) a.LayoutMode = AreaLayoutModes.Flat; - } - - var storages = await _db.Storages.ToListAsync(); - foreach (var s in storages) - { - if (string.IsNullOrWhiteSpace(s.LocationKind)) - s.LocationKind = LocationKinds.Station; - if (s.LevelNo <= 0) s.LevelNo = 1; - if (s.DepthNo <= 0) s.DepthNo = 1; - if (string.IsNullOrWhiteSpace(s.SiteCode)) s.SiteCode = s.SiteId; - s.Status = StorageStatuses.Normalize(s.Status) switch - { - StorageStatuses.Available or StorageStatuses.Idle => StorageStatuses.Empty, - StorageStatuses.Occupied => StorageStatuses.FullContainer, - var x => x - }; - if (!s.Enabled) s.Status = StorageStatuses.Disabled; - } - - var containers = await _db.Containers.ToListAsync(); - foreach (var c in containers) - { - c.Status = c.Status switch - { - "Idle" or "Empty" => ContainerStatuses.EmptyMaterial, - "Loaded" => ContainerStatuses.FullMaterial, - _ when ContainerStatuses.All.Contains(c.Status) => c.Status, - _ => ContainerStatuses.EmptyMaterial - }; - } - - var materials = await _db.Materials.Where(x => string.IsNullOrWhiteSpace(x.LifecycleStatus)).ToListAsync(); - foreach (var m in materials) - m.LifecycleStatus = MaterialLifecycles.Active; - - var binds = await _db.ContainerMaterials.ToListAsync(); - foreach (var b in binds) - { - b.Quantity = 1; - if (b.BoundAt == default) b.BoundAt = b.LoadedAt == default ? DateTimeOffset.UtcNow : b.LoadedAt; - if (!ContainerMaterialStatuses.ActiveBind.Contains(b.Status)) - b.Status = ContainerMaterialStatuses.Bound; - } - - await _db.SaveChangesAsync(); - - foreach (var s in storages.Where(x => x.Status != StorageStatuses.Disabled)) - await SyncOccupancyStatus(storageId: s.Id); - } - public async Task> LocationHistory(Guid? containerId = null) { var rows = await (containerId.HasValue @@ -780,13 +737,14 @@ public sealed class WmsService if (exists) throw new InvalidOperationException("同库区网格坐标已存在"); } - private async Task AddBindUnbindEventAsync(string eventType, Material material, Guid containerId, string actor, string reason, DateTimeOffset now) + private async Task AddBindUnbindEventAsync(StockEventType eventType, Material material, Guid containerId, string actor, string reason, DateTimeOffset now) { var ctn = await _db.Containers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == containerId); var loc = await _db.ContainerLocations.AsNoTracking().FirstOrDefaultAsync(x => x.ContainerId == containerId); Storage? storage = null; WarehouseArea? area = null; - if (loc is { LocationType: ContainerLocationTypes.Storage } && Guid.TryParse(loc.LocationId, out var sid)) + if (loc is { LocationType: ContainerLocationType.Storage } && + (loc.StorageId is { } sid || Guid.TryParse(loc.LocationId, out sid))) { storage = await _db.Storages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == sid); if (storage != null) @@ -850,19 +808,13 @@ public sealed class WmsService }); } - private async Task FindEditable(DbSet 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 IEditableRepository Repo() + where T : class, IEntity, ISoftDeletable, IVersioned, ILockable + => _services.GetRequiredService>(); - private static void EnsureVersion(EntityBase entity, long? version) - { - if (version.HasValue && entity.Version != version.Value) - throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试"); - } + private Task FindEditable(Guid id, long? version) + where T : class, IEntity, ISoftDeletable, IVersioned, ILockable + => Repo().GetEditableAsync(id, version); private static bool IsUniqueConstraintViolation(DbUpdateException ex) { @@ -878,18 +830,13 @@ public sealed class WmsService return false; } - private static void EnsureUnlocked(EntityBase entity) - { - if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改"); - } - - private static void StampCreate(EntityBase entity, string actor) + private static void StampCreate(AggregateRoot entity, string actor) { entity.CreatedBy = actor; entity.UpdatedBy = actor; } - private void ApplyCommon(EntityBase entity, CommonRequest req, string actor) + private void ApplyCommon(AggregateRoot entity, CommonRequest req, string actor) { entity.IsLock = req.IsLock; entity.Remark = req.Remark.TrimOr(""); @@ -897,11 +844,6 @@ public sealed class WmsService entity.UpdatedBy = actor; } - private static async Task EnsureUnique(IQueryable query, System.Linq.Expressions.Expression> predicate, string message) - { - if (await query.AnyAsync(predicate)) throw new InvalidOperationException(message); - } - private string NormalizeExtend(string? extend) { if (string.IsNullOrWhiteSpace(extend)) return "{}"; @@ -930,73 +872,14 @@ public sealed class WmsService } 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); + x.Id, x.ContainerId, x.LocationType.ToString(), x.LocationId, x.LocationCode, x.LocationName, + x.Status.ToString(), 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.BoundAt, x.LoadedAt, x.UnloadedAt, x.Version); + x.Id, x.ContainerId, x.MaterialId, x.Quantity, x.BatchNo, x.SerialNo, x.Status.ToString(), + x.BoundAt, x.LoadedAt, x.UnloadedAt, x.Version); } -public sealed record InventoryMaterialRow( - Guid MaterialId, string MaterialCode, string MaterialName, string MaterialBarcode, string TypeCode, - Guid ContainerId, string ContainerCode, string ContainerName, - Guid? StorageId, string StorageCode, string StorageName, - Guid? AreaId, string AreaCode, string AreaName, - string LocationType, DateTimeOffset BoundAt); - -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 BoundAt, 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 AreaRequest( - Guid? Id, long? Version, Guid? WarehouseId, string Code, string Name, string Type, string LayoutMode, string State, - 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 LocationKind, - int ColumnNo, int LevelNo, int DepthNo, string SiteId, string SiteCode, string Barcode, int Capacity, - string Status, string Usage, int Priority, string ZoneCode, bool AllowInbound, bool AllowOutbound, bool Enabled, - bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend); - -public sealed record GenerateBinsRequest(int ColumnFrom, int ColumnTo, int LevelFrom, int LevelTo, int DepthFrom, int DepthTo, string? CodePattern); - -public sealed record ContainerRequest( - Guid? Id, long? Version, Guid? AreaId, string Code, string Name, string Type, string Status, string Barcode, - double Length, double Width, double Height, bool Enabled, bool IsLock, string Remark, string Extend) - : CommonRequest(Id, Version, IsLock, Remark, Extend); - -public sealed record MaterialTypeRequest( - Guid? Id, long? Version, string Code, string Name, string Spec, string Unit, string Category, string BarcodePrefix, - 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 TypeCode, string Barcode, string Spec, string Unit, - string Category, string LifecycleStatus, 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 BindMaterialRequest( - Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, 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) => diff --git a/MiGu.Server/Wms/WmsStatusAliases.cs b/MiGu.Server/Wms/WmsStatusAliases.cs new file mode 100644 index 0000000..ad4578e --- /dev/null +++ b/MiGu.Server/Wms/WmsStatusAliases.cs @@ -0,0 +1,18 @@ +// Server 侧状态别名:把 MiGu.DB 的复数辅助类导入为短名,兼容业务代码中的 StorageStatuses / LocationKinds 等写法。 +// 枚举类型本身通过下方 global using Domains 命名空间直接可用(如 StorageStatus、LocationKind)。 +global using MiGu.DB.Domains.Wms; +global using MiGu.DB.Domains.Transport; +global using AreaLayoutModes = MiGu.DB.Domains.Wms.AreaLayoutModes; +global using StorageTypeCodes = MiGu.DB.Domains.Wms.StorageTypeCodes; +global using LocationKinds = MiGu.DB.Domains.Wms.LocationKinds; +global using StorageStatuses = MiGu.DB.Domains.Wms.StorageStatuses; +global using ContainerStatuses = MiGu.DB.Domains.Wms.ContainerStatuses; +global using MaterialLifecycles = MiGu.DB.Domains.Wms.MaterialLifecycles; +global using ContainerLocationTypes = MiGu.DB.Domains.Wms.ContainerLocationTypes; +global using ContainerLocationStatuses = MiGu.DB.Domains.Wms.ContainerLocationStatuses; +global using ContainerMaterialStatuses = MiGu.DB.Domains.Wms.ContainerMaterialStatuses; +global using StockEventTypes = MiGu.DB.Domains.Wms.StockEventTypes; +global using WmsDefaults = MiGu.DB.Domains.Wms.WmsDefaults; +global using WmsTransportTaskStatuses = MiGu.DB.Domains.Transport.WmsTransportTaskStatuses; +global using WmsTransportTriggerTypes = MiGu.DB.Domains.Transport.WmsTransportTriggerTypes; +global using WmsReservationStatuses = MiGu.DB.Domains.Transport.WmsReservationStatuses; diff --git a/MiGu.Server/Wms/WmsTransportPlanner.cs b/MiGu.Server/Wms/WmsTransportPlanner.cs index e01e3bd..926b532 100644 --- a/MiGu.Server/Wms/WmsTransportPlanner.cs +++ b/MiGu.Server/Wms/WmsTransportPlanner.cs @@ -6,18 +6,19 @@ namespace MiGu.Server.Wms; public sealed class WmsTransportPlanner { - private readonly PlatformDbContext _db; + private readonly MiGuDbContext _db; private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web); - public WmsTransportPlanner(PlatformDbContext db) => _db = db; + public WmsTransportPlanner(MiGuDbContext db) => _db = db; public async Task PreviewAsync(WmsTransportRequest request) { - if (!WmsTransportTriggerTypes.All.Contains(request.TriggerType)) + if (!WmsTransportTriggerTypes.IsDefined(request.TriggerType)) throw new InvalidOperationException("触发类型无效"); + var trigger = WmsTransportTriggerTypes.ParseOr(request.TriggerType); var ctx = await BuildContextAsync(); - var rules = await LoadRulesAsync(request); + var rules = await LoadRulesAsync(request, trigger); var ruleResults = new List(); var candidates = new List(); @@ -26,7 +27,7 @@ public sealed class WmsTransportPlanner var sourceSelector = TransportSelectorParser.ParseSelector(rule.SourceSelectorJson); var targetSelector = TransportSelectorParser.ParseSelector(rule.TargetSelectorJson); var options = TransportSelectorParser.ParseTaskOptions(rule.TaskOptionsJson); - var ruleCandidates = BuildCandidates(ctx, request, rule, sourceSelector, targetSelector, options); + var ruleCandidates = BuildCandidates(ctx, request, trigger, rule, sourceSelector, targetSelector, options); var rejectReasons = ruleCandidates.Count == 0 ? new List { "未找到满足条件的起点/终点组合" } : new List(); @@ -52,10 +53,10 @@ public sealed class WmsTransportPlanner return preview.Candidates.FirstOrDefault(); } - private async Task> LoadRulesAsync(WmsTransportRequest request) + private async Task> LoadRulesAsync(WmsTransportRequest request, WmsTransportTriggerType trigger) { var query = _db.WmsTransportRules.AsNoTracking() - .Where(x => x.Enabled && x.TriggerType == request.TriggerType); + .Where(x => x.Enabled && x.TriggerType == trigger); if (request.RuleId.HasValue) query = query.Where(x => x.Id == request.RuleId.Value); return await query.OrderByDescending(x => x.Priority).ThenBy(x => x.Code).ToListAsync(); @@ -97,17 +98,18 @@ public sealed class WmsTransportPlanner private List BuildCandidates( PlannerContext ctx, WmsTransportRequest request, + WmsTransportTriggerType trigger, WmsTransportRule rule, TransportSelector sourceSelector, TransportSelector targetSelector, TransportTaskOptions options) { var results = new List(); - var sources = BuildSourceCandidates(ctx, request, sourceSelector, options); + var sources = BuildSourceCandidates(ctx, request, trigger, sourceSelector, options); foreach (var src in sources) { - var targets = BuildTargetCandidates(ctx, request, targetSelector, options, src.StorageId); + var targets = BuildTargetCandidates(ctx, request, trigger, targetSelector, options, src.StorageId); foreach (var tgt in targets) { var score = rule.Priority * 1000 + src.Score + tgt.Score; @@ -127,6 +129,7 @@ public sealed class WmsTransportPlanner private List BuildSourceCandidates( PlannerContext ctx, WmsTransportRequest request, + WmsTransportTriggerType trigger, TransportSelector selector, TransportTaskOptions options) { @@ -148,7 +151,7 @@ public sealed class WmsTransportPlanner continue; if (request.RequestSiteId is { Length: > 0 } siteId && - string.Equals(request.TriggerType, WmsTransportTriggerTypes.FinishedGoodsOffline, StringComparison.OrdinalIgnoreCase) && + trigger == WmsTransportTriggerTypes.FinishedGoodsOffline && !string.Equals(storage.SiteId, siteId, StringComparison.OrdinalIgnoreCase)) continue; @@ -165,6 +168,7 @@ public sealed class WmsTransportPlanner private List BuildTargetCandidates( PlannerContext ctx, WmsTransportRequest request, + WmsTransportTriggerType trigger, TransportSelector selector, TransportTaskOptions options, Guid sourceStorageId) @@ -179,7 +183,7 @@ public sealed class WmsTransportPlanner if (ctx.ReservedTargetStorages.Contains(storage.Id)) continue; if (request.RequestSiteId is { Length: > 0 } siteId && - string.Equals(request.TriggerType, WmsTransportTriggerTypes.MaterialCall, StringComparison.OrdinalIgnoreCase) && + trigger == WmsTransportTriggerTypes.MaterialCall && selector.Storage?.SiteIds is { Count: 0 } && !string.Equals(storage.SiteId, siteId, StringComparison.OrdinalIgnoreCase)) continue; @@ -203,7 +207,8 @@ public sealed class WmsTransportPlanner { if (request.MaterialId.HasValue && row.MaterialId != request.MaterialId.Value) continue; if (filter?.MaterialIds is { Count: > 0 } ids && !ids.Contains(row.MaterialId.ToString("D"))) continue; - if (filter?.StatusIn is { Count: > 0 } st && !st.Contains(row.Status, StringComparer.OrdinalIgnoreCase)) continue; + if (filter?.StatusIn is { Count: > 0 } st && + !st.Contains(row.Status.ToString(), StringComparer.OrdinalIgnoreCase)) continue; if (filter?.MinQuantity is { } min && row.Quantity < min) continue; if (request.Quantity is { } reqQty && row.Quantity < reqQty) continue; if (request.BatchNo is { Length: > 0 } batch && !string.Equals(row.BatchNo, batch, StringComparison.OrdinalIgnoreCase)) continue; @@ -223,7 +228,8 @@ public sealed class WmsTransportPlanner if (filter.AreaIds is { Count: > 0 } && !filter.AreaIds.Contains(storage.AreaId.ToString("D"))) return false; if (filter.StorageTypes is { Count: > 0 } && !filter.StorageTypes.Contains(storage.StorageType, StringComparer.OrdinalIgnoreCase)) return false; if (filter.ZoneCodes is { Count: > 0 } && !filter.ZoneCodes.Contains(storage.ZoneCode, StringComparer.OrdinalIgnoreCase)) return false; - if (filter.StatusIn is { Count: > 0 } && !filter.StatusIn.Contains(storage.Status, StringComparer.OrdinalIgnoreCase)) return false; + if (filter.StatusIn is { Count: > 0 } && + !filter.StatusIn.Contains(storage.Status.ToString(), StringComparer.OrdinalIgnoreCase)) return false; if (filter.SiteIds is { Count: > 0 } && !filter.SiteIds.Contains(storage.SiteId, StringComparer.OrdinalIgnoreCase)) return false; if (filter.AllowInbound == true && !storage.AllowInbound) return false; if (filter.AllowOutbound == true && !storage.AllowOutbound) return false; @@ -239,7 +245,8 @@ public sealed class WmsTransportPlanner { if (filter == null) return true; if (filter.ContainerTypes is { Count: > 0 } && !filter.ContainerTypes.Contains(container.ContainerType, StringComparer.OrdinalIgnoreCase)) return false; - if (filter.StatusIn is { Count: > 0 } && !filter.StatusIn.Contains(container.Status, StringComparer.OrdinalIgnoreCase)) return false; + if (filter.StatusIn is { Count: > 0 } && + !filter.StatusIn.Contains(container.Status.ToString(), StringComparer.OrdinalIgnoreCase)) return false; if (filter.ExcludeReserved && (ctx.ReservedContainers.Contains(container.Id) || ctx.TaskReservedContainers.Contains(container.Id))) return false; diff --git a/MiGu.Server/Wms/WmsTransportRuleService.cs b/MiGu.Server/Wms/WmsTransportRuleService.cs index e69c2cf..fbd6fbc 100644 --- a/MiGu.Server/Wms/WmsTransportRuleService.cs +++ b/MiGu.Server/Wms/WmsTransportRuleService.cs @@ -1,19 +1,33 @@ using Microsoft.EntityFrameworkCore; +using MiGu.DB.Abstractions.Exceptions; +using MiGu.DB.Abstractions.Persistence; using MiGu.Server.Persistence; namespace MiGu.Server.Wms; public sealed class WmsTransportRuleService { - private readonly PlatformDbContext _db; + private readonly MiGuDbContext _db; + private readonly IEditableRepository _rules; + private readonly IUnitOfWork _uow; - public WmsTransportRuleService(PlatformDbContext db) => _db = db; + public WmsTransportRuleService( + MiGuDbContext db, + IEditableRepository rules, + IUnitOfWork uow) + { + _db = db; + _rules = rules; + _uow = uow; + } public Task> ListAsync(string? triggerType = null, string? q = null) { var query = _db.WmsTransportRules.AsNoTracking().OrderByDescending(x => x.Priority).ThenBy(x => x.Code).AsQueryable(); - if (!string.IsNullOrWhiteSpace(triggerType)) - query = query.Where(x => x.TriggerType == triggerType); + if (!string.IsNullOrWhiteSpace(triggerType) && + Enum.TryParse(triggerType.Trim(), true, out var tt) && + WmsTransportTriggerTypes.All.Contains(tt)) + query = query.Where(x => x.TriggerType == tt); if (!string.IsNullOrWhiteSpace(q)) { var s = q.Trim(); @@ -24,12 +38,12 @@ public sealed class WmsTransportRuleService public async Task SaveAsync(TransportRuleRequest req, string actor) { - if (!WmsTransportTriggerTypes.All.Contains(req.TriggerType)) + if (!WmsTransportTriggerTypes.IsDefined(req.TriggerType)) throw new InvalidOperationException("触发类型无效"); WmsTransportRule entity; if (req.Id.HasValue) - entity = await FindEditable(req.Id.Value, req.Version); + entity = await _rules.GetEditableAsync(req.Id.Value, req.Version); else { entity = new WmsTransportRule(); @@ -37,53 +51,33 @@ public sealed class WmsTransportRuleService _db.WmsTransportRules.Add(entity); } - await EnsureUniqueCode(req.Code, entity.Id); + await _rules.EnsureUniqueAsync(x => x.Code == req.Code.Trim() && x.Id != entity.Id, "规则编码已存在"); entity.Code = req.Code.Trim(); entity.Name = req.Name.Trim(); - entity.TriggerType = req.TriggerType.Trim(); + entity.TriggerType = WmsTransportTriggerTypes.ParseOr(req.TriggerType); entity.Enabled = req.Enabled; entity.Priority = req.Priority; entity.SourceSelectorJson = TransportSelectorParser.NormalizeSelectorJson(req.SourceSelectorJson); entity.TargetSelectorJson = TransportSelectorParser.NormalizeSelectorJson(req.TargetSelectorJson); entity.TaskOptionsJson = TransportSelectorParser.NormalizeTaskOptionsJson(req.TaskOptionsJson); ApplyCommon(entity, req, actor); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return entity; } public async Task DeleteAsync(Guid id, long? version, string actor) { - var entity = await FindEditable(id, version); - entity.IsDeleted = true; - entity.DeletedAt = DateTimeOffset.UtcNow; - entity.DeletedBy = actor; - entity.UpdatedBy = actor; - await _db.SaveChangesAsync(); + await _rules.SoftDeleteAsync(id, version); + await _uow.SaveChangesAsync(); } - private async Task FindEditable(Guid id, long? version) - { - var entity = await _db.WmsTransportRules.FirstOrDefaultAsync(x => x.Id == id) - ?? throw new InvalidOperationException("规则不存在"); - if (version.HasValue && entity.Version != version.Value) - throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试"); - if (entity.IsLock) throw new InvalidOperationException("规则已锁定,不能修改"); - return entity; - } - - private async Task EnsureUniqueCode(string code, Guid id) - { - if (await _db.WmsTransportRules.AnyAsync(x => x.Code == code.Trim() && x.Id != id)) - throw new InvalidOperationException("规则编码已存在"); - } - - private static void StampCreate(EntityBase entity, string actor) + private static void StampCreate(AggregateRoot entity, string actor) { entity.CreatedBy = actor; entity.UpdatedBy = actor; } - private static void ApplyCommon(EntityBase entity, CommonRequest req, string actor) + private static void ApplyCommon(AggregateRoot entity, CommonRequest req, string actor) { entity.IsLock = req.IsLock; entity.Remark = req.Remark.TrimOr(""); diff --git a/MiGu.Server/Wms/WmsTransportTaskService.cs b/MiGu.Server/Wms/WmsTransportTaskService.cs index 64bfb34..c387f2f 100644 --- a/MiGu.Server/Wms/WmsTransportTaskService.cs +++ b/MiGu.Server/Wms/WmsTransportTaskService.cs @@ -6,14 +6,14 @@ namespace MiGu.Server.Wms; public sealed class WmsTransportTaskService { - private readonly PlatformDbContext _db; + private readonly MiGuDbContext _db; private readonly WmsTransportPlanner _planner; private readonly WmsService _wms; private readonly IWmsDispatchAdapter _dispatch; private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web); public WmsTransportTaskService( - PlatformDbContext db, + MiGuDbContext db, WmsTransportPlanner planner, WmsService wms, IWmsDispatchAdapter dispatch) @@ -27,12 +27,13 @@ public sealed class WmsTransportTaskService public async Task> ListAsync(string? status = null, string? triggerType = null) { var query = _db.WmsTransportTasks.AsNoTracking().AsQueryable(); - if (!string.IsNullOrWhiteSpace(status)) - query = query.Where(x => x.Status == status); + if (!string.IsNullOrWhiteSpace(status) && + Enum.TryParse(status.Trim(), true, out var st) && + WmsTransportTaskStatuses.All.Contains(st)) + query = query.Where(x => x.Status == st); if (!string.IsNullOrWhiteSpace(triggerType)) query = query.Where(x => x.BusinessType == triggerType); - var rows = await query.ToListAsync(); - return rows.OrderByDescending(x => x.CreatedAt).Take(500).ToList(); + return await query.OrderByDescending(x => x.CreatedAt).Take(500).ToListAsync(); } public Task PreviewCandidatesAsync(WmsTransportRequest request) => @@ -80,7 +81,7 @@ public sealed class WmsTransportTaskService if (status == WmsTransportTaskStatuses.Reserved) await CreateReservationAsync(task, options, actor); - await AppendHistoryAsync(task.Id, "", task.Status, actor, request.Reason, "", task.SnapshotJson); + await AppendHistoryAsync(task.Id, "", task.Status.ToString(), actor, request.Reason, "", task.SnapshotJson); await tx.CommitAsync(); return task; } @@ -147,8 +148,8 @@ public sealed class WmsTransportTaskService try { await _wms.BindOrTransferLocation(new ContainerLocationRequest( - null, null, task.ContainerId, ContainerLocationTypes.Storage, - task.TargetStorageId.ToString("D"), ContainerLocationStatuses.Active, + null, null, task.ContainerId, ContainerLocationTypes.Storage.ToString(), + task.TargetStorageId.ToString("D"), ContainerLocationStatuses.Active.ToString(), DateTimeOffset.UtcNow, "TransportTask", reason, false, "", "{}"), actor); await ReleaseReservationsAsync(task.Id); @@ -284,13 +285,13 @@ public sealed class WmsTransportTaskService await _db.SaveChangesAsync(); } - private async Task ChangeStatusAsync(WmsTransportTask task, string toStatus, string actor, string reason) + private async Task ChangeStatusAsync(WmsTransportTask task, WmsTransportTaskStatus toStatus, string actor, string reason) { var from = task.Status; task.Status = toStatus; task.UpdatedBy = actor; await _db.SaveChangesAsync(); - await AppendHistoryAsync(task.Id, from, toStatus, actor, reason, task.ErrorMessage, task.SnapshotJson); + await AppendHistoryAsync(task.Id, from.ToString(), toStatus.ToString(), actor, reason, task.ErrorMessage, task.SnapshotJson); } private async Task AppendHistoryAsync(Guid taskId, string from, string to, string actor, string reason, string error, string snapshot) @@ -330,7 +331,7 @@ public sealed class WmsTransportTaskService GeneratedAt = DateTimeOffset.UtcNow }; - private static void StampCreate(EntityBase entity, string actor) + private static void StampCreate(AggregateRoot entity, string actor) { entity.CreatedBy = actor; entity.UpdatedBy = actor; diff --git a/MiGu.Server/appsettings.Development.json b/MiGu.Server/appsettings.Development.json index 879581e..4d88230 100644 --- a/MiGu.Server/appsettings.Development.json +++ b/MiGu.Server/appsettings.Development.json @@ -6,6 +6,9 @@ "Yarp": "Debug" } }, + "Database": { + "SchemaMode": "EnsureCreated" + }, "SimpleLite": { "Enabled": true, "ExecutablePath": "D:\\Code\\Products\\MIGU2.0\\SimpleLite\\SimpleLite.exe", diff --git a/MiGu.Server/appsettings.json b/MiGu.Server/appsettings.json index 346519e..c0c9dac 100644 --- a/MiGu.Server/appsettings.json +++ b/MiGu.Server/appsettings.json @@ -132,5 +132,13 @@ } } } + }, + "Database": { + "Provider": "sqlite", + "SchemaMode": "Migrate", + "ApplyDataMigratorsOnStartup": true + }, + "ConnectionStrings": { + "Platform": "Data Source=data/platform.db" } } diff --git a/frontends/apps/simple-platform-vue/src/api/http.ts b/frontends/apps/simple-platform-vue/src/api/http.ts index 7b37a87..c6b40a3 100644 --- a/frontends/apps/simple-platform-vue/src/api/http.ts +++ b/frontends/apps/simple-platform-vue/src/api/http.ts @@ -13,6 +13,10 @@ const http: AxiosInstance = axios.create({ }) http.interceptors.request.use((config: InternalAxiosRequestConfig) => { + // FormData 必须由浏览器自动带 multipart boundary;清掉默认 application/json。 + if (typeof FormData !== 'undefined' && config.data instanceof FormData) { + config.headers.delete('Content-Type') + } // 双轨:优先用 localStorage 里的 token(过渡期 fallback),Cookie 会自动带; // 后端首先看 Authorization: Bearer,没有再看 Cookie,两路任一通过即可。 const token = localStorage.getItem('simple.auth.token') diff --git a/frontends/apps/simple-platform-vue/src/api/ota.ts b/frontends/apps/simple-platform-vue/src/api/ota.ts index f74f6f0..2537556 100644 --- a/frontends/apps/simple-platform-vue/src/api/ota.ts +++ b/frontends/apps/simple-platform-vue/src/api/ota.ts @@ -47,7 +47,6 @@ export async function uploadOtaPackage(file: File): Promise { const form = new FormData() form.append('file', file) const { data } = await http.post('/ota/packages/upload', form, { - headers: { 'Content-Type': 'multipart/form-data' }, timeout: 300000 }) return data @@ -116,7 +115,6 @@ export async function pushOtaCustomFile(opts: { form.append('restartOps', JSON.stringify(opts.restartOps.length ? opts.restartOps : [-1])) for (const f of opts.files) form.append('files', f) const { data } = await http.post('/ota/custom-file', form, { - headers: { 'Content-Type': 'multipart/form-data' }, timeout: 300000 }) return data diff --git a/frontends/apps/simple-platform-vue/src/types/wms.ts b/frontends/apps/simple-platform-vue/src/types/wms.ts index 7e664a5..9b02643 100644 --- a/frontends/apps/simple-platform-vue/src/types/wms.ts +++ b/frontends/apps/simple-platform-vue/src/types/wms.ts @@ -206,7 +206,6 @@ export interface MasterDataPayload { code: string name: string type: string - status: string enabled: boolean sortOrder: number isLock: boolean diff --git a/frontends/apps/simple-platform-vue/src/views/admin/WarehouseManagementView.vue b/frontends/apps/simple-platform-vue/src/views/admin/WarehouseManagementView.vue index 12794ee..1eb5f81 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/WarehouseManagementView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/WarehouseManagementView.vue @@ -563,7 +563,7 @@ async function submitDialog() { ...masterForm, barcode: containerBarcode.value, length: 0, width: 0, height: 0, - status: 'EmptyMaterial' + status: containers.value.find((c) => c.id === masterForm.id)?.status ?? 'EmptyMaterial' }) } else if (kind === 'storage') await wmsApi.saveStorage(storageForm) @@ -683,7 +683,6 @@ function baseMaster(row?: WarehouseArea | Container): MasterDataPayload { code: row?.code ?? '', name: row?.name ?? '', type: 'type' in (row ?? {}) ? (row as WarehouseArea).type : (row as Container | undefined)?.containerType ?? 'Box', - status: (row as Container | undefined)?.status ?? 'Idle', enabled: row?.enabled ?? true, sortOrder: (row as WarehouseArea | undefined)?.sortOrder ?? 0 } diff --git a/frontends/apps/simple-platform-vue/src/views/shared/VehicleHubView.vue b/frontends/apps/simple-platform-vue/src/views/shared/VehicleHubView.vue index 1be40bc..e26d774 100644 --- a/frontends/apps/simple-platform-vue/src/views/shared/VehicleHubView.vue +++ b/frontends/apps/simple-platform-vue/src/views/shared/VehicleHubView.vue @@ -148,7 +148,7 @@ const auth = useAuthStore() const canWrite = computed(() => { if (auth.scope === 'Platform') return true const ops = auth.effectivePermissions?.allowedOps ?? [] - return ops.includes('*') || ops.some((o) => o === 'ops.ota' || o.startsWith('ops.ota.')) + return ops.includes('*') || ops.some((o) => o === 'ops.ota' || o === 'ops.ota.write' || o.startsWith('ops.ota.')) }) // Tab 与 URL ?tab= 同步,支持深链接(旧 /config/vehicle、/config/fleet 已下线,统一进车辆运维)。 diff --git a/frontends/apps/simple-platform-vue/src/views/shared/ota/OtaSettingsPane.vue b/frontends/apps/simple-platform-vue/src/views/shared/ota/OtaSettingsPane.vue index d9673dd..88c1815 100644 --- a/frontends/apps/simple-platform-vue/src/views/shared/ota/OtaSettingsPane.vue +++ b/frontends/apps/simple-platform-vue/src/views/shared/ota/OtaSettingsPane.vue @@ -21,18 +21,10 @@ 跳过该车 - 仍允许(需确认) + 仍允许下发 -

备份

- - - - - - -

展示