From 51c3fc1994e4991793ac9520a98c34bc49cda924 Mon Sep 17 00:00:00 2001 From: ArtoriasWu Date: Mon, 27 Jul 2026 14:26:04 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=B1=82=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=B8=BA=E7=8B=AC=E7=AB=8B=20MiGu.DB=20=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B=E5=B9=B6=E4=BC=98=E5=8C=96=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MiGu.DB 项目,迁移所有领域实体与枚举,统一模型约定 - 实现 Entity/Repository/UoW/Provider/Exception 等接口与实现 - 支持数据修补机制,完善 Sqlite 初始迁移与数据库管理 - Server 侧移除 EF Core 相关,依赖 MiGu.DB,PlatformPersistence 适配 - 业务服务注入 UoW/Repository,状态字段统一用 enum 及辅助类 - 统一异常处理,Controller 映射 HTTP 状态码 - 配置项与文档补充数据库启动、SchemaMode、迁移说明 - 新增 GlobalUsings.Db.cs、WmsStatusAliases.cs 简化类型引用 - 新增 HttpActorContextMiddleware 支持操作者上下文一致性 - 新增 MiGuDbContextModelSnapshot 追踪数据库结构 - 优化代码结构,解耦领域与持久层,提升扩展性与安全性 --- .gitignore | 2 + MiGu.DB/Abstractions/Entities/Capabilities.cs | 45 + MiGu.DB/Abstractions/Exceptions/Exceptions.cs | 42 + MiGu.DB/Abstractions/Modules/IEntityModule.cs | 10 + .../Persistence/PersistenceContracts.cs | 32 + .../Providers/IDbProviderSetup.cs | 11 + .../Abstractions/Runtime/RuntimeContracts.cs | 44 + MiGu.DB/Domains/.gitkeep | 0 .../Dashboard/UserDashboardShortcut.cs | 3 +- MiGu.DB/Domains/Migrators/DataMigrators.cs | 105 + MiGu.DB/Domains/SimpleFields/SimpleField.cs | 32 + .../Domains/Transport/TransportEntities.cs | 104 + MiGu.DB/Domains/Transport/TransportEnums.cs | 35 + .../TransportAndOtherConfigurations.cs | 112 ++ MiGu.DB/Domains/Wms/WmsConfigurations.cs | 170 ++ MiGu.DB/Domains/Wms/WmsEntities.cs | 174 ++ MiGu.DB/Domains/Wms/WmsEnums.cs | 66 + MiGu.DB/Domains/Wms/WmsStatusHelpers.cs | 116 ++ MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs | 28 + MiGu.DB/Kernel/Context/MiGuDbContext.cs | 25 + MiGu.DB/Kernel/Conventions/Conventions.cs | 167 ++ MiGu.DB/Kernel/Design/MiGuDbContextFactory.cs | 20 + MiGu.DB/Kernel/Entities/EntityBases.cs | 46 + .../Hosting/ServiceCollectionExtensions.cs | 224 +++ MiGu.DB/Kernel/Providers/ProviderSetups.cs | 47 + MiGu.DB/Kernel/Repositories/Repositories.cs | 116 ++ MiGu.DB/MIGRATIONS.md | 221 +++ MiGu.DB/MiGu.DB.csproj | 30 + MiGu.DB/Migrations/Sqlite/.gitkeep | 0 ...20260727011546_InitialPlatform.Designer.cs | 1686 +++++++++++++++++ .../Sqlite/20260727011546_InitialPlatform.cs | 754 ++++++++ .../Sqlite/MiGuDbContextModelSnapshot.cs | 1683 ++++++++++++++++ MiGu.DB/README.md | 57 + MiGu.DB/build-out.txt | Bin 0 -> 46784 bytes MiGu.Server.sln | 6 + MiGu.Server/Controllers/WmsController.cs | 23 +- MiGu.Server/GlobalUsings.Db.cs | 19 + MiGu.Server/MiGu.Server.csproj | 13 +- MiGu.Server/Persistence/EntityBase.cs | 17 - .../Persistence/HttpActorContextMiddleware.cs | 36 + MiGu.Server/Persistence/PlatformDbContext.cs | 253 --- .../Persistence/PlatformPersistence.cs | 468 +---- MiGu.Server/Program.cs | 1 + MiGu.Server/README.md | 86 +- .../SimpleFields/SimpleFieldDateTime.cs | 21 - MiGu.Server/SimpleFields/SimpleFieldModels.cs | 67 - .../SimpleFields/SimpleFieldService.cs | 1 + MiGu.Server/Wms/WmsModels.cs | 390 +--- MiGu.Server/Wms/WmsReferenceValidator.cs | 4 +- MiGu.Server/Wms/WmsService.cs | 313 ++- MiGu.Server/Wms/WmsStatusAliases.cs | 19 + MiGu.Server/Wms/WmsTransportPlanner.cs | 31 +- MiGu.Server/Wms/WmsTransportRuleService.cs | 54 +- MiGu.Server/Wms/WmsTransportTaskService.cs | 19 +- MiGu.Server/appsettings.Development.json | 3 + MiGu.Server/appsettings.json | 8 + MiGu.Server/data/platform.db | Bin 462848 -> 4096 bytes 57 files changed, 6663 insertions(+), 1396 deletions(-) create mode 100644 MiGu.DB/Abstractions/Entities/Capabilities.cs create mode 100644 MiGu.DB/Abstractions/Exceptions/Exceptions.cs create mode 100644 MiGu.DB/Abstractions/Modules/IEntityModule.cs create mode 100644 MiGu.DB/Abstractions/Persistence/PersistenceContracts.cs create mode 100644 MiGu.DB/Abstractions/Providers/IDbProviderSetup.cs create mode 100644 MiGu.DB/Abstractions/Runtime/RuntimeContracts.cs create mode 100644 MiGu.DB/Domains/.gitkeep rename {MiGu.Server => MiGu.DB/Domains}/Dashboard/UserDashboardShortcut.cs (65%) create mode 100644 MiGu.DB/Domains/Migrators/DataMigrators.cs create mode 100644 MiGu.DB/Domains/SimpleFields/SimpleField.cs create mode 100644 MiGu.DB/Domains/Transport/TransportEntities.cs create mode 100644 MiGu.DB/Domains/Transport/TransportEnums.cs create mode 100644 MiGu.DB/Domains/TransportAndOtherConfigurations.cs create mode 100644 MiGu.DB/Domains/Wms/WmsConfigurations.cs create mode 100644 MiGu.DB/Domains/Wms/WmsEntities.cs create mode 100644 MiGu.DB/Domains/Wms/WmsEnums.cs create mode 100644 MiGu.DB/Domains/Wms/WmsStatusHelpers.cs create mode 100644 MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs create mode 100644 MiGu.DB/Kernel/Context/MiGuDbContext.cs create mode 100644 MiGu.DB/Kernel/Conventions/Conventions.cs create mode 100644 MiGu.DB/Kernel/Design/MiGuDbContextFactory.cs create mode 100644 MiGu.DB/Kernel/Entities/EntityBases.cs create mode 100644 MiGu.DB/Kernel/Hosting/ServiceCollectionExtensions.cs create mode 100644 MiGu.DB/Kernel/Providers/ProviderSetups.cs create mode 100644 MiGu.DB/Kernel/Repositories/Repositories.cs create mode 100644 MiGu.DB/MIGRATIONS.md create mode 100644 MiGu.DB/MiGu.DB.csproj create mode 100644 MiGu.DB/Migrations/Sqlite/.gitkeep create mode 100644 MiGu.DB/Migrations/Sqlite/20260727011546_InitialPlatform.Designer.cs create mode 100644 MiGu.DB/Migrations/Sqlite/20260727011546_InitialPlatform.cs create mode 100644 MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs create mode 100644 MiGu.DB/README.md create mode 100644 MiGu.DB/build-out.txt create mode 100644 MiGu.Server/GlobalUsings.Db.cs delete mode 100644 MiGu.Server/Persistence/EntityBase.cs create mode 100644 MiGu.Server/Persistence/HttpActorContextMiddleware.cs delete mode 100644 MiGu.Server/Persistence/PlatformDbContext.cs delete mode 100644 MiGu.Server/SimpleFields/SimpleFieldDateTime.cs create mode 100644 MiGu.Server/Wms/WmsStatusAliases.cs diff --git a/.gitignore b/.gitignore index dcc3755..e46bc56 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,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.DB/Domains/Migrators/DataMigrators.cs b/MiGu.DB/Domains/Migrators/DataMigrators.cs new file mode 100644 index 0000000..8bdfece --- /dev/null +++ b/MiGu.DB/Domains/Migrators/DataMigrators.cs @@ -0,0 +1,105 @@ +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); + } +} + +/// +/// 把库内旧状态字符串刷成规范枚举名(Available/Idle→Empty,Occupied→FullContainer)。 +/// 仅 Sqlite 表名/列名;绕过枚举 HasConversion(ExecuteUpdate + 字符串比较会 InvalidCast)。 +/// +public sealed class LegacyStatusNormalizationMigrator : IDataMigrator +{ + public int Order => 15; + public string Name => "Wms.LegacyStatusNormalization"; + + public async Task MigrateAsync(DbContext db, CancellationToken ct = default) + { + if (db is not MiGuDbContext) return; + + await db.Database.ExecuteSqlRawAsync( + """ + UPDATE wms_storages + SET Status = 'Empty' + WHERE Status IN ('Available', 'Idle') + """, + ct); + + await db.Database.ExecuteSqlRawAsync( + """ + UPDATE wms_storages + SET Status = 'FullContainer' + WHERE Status = 'Occupied' + """, + ct); + + 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..c071a36 --- /dev/null +++ b/MiGu.DB/Domains/Transport/TransportEntities.cs @@ -0,0 +1,104 @@ +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; +} + +public static class WmsDispatchStatuses +{ + public const WmsDispatchStatus Dispatched = WmsDispatchStatus.Dispatched; + public const WmsDispatchStatus Failed = WmsDispatchStatus.Failed; +} diff --git a/MiGu.DB/Domains/Transport/TransportEnums.cs b/MiGu.DB/Domains/Transport/TransportEnums.cs new file mode 100644 index 0000000..06621d4 --- /dev/null +++ b/MiGu.DB/Domains/Transport/TransportEnums.cs @@ -0,0 +1,35 @@ +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 +} + +public enum WmsDispatchStatus +{ + Dispatched, + Failed +} 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..55f3bae --- /dev/null +++ b/MiGu.DB/Domains/Wms/WmsEnums.cs @@ -0,0 +1,66 @@ +namespace MiGu.DB.Domains.Wms; + +/// +/// WMS 状态/类型枚举。列以字符串存储(见约定 HasConversion<string>),成员名即库内合法值。 +/// 旧别名(Available/Idle/Occupied 等)不进入枚举,由 LegacyStatusNormalizationMigrator 刷库。 +/// +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..23d6e5a --- /dev/null +++ b/MiGu.DB/Domains/Wms/WmsStatusHelpers.cs @@ -0,0 +1,116 @@ +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 }; + + /// + /// 写路径归一:兼容历史字符串 Available/Idle/Occupied。 + /// 库内残留旧值须靠 DataMigrator 刷掉,不能依赖 converter 读侧归一(WHERE 会漏行)。 + /// + public static StorageStatus Normalize(string? status) => status switch + { + "Available" or "Idle" => Empty, + "Occupied" => FullContainer, + _ when Enum.TryParse(status, true, out var e) && All.Contains(e) => e, + _ => Empty + }; +} + +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..5b44021 --- /dev/null +++ b/MiGu.DB/Kernel/Context/MiGuDbContext.Sets.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using MiGu.DB.Domains.Dashboard; +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(); +} 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..ec1f1ea --- /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` 即可(`EnsurePlatformDatabaseAsync` → `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#数据库启动流程)** + +摘要:`EnsurePlatformDatabaseAsync` → `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` + 原始字符串比较可能触发转换异常;旧值刷库需绕过 converter(参见 `LegacyStatusNormalizationMigrator`)。 + +--- + +## 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/MiGuDbContextModelSnapshot.cs b/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs new file mode 100644 index 0000000..31568f3 --- /dev/null +++ b/MiGu.DB/Migrations/Sqlite/MiGuDbContextModelSnapshot.cs @@ -0,0 +1,1683 @@ +// +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.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..451fd30 --- /dev/null +++ b/MiGu.DB/README.md @@ -0,0 +1,57 @@ +# 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.EnsurePlatformDatabaseAsync(); // 内部 MigrateMiGuDbAsync +``` + +**启动流程、SchemaMode、开发/发版配置说明见 [MiGu.Server/README.md · 数据库启动流程](../MiGu.Server/README.md#数据库启动流程)。** + +配置键(`Database:*` / `ConnectionStrings:Platform`)由 Server 的 `appsettings*.json` 提供;本库通过 `AddMiGuDb(IConfiguration)` 读取。 + +## 实体与枚举 + +- 业务状态字段为 **enum**,约定自动 `HasConversion()`(严格 1:1,不做读侧归一)。 +- 旧库值(如 `Available`/`Idle`)由 `LegacyStatusNormalizationMigrator` 一次性刷成规范名。 +- `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 0000000000000000000000000000000000000000..b2ca9cc4d8572c0c9ce22621b0579cfd472168b0 GIT binary patch literal 46784 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 - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + 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 4b83d0e..0000000 --- a/MiGu.Server/Persistence/PlatformDbContext.cs +++ /dev/null @@ -1,253 +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(); - - 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(); - 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); - } - - 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 f4b45e6..5750a3b 100644 --- a/MiGu.Server/Persistence/PlatformPersistence.cs +++ b/MiGu.Server/Persistence/PlatformPersistence.cs @@ -1,464 +1,28 @@ -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 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 ''"); - } - - /// 为已存在的数据库补建 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 - ); - """); - } - - private static async Task EnsureSqliteColumnAsync(PlatformDbContext db, string table, string column, string definition) - { - 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(); - } + /// 启动建库/迁移入口(内部即 MigrateMiGuDbAsync:Migrate + 基线 + DataMigrator)。 + public static Task EnsurePlatformDatabaseAsync(this IServiceProvider services) + => services.MigrateMiGuDbAsync(); } diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs index b26ffdb..f7b821c 100644 --- a/MiGu.Server/Program.cs +++ b/MiGu.Server/Program.cs @@ -303,6 +303,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..73727f7 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() + │ + ▼ +EnsurePlatformDatabaseAsync() # → MiGu.DB.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..e32e214 100644 --- a/MiGu.Server/SimpleFields/SimpleFieldService.cs +++ b/MiGu.Server/SimpleFields/SimpleFieldService.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using MiGu.Server.Persistence; +using MiGu.DB.Domains.SimpleFields; namespace MiGu.Server.SimpleFields; diff --git a/MiGu.Server/Wms/WmsModels.cs b/MiGu.Server/Wms/WmsModels.cs index 5a9d2f3..7464f33 100644 --- a/MiGu.Server/Wms/WmsModels.cs +++ b/MiGu.Server/Wms/WmsModels.cs @@ -1,358 +1,64 @@ -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, string Status, 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 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 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 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); diff --git a/MiGu.Server/Wms/WmsReferenceValidator.cs b/MiGu.Server/Wms/WmsReferenceValidator.cs index b3ef8d4..b435a31 100644 --- a/MiGu.Server/Wms/WmsReferenceValidator.cs +++ b/MiGu.Server/Wms/WmsReferenceValidator.cs @@ -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 065eb4d..72f1fdf 100644 --- a/MiGu.Server/Wms/WmsService.cs +++ b/MiGu.Server/Wms/WmsService.cs @@ -1,6 +1,9 @@ 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; @@ -8,12 +11,20 @@ namespace MiGu.Server.Wms; public sealed class WmsService { private readonly PlatformDbContext _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( + PlatformDbContext 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,61 +87,71 @@ 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(); @@ -137,7 +165,7 @@ public sealed class WmsService entity.Enabled = req.Enabled; entity.SortOrder = req.SortOrder; ApplyCommon(entity, req, actor); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return entity; } @@ -150,7 +178,7 @@ 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(); @@ -162,24 +190,24 @@ public sealed class WmsService 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(); @@ -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.Normalize(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,7 +319,7 @@ 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(); @@ -303,15 +331,14 @@ public sealed class WmsService 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,7 +346,7 @@ 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(); @@ -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,7 +371,7 @@ 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(); @@ -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 { - var set = _db.Set(); - var entity = await FindEditable(set, id, version); + var entity = await FindEditable(id, version); if (typeof(T) == typeof(WarehouseArea)) { if (await _db.Storages.AnyAsync(x => x.AreaId == id)) @@ -423,19 +448,22 @@ public sealed class WmsService entity.DeletedAt = DateTimeOffset.UtcNow; entity.DeletedBy = actor; entity.UpdatedBy = actor; - await _db.SaveChangesAsync(); + 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 +483,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; @@ -471,11 +501,12 @@ public sealed class WmsService EnsureUnlocked(current); } - 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 +517,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,7 +530,7 @@ public sealed class WmsService }); await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId); await SyncOccupancyStatus(storageId: toStorageId); return current; @@ -520,7 +551,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 = "{}", @@ -531,7 +562,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); } @@ -578,7 +609,7 @@ 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; } @@ -620,7 +651,7 @@ public sealed class WmsService } _db.ContainerMaterials.Remove(current); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); await SyncOccupancyStatus(containerId: containerId); } @@ -666,7 +697,7 @@ public sealed class WmsService } } - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); } public async Task EnsureDefaultWarehouseAsync(string actor = "system") @@ -683,7 +714,7 @@ public sealed class WmsService }; StampCreate(wh, actor); _db.Warehouses.Add(wh); - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); return wh; } @@ -692,44 +723,26 @@ public sealed class WmsService 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 (!StorageStatuses.All.Contains(s.Status)) + s.Status = StorageStatuses.Empty; 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 - }; + if (!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) { @@ -739,7 +752,7 @@ public sealed class WmsService b.Status = ContainerMaterialStatuses.Bound; } - await _db.SaveChangesAsync(); + await _uow.SaveChangesAsync(); foreach (var s in storages.Where(x => x.Status != StorageStatuses.Disabled)) await SyncOccupancyStatus(storageId: s.Id); @@ -773,13 +786,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) @@ -843,23 +857,21 @@ 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 Task FindEditable(Guid id, long? version) + where T : class, IEntity, ISoftDeletable, IVersioned, ILockable + => _services.GetRequiredService>().GetEditableAsync(id, version); private static void EnsureVersion(EntityBase entity, long? version) { if (version.HasValue && entity.Version != version.Value) - throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试"); + throw new MiGu.DB.Abstractions.Exceptions.ConcurrencyConflictException( + entity.GetType().Name, entity.Id, version); } private static void EnsureUnlocked(EntityBase entity) { - if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改"); + if (entity.IsLock) + throw new MiGu.DB.Abstractions.Exceptions.EntityLockedException(entity.GetType().Name, entity.Id); } private static void StampCreate(EntityBase entity, string actor) @@ -909,73 +921,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..b5be817 --- /dev/null +++ b/MiGu.Server/Wms/WmsStatusAliases.cs @@ -0,0 +1,19 @@ +// 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; +global using WmsDispatchStatuses = MiGu.DB.Domains.Transport.WmsDispatchStatuses; diff --git a/MiGu.Server/Wms/WmsTransportPlanner.cs b/MiGu.Server/Wms/WmsTransportPlanner.cs index 1dee59b..f5feab3 100644 --- a/MiGu.Server/Wms/WmsTransportPlanner.cs +++ b/MiGu.Server/Wms/WmsTransportPlanner.cs @@ -13,11 +13,12 @@ public sealed class WmsTransportPlanner 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(); @@ -94,17 +95,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; @@ -124,6 +126,7 @@ public sealed class WmsTransportPlanner private List BuildSourceCandidates( PlannerContext ctx, WmsTransportRequest request, + WmsTransportTriggerType trigger, TransportSelector selector, TransportTaskOptions options) { @@ -145,7 +148,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; @@ -162,6 +165,7 @@ public sealed class WmsTransportPlanner private List BuildTargetCandidates( PlannerContext ctx, WmsTransportRequest request, + WmsTransportTriggerType trigger, TransportSelector selector, TransportTaskOptions options, Guid sourceStorageId) @@ -176,7 +180,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; @@ -200,7 +204,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; @@ -220,7 +225,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; @@ -236,7 +242,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..d207288 100644 --- a/MiGu.Server/Wms/WmsTransportRuleService.cs +++ b/MiGu.Server/Wms/WmsTransportRuleService.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using MiGu.DB.Abstractions.Exceptions; +using MiGu.DB.Abstractions.Persistence; using MiGu.Server.Persistence; namespace MiGu.Server.Wms; @@ -6,14 +8,26 @@ namespace MiGu.Server.Wms; public sealed class WmsTransportRuleService { private readonly PlatformDbContext _db; + private readonly IEditableRepository _rules; + private readonly IUnitOfWork _uow; - public WmsTransportRuleService(PlatformDbContext db) => _db = db; + public WmsTransportRuleService( + PlatformDbContext 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,44 +51,24 @@ 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(); - } - - 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("规则编码已存在"); + await _rules.SoftDeleteAsync(id, version); + await _uow.SaveChangesAsync(); } private static void StampCreate(EntityBase entity, string actor) diff --git a/MiGu.Server/Wms/WmsTransportTaskService.cs b/MiGu.Server/Wms/WmsTransportTaskService.cs index d4ff880..9b65380 100644 --- a/MiGu.Server/Wms/WmsTransportTaskService.cs +++ b/MiGu.Server/Wms/WmsTransportTaskService.cs @@ -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); @@ -245,13 +246,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) 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 2c97ef8..e5c7211 100644 --- a/MiGu.Server/appsettings.json +++ b/MiGu.Server/appsettings.json @@ -114,5 +114,13 @@ } } } + }, + "Database": { + "Provider": "sqlite", + "SchemaMode": "Migrate", + "ApplyDataMigratorsOnStartup": true + }, + "ConnectionStrings": { + "Platform": "Data Source=data/platform.db" } } diff --git a/MiGu.Server/data/platform.db b/MiGu.Server/data/platform.db index ad4309c895d49bdc893896477aa8da8ca124431d..9a472209435d88229f65ab7a0ca30d67df87a462 100644 GIT binary patch delta 28 icmZozAk&~QL7J73fq{W>vY>!HkfoRQYFh!@9DV>_5C+`< literal 462848 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

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

备份

- - - - - - -

展示

From 125372b157219b49c0d32fa34a2c8d04cbd5e9d3 Mon Sep 17 00:00:00 2001 From: ArtoriasWu Date: Mon, 3 Aug 2026 09:41:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=BD=92=E4=B8=80=E5=8C=96=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=81=97=E7=95=99=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 LegacyStatusNormalizationMigrator,仅保留 AreaLayoutModeDefaultMigrator - 删除 WmsDispatchStatus 枚举、常量和全局 using - 统一所有 DbContext 注入为 MiGuDbContext,移除 PlatformDbContext - 服务实体操作统一用 IEditableRepository,移除本地实现 - 移除 WmsService 的 MigrateLegacyAsync 方法 - 精简接口模型,移除部分字段和请求体 - 前端保存容器时 status 字段取自已有数据,移除写死默认值 - 更新文档,去除旧说明,统一数据库初始化流程 --- MiGu.DB/Domains/Migrators/DataMigrators.cs | 27 +---- .../Domains/Transport/TransportEntities.cs | 6 - MiGu.DB/Domains/Transport/TransportEnums.cs | 6 - MiGu.DB/Domains/Wms/WmsEnums.cs | 1 - MiGu.DB/Domains/Wms/WmsStatusHelpers.cs | 13 +- MiGu.DB/MIGRATIONS.md | 6 +- MiGu.DB/README.md | 3 +- MiGu.Server/Controllers/FleetController.cs | 4 +- MiGu.Server/Controllers/WmsController.cs | 4 - .../Dashboard/DashboardShortcutService.cs | 4 +- MiGu.Server/Fleet/AlarmCollector.cs | 4 +- MiGu.Server/Fleet/CdmTaskSync.cs | 4 +- MiGu.Server/GlobalUsings.Db.cs | 4 +- .../Persistence/PlatformPersistence.cs | 4 - MiGu.Server/Program.cs | 3 +- MiGu.Server/README.md | 4 +- .../SimpleFields/SimpleFieldService.cs | 4 +- MiGu.Server/Wms/WmsModels.cs | 7 +- MiGu.Server/Wms/WmsReferenceValidator.cs | 4 +- MiGu.Server/Wms/WmsService.cs | 111 ++++-------------- MiGu.Server/Wms/WmsStatusAliases.cs | 1 - MiGu.Server/Wms/WmsTransportPlanner.cs | 4 +- MiGu.Server/Wms/WmsTransportRuleService.cs | 8 +- MiGu.Server/Wms/WmsTransportTaskService.cs | 6 +- .../apps/simple-platform-vue/src/types/wms.ts | 1 - .../views/admin/WarehouseManagementView.vue | 3 +- 26 files changed, 63 insertions(+), 183 deletions(-) diff --git a/MiGu.DB/Domains/Migrators/DataMigrators.cs b/MiGu.DB/Domains/Migrators/DataMigrators.cs index 8bdfece..6e3a643 100644 --- a/MiGu.DB/Domains/Migrators/DataMigrators.cs +++ b/MiGu.DB/Domains/Migrators/DataMigrators.cs @@ -29,35 +29,16 @@ public sealed class SimpleFieldsCarTypeBackfillMigrator : IDataMigrator } } -/// -/// 把库内旧状态字符串刷成规范枚举名(Available/Idle→Empty,Occupied→FullContainer)。 -/// 仅 Sqlite 表名/列名;绕过枚举 HasConversion(ExecuteUpdate + 字符串比较会 InvalidCast)。 -/// -public sealed class LegacyStatusNormalizationMigrator : IDataMigrator +/// 库区 LayoutMode 空值补 Flat(raw UPDATE,避开枚举 converter)。 +public sealed class AreaLayoutModeDefaultMigrator : IDataMigrator { public int Order => 15; - public string Name => "Wms.LegacyStatusNormalization"; + 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_storages - SET Status = 'Empty' - WHERE Status IN ('Available', 'Idle') - """, - ct); - - await db.Database.ExecuteSqlRawAsync( - """ - UPDATE wms_storages - SET Status = 'FullContainer' - WHERE Status = 'Occupied' - """, - ct); - await db.Database.ExecuteSqlRawAsync( """ UPDATE wms_areas @@ -98,7 +79,7 @@ public static class DataMigratorRegistration public static IServiceCollection AddMiGuDataMigrators(this IServiceCollection services) { services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); return services; } diff --git a/MiGu.DB/Domains/Transport/TransportEntities.cs b/MiGu.DB/Domains/Transport/TransportEntities.cs index c071a36..a35a05b 100644 --- a/MiGu.DB/Domains/Transport/TransportEntities.cs +++ b/MiGu.DB/Domains/Transport/TransportEntities.cs @@ -96,9 +96,3 @@ public static class WmsReservationStatuses public const WmsReservationStatus Released = WmsReservationStatus.Released; public const WmsReservationStatus Expired = WmsReservationStatus.Expired; } - -public static class WmsDispatchStatuses -{ - public const WmsDispatchStatus Dispatched = WmsDispatchStatus.Dispatched; - public const WmsDispatchStatus Failed = WmsDispatchStatus.Failed; -} diff --git a/MiGu.DB/Domains/Transport/TransportEnums.cs b/MiGu.DB/Domains/Transport/TransportEnums.cs index 06621d4..014477c 100644 --- a/MiGu.DB/Domains/Transport/TransportEnums.cs +++ b/MiGu.DB/Domains/Transport/TransportEnums.cs @@ -27,9 +27,3 @@ public enum WmsReservationStatus Released, Expired } - -public enum WmsDispatchStatus -{ - Dispatched, - Failed -} diff --git a/MiGu.DB/Domains/Wms/WmsEnums.cs b/MiGu.DB/Domains/Wms/WmsEnums.cs index 55f3bae..e5bf428 100644 --- a/MiGu.DB/Domains/Wms/WmsEnums.cs +++ b/MiGu.DB/Domains/Wms/WmsEnums.cs @@ -2,7 +2,6 @@ namespace MiGu.DB.Domains.Wms; /// /// WMS 状态/类型枚举。列以字符串存储(见约定 HasConversion<string>),成员名即库内合法值。 -/// 旧别名(Available/Idle/Occupied 等)不进入枚举,由 LegacyStatusNormalizationMigrator 刷库。 /// public enum AreaLayoutMode { diff --git a/MiGu.DB/Domains/Wms/WmsStatusHelpers.cs b/MiGu.DB/Domains/Wms/WmsStatusHelpers.cs index 23d6e5a..5961b1d 100644 --- a/MiGu.DB/Domains/Wms/WmsStatusHelpers.cs +++ b/MiGu.DB/Domains/Wms/WmsStatusHelpers.cs @@ -33,17 +33,8 @@ public static class StorageStatuses public static readonly HashSet All = new() { Empty, EmptyContainer, FullContainer, Disabled }; - /// - /// 写路径归一:兼容历史字符串 Available/Idle/Occupied。 - /// 库内残留旧值须靠 DataMigrator 刷掉,不能依赖 converter 读侧归一(WHERE 会漏行)。 - /// - public static StorageStatus Normalize(string? status) => status switch - { - "Available" or "Idle" => Empty, - "Occupied" => FullContainer, - _ when Enum.TryParse(status, true, out var e) && All.Contains(e) => e, - _ => Empty - }; + 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 diff --git a/MiGu.DB/MIGRATIONS.md b/MiGu.DB/MIGRATIONS.md index ec1f1ea..2ca301d 100644 --- a/MiGu.DB/MIGRATIONS.md +++ b/MiGu.DB/MIGRATIONS.md @@ -94,7 +94,7 @@ dotnet ef migrations add <迁移名称> ` ### 3.4 应用到本地库 -启动 `MiGu.Server` 即可(`EnsurePlatformDatabaseAsync` → `MigrateMiGuDbAsync`),或: +启动 `MiGu.Server` 即可(`Program.cs` 直接调用 `MigrateMiGuDbAsync`),或: ```powershell dotnet ef database update ` @@ -151,7 +151,7 @@ dotnet ef database update <目标迁移名> ` → **[MiGu.Server/README.md · 数据库启动流程](../MiGu.Server/README.md#数据库启动流程)** -摘要:`EnsurePlatformDatabaseAsync` → `MigrateMiGuDbAsync`;`Database:SchemaMode` 为 `EnsureCreated` 或 `Migrate`;之后按需跑 `IDataMigrator`。 +摘要:`MigrateMiGuDbAsync`;`Database:SchemaMode` 为 `EnsureCreated` 或 `Migrate`;之后按需跑 `IDataMigrator`。 **Migrate 基线:** 已有表、无 History 时,会把**全部** pending Migration 写入 `__EFMigrationsHistory`(假定 EnsureCreated 库已对齐模型 tip)。发版前若开发期改过模型,须先 `migrations add` 再切 `Migrate`。 @@ -165,7 +165,7 @@ dotnet ef database update <目标迁移名> ` | 刷旧枚举字符串、回填列 | 新增 `IDataMigrator`,注册到 `AddMiGuDataMigrators` | | 开发机整库清空重来 | 删 `data/platform.db*` 后启动(等同空库 Migrate);**不要**在生产用 EnsureDeleted | -注意:带 `HasConversion` 的枚举列,用 `ExecuteUpdate` + 原始字符串比较可能触发转换异常;旧值刷库需绕过 converter(参见 `LegacyStatusNormalizationMigrator`)。 +注意:带 `HasConversion` 的枚举列,用 `ExecuteUpdate` + 原始字符串比较可能触发转换异常;空 LayoutMode 等特例用 raw UPDATE(参见 `AreaLayoutModeDefaultMigrator`)。 --- diff --git a/MiGu.DB/README.md b/MiGu.DB/README.md index 451fd30..debc756 100644 --- a/MiGu.DB/README.md +++ b/MiGu.DB/README.md @@ -17,7 +17,7 @@ ```csharp builder.Services.AddPlatformPersistence(builder.Configuration); // 内部 AddMiGuDb -await app.Services.EnsurePlatformDatabaseAsync(); // 内部 MigrateMiGuDbAsync +await app.Services.MigrateMiGuDbAsync(); ``` **启动流程、SchemaMode、开发/发版配置说明见 [MiGu.Server/README.md · 数据库启动流程](../MiGu.Server/README.md#数据库启动流程)。** @@ -27,7 +27,6 @@ await app.Services.EnsurePlatformDatabaseAsync(); // 内部 Migrat ## 实体与枚举 - 业务状态字段为 **enum**,约定自动 `HasConversion()`(严格 1:1,不做读侧归一)。 -- 旧库值(如 `Available`/`Idle`)由 `LegacyStatusNormalizationMigrator` 一次性刷成规范名。 - `Version` 为乐观并发令牌;软删走全局 `HasQueryFilter`。 - 复数辅助类(`StorageStatuses`、`LocationKinds`…)负责 API 字符串解析与集合校验。 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/WmsController.cs b/MiGu.Server/Controllers/WmsController.cs index 908c615..feccc1a 100644 --- a/MiGu.Server/Controllers/WmsController.cs +++ b/MiGu.Server/Controllers/WmsController.cs @@ -192,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())); 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 6c2e919..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,7 +161,7 @@ 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); 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 index b4c7f3b..c3811e3 100644 --- a/MiGu.Server/GlobalUsings.Db.cs +++ b/MiGu.Server/GlobalUsings.Db.cs @@ -1,5 +1,5 @@ -global using PlatformDbContext = MiGu.DB.Kernel.Context.MiGuDbContext; -global using EntityBase = MiGu.DB.Kernel.Entities.AggregateRoot; +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; diff --git a/MiGu.Server/Persistence/PlatformPersistence.cs b/MiGu.Server/Persistence/PlatformPersistence.cs index 5750a3b..a176778 100644 --- a/MiGu.Server/Persistence/PlatformPersistence.cs +++ b/MiGu.Server/Persistence/PlatformPersistence.cs @@ -21,8 +21,4 @@ public static class PlatformPersistence services.AddScoped(); return services; } - - /// 启动建库/迁移入口(内部即 MigrateMiGuDbAsync:Migrate + 基线 + DataMigrator)。 - public static Task EnsurePlatformDatabaseAsync(this IServiceProvider services) - => services.MigrateMiGuDbAsync(); } diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs index 8dd4939..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(); diff --git a/MiGu.Server/README.md b/MiGu.Server/README.md index 73727f7..6a94aef 100644 --- a/MiGu.Server/README.md +++ b/MiGu.Server/README.md @@ -110,8 +110,8 @@ AddPlatformPersistence(configuration) # 注册 AddMiGuDb + Wms/SimpleFields builder.Build() │ ▼ -EnsurePlatformDatabaseAsync() # → MiGu.DB.MigrateMiGuDbAsync() - │ # Schema 初始化 + 可选 IDataMigrator +MigrateMiGuDbAsync() # Schema 初始化 + 可选 IDataMigrator + │ ▼ … 其余中间件 … UseMiddleware # 请求级写入 IActorContext(审计戳) diff --git a/MiGu.Server/SimpleFields/SimpleFieldService.cs b/MiGu.Server/SimpleFields/SimpleFieldService.cs index e32e214..47bb27b 100644 --- a/MiGu.Server/SimpleFields/SimpleFieldService.cs +++ b/MiGu.Server/SimpleFields/SimpleFieldService.cs @@ -6,9 +6,9 @@ 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 7464f33..0bb0d60 100644 --- a/MiGu.Server/Wms/WmsModels.cs +++ b/MiGu.Server/Wms/WmsModels.cs @@ -20,7 +20,7 @@ public sealed record ContainerMaterialSnapshot( 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, + 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 record AreaRequest( @@ -57,8 +57,3 @@ public sealed record ContainerLocationRequest( 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); diff --git a/MiGu.Server/Wms/WmsReferenceValidator.cs b/MiGu.Server/Wms/WmsReferenceValidator.cs index b435a31..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; } diff --git a/MiGu.Server/Wms/WmsService.cs b/MiGu.Server/Wms/WmsService.cs index e39324f..7e563ab 100644 --- a/MiGu.Server/Wms/WmsService.cs +++ b/MiGu.Server/Wms/WmsService.cs @@ -10,14 +10,14 @@ 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, + MiGuDbContext db, IUnitOfWork uow, IServiceProvider services, WmsReferenceValidator refs) @@ -158,7 +158,7 @@ public sealed class WmsService 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"); @@ -185,7 +185,7 @@ public sealed class WmsService 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(); @@ -214,7 +214,7 @@ public sealed class WmsService 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(); @@ -227,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); + 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)) @@ -326,7 +326,7 @@ public sealed class WmsService 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(); @@ -353,7 +353,7 @@ public sealed class WmsService 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(""); @@ -378,9 +378,9 @@ public sealed class WmsService 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(""); @@ -425,9 +425,9 @@ public sealed class WmsService 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 entity = await FindEditable(id, version); if (typeof(T) == typeof(WarehouseArea)) { if (await _db.Storages.AnyAsync(x => x.AreaId == id)) @@ -444,10 +444,8 @@ 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 Repo().SoftDeleteAsync(id, version); await _uow.SaveChangesAsync(); } @@ -497,8 +495,7 @@ public sealed class WmsService } else { - EnsureVersion(current, req.Version); - EnsureUnlocked(current); + await Repo().GetEditableAsync(current.Id, req.Version); } current.LocationType = locationType; @@ -540,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; @@ -614,15 +611,9 @@ public sealed class WmsService 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; @@ -718,46 +709,6 @@ public sealed class WmsService 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; - - var storages = await _db.Storages.ToListAsync(); - foreach (var s in storages) - { - if (s.LevelNo <= 0) s.LevelNo = 1; - if (s.DepthNo <= 0) s.DepthNo = 1; - if (string.IsNullOrWhiteSpace(s.SiteCode)) s.SiteCode = s.SiteId; - if (!StorageStatuses.All.Contains(s.Status)) - s.Status = StorageStatuses.Empty; - if (!s.Enabled) s.Status = StorageStatuses.Disabled; - } - - var containers = await _db.Containers.ToListAsync(); - foreach (var c in containers) - { - if (!ContainerStatuses.All.Contains(c.Status)) - c.Status = ContainerStatuses.EmptyMaterial; - } - - 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 _uow.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 @@ -857,16 +808,13 @@ public sealed class WmsService }); } + private IEditableRepository Repo() + where T : class, IEntity, ISoftDeletable, IVersioned, ILockable + => _services.GetRequiredService>(); + private Task FindEditable(Guid id, long? version) where T : class, IEntity, ISoftDeletable, IVersioned, ILockable - => _services.GetRequiredService>().GetEditableAsync(id, version); - - private static void EnsureVersion(EntityBase entity, long? version) - { - if (version.HasValue && entity.Version != version.Value) - throw new MiGu.DB.Abstractions.Exceptions.ConcurrencyConflictException( - entity.GetType().Name, entity.Id, version); - } + => Repo().GetEditableAsync(id, version); private static bool IsUniqueConstraintViolation(DbUpdateException ex) { @@ -882,19 +830,13 @@ public sealed class WmsService return false; } - private static void EnsureUnlocked(EntityBase entity) - { - if (entity.IsLock) - throw new MiGu.DB.Abstractions.Exceptions.EntityLockedException(entity.GetType().Name, entity.Id); - } - - 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(""); @@ -902,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 "{}"; diff --git a/MiGu.Server/Wms/WmsStatusAliases.cs b/MiGu.Server/Wms/WmsStatusAliases.cs index b5be817..ad4578e 100644 --- a/MiGu.Server/Wms/WmsStatusAliases.cs +++ b/MiGu.Server/Wms/WmsStatusAliases.cs @@ -16,4 +16,3 @@ 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; -global using WmsDispatchStatuses = MiGu.DB.Domains.Transport.WmsDispatchStatuses; diff --git a/MiGu.Server/Wms/WmsTransportPlanner.cs b/MiGu.Server/Wms/WmsTransportPlanner.cs index f978bab..926b532 100644 --- a/MiGu.Server/Wms/WmsTransportPlanner.cs +++ b/MiGu.Server/Wms/WmsTransportPlanner.cs @@ -6,10 +6,10 @@ 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) { diff --git a/MiGu.Server/Wms/WmsTransportRuleService.cs b/MiGu.Server/Wms/WmsTransportRuleService.cs index d207288..fbd6fbc 100644 --- a/MiGu.Server/Wms/WmsTransportRuleService.cs +++ b/MiGu.Server/Wms/WmsTransportRuleService.cs @@ -7,12 +7,12 @@ 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, + MiGuDbContext db, IEditableRepository rules, IUnitOfWork uow) { @@ -71,13 +71,13 @@ public sealed class WmsTransportRuleService await _uow.SaveChangesAsync(); } - 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 fb842f5..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) @@ -331,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/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 }