持久层重构为独立 MiGu.DB 工程并优化集成
- 新增 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 追踪数据库结构 - 优化代码结构,解耦领域与持久层,提升扩展性与安全性
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
namespace MiGu.DB.Abstractions.Entities;
|
||||
|
||||
public interface IEntity<TKey>
|
||||
{
|
||||
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
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Linq.Expressions;
|
||||
using MiGu.DB.Abstractions.Entities;
|
||||
|
||||
namespace MiGu.DB.Abstractions.Persistence;
|
||||
|
||||
public interface IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>
|
||||
{
|
||||
IQueryable<TEntity> Query(bool asNoTracking = true);
|
||||
Task<TEntity?> FindAsync(TKey id, CancellationToken ct = default);
|
||||
Task AddAsync(TEntity entity, CancellationToken ct = default);
|
||||
void Update(TEntity entity);
|
||||
}
|
||||
|
||||
public interface IEditableRepository<TEntity> : IRepository<TEntity, Guid>
|
||||
where TEntity : class, IEntity<Guid>, ISoftDeletable, IVersioned, ILockable
|
||||
{
|
||||
Task<TEntity> GetEditableAsync(Guid id, long? expectedVersion, CancellationToken ct = default);
|
||||
Task SoftDeleteAsync(Guid id, long? expectedVersion, CancellationToken ct = default);
|
||||
Task EnsureUniqueAsync(Expression<Func<TEntity, bool>> predicate, string errorMessage, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IHistoryRepository<TEntity> : IRepository<TEntity, Guid>
|
||||
where TEntity : class, IEntity<Guid>, IHistoryEntry
|
||||
{
|
||||
Task AppendAsync(TEntity entry, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||||
Task ExecuteInTransactionAsync(Func<CancellationToken, Task> action, CancellationToken ct = default);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动期 Schema 初始化策略。
|
||||
/// <see cref="Migrate"/>:版本化迁移(发版/现场);
|
||||
/// <see cref="EnsureCreated"/>:按当前模型建库(开发期,改模型需删库重建)。
|
||||
/// </summary>
|
||||
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; } = "";
|
||||
|
||||
/// <summary>Schema 初始化模式,对应配置 Database:SchemaMode。</summary>
|
||||
public MiGuSchemaMode SchemaMode { get; set; } = MiGuSchemaMode.Migrate;
|
||||
|
||||
/// <summary>为 true 时,Schema 初始化后按 Order 执行全部 IDataMigrator(默认开启)。</summary>
|
||||
public bool ApplyDataMigratorsOnStartup { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MiGu.DB.Domains.Dashboard;
|
||||
|
||||
public sealed class UserDashboardShortcut
|
||||
{
|
||||
public string UserId { get; set; } = "";
|
||||
public string Scope { get; set; } = "";
|
||||
public string KeysJson { get; set; } = "[]";
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -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)。
|
||||
|
||||
/// <summary>将 simple_fields.other 回填到 car_type(替代原 PlatformPersistence raw UPDATE)。</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把库内旧状态字符串刷成规范枚举名(Available/Idle→Empty,Occupied→FullContainer)。
|
||||
/// 仅 Sqlite 表名/列名;绕过枚举 HasConversion(ExecuteUpdate + 字符串比较会 InvalidCast)。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LocationType=Storage 时回填 StorageId,供库存 join;写路径 BindOrTransferLocation 也会维护该字段。
|
||||
/// </summary>
|
||||
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<IDataMigrator, SimpleFieldsCarTypeBackfillMigrator>();
|
||||
services.AddSingleton<IDataMigrator, LegacyStatusNormalizationMigrator>();
|
||||
services.AddSingleton<IDataMigrator, ContainerLocationStorageIdBackfillMigrator>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MiGu.DB.Kernel.Entities;
|
||||
|
||||
namespace MiGu.DB.Domains.SimpleFields;
|
||||
|
||||
public sealed class SimpleField : Entity<Guid>
|
||||
{
|
||||
[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);
|
||||
}
|
||||
@@ -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<Guid>
|
||||
{
|
||||
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<WmsTransportTriggerType> All = new()
|
||||
{ MaterialCall, FinishedGoodsOffline, AutoTransfer };
|
||||
|
||||
public static bool IsDefined(string? value) =>
|
||||
Enum.TryParse<WmsTransportTriggerType>(value, true, out var e) && All.Contains(e);
|
||||
|
||||
public static WmsTransportTriggerType ParseOr(string? value, WmsTransportTriggerType fallback = WmsTransportTriggerType.MaterialCall) =>
|
||||
Enum.TryParse<WmsTransportTriggerType>(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<WmsTransportTaskStatus> Active = new()
|
||||
{ Pending, Reserved, Dispatched, InTransit };
|
||||
public static readonly HashSet<WmsTransportTaskStatus> 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;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace MiGu.DB.Domains.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// 运输域状态/触发类型枚举。存储规则同 Wms:HasConversion<string>,成员名 = 列值。
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
@@ -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<WmsTransportRule>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WmsTransportRule> 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<WmsTransportTask>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WmsTransportTask> 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<WmsTransportReservation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WmsTransportReservation> 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<WmsTransportTaskHistory>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WmsTransportTaskHistory> 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<SimpleField>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SimpleField> 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<DateTimeOffset, string>(
|
||||
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<UserDashboardShortcut>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserDashboardShortcut> 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<DateTimeOffset, string>(
|
||||
v => v.UtcDateTime.ToString("O"),
|
||||
v => DateTimeOffset.Parse(v));
|
||||
b.Property(x => x.UpdatedAt).HasColumnName("updated_at").HasConversion(dateTime).HasMaxLength(40);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Warehouse>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Warehouse> b)
|
||||
{
|
||||
b.ToTable("wms_warehouses");
|
||||
b.HasKey(x => x.Id);
|
||||
b.HasIndex(x => x.Code).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WarehouseAreaConfiguration : IEntityTypeConfiguration<WarehouseArea>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WarehouseArea> 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<Storage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Storage> 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<Container>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Container> b)
|
||||
{
|
||||
b.ToTable("wms_containers");
|
||||
b.HasKey(x => x.Id);
|
||||
b.HasIndex(x => x.Code).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MaterialTypeConfiguration : IEntityTypeConfiguration<MaterialType>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MaterialType> b)
|
||||
{
|
||||
b.ToTable("wms_material_types");
|
||||
b.HasKey(x => x.Id);
|
||||
b.HasIndex(x => x.Code).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MaterialConfiguration : IEntityTypeConfiguration<Material>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Material> 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<ContainerLocation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContainerLocation> 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<ContainerMaterial>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContainerMaterial> 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<StockEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockEvent> 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<T> : IEntityTypeConfiguration<T> where T : HistoryEntity
|
||||
{
|
||||
private readonly string _table;
|
||||
protected HistoryConfigurationBase(string table) => _table = table;
|
||||
|
||||
public virtual void Configure(EntityTypeBuilder<T> 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<ContainerLocationHistory>
|
||||
{
|
||||
public ContainerLocationHistoryConfiguration() : base("wms_container_location_history") { }
|
||||
}
|
||||
|
||||
public sealed class ContainerMaterialHistoryConfiguration : HistoryConfigurationBase<ContainerMaterialHistory>
|
||||
{
|
||||
public ContainerMaterialHistoryConfiguration() : base("wms_container_material_history") { }
|
||||
|
||||
public override void Configure(EntityTypeBuilder<ContainerMaterialHistory> b)
|
||||
{
|
||||
base.Configure(b);
|
||||
b.Property(x => x.QuantityDelta).HasPrecision(18, 4);
|
||||
}
|
||||
}
|
||||
@@ -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; } = "";
|
||||
/// <summary>LocationType=Storage 时的库位 Id;由 LocationId 回填,供库存 join 下推。</summary>
|
||||
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<Guid>
|
||||
{
|
||||
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 = "默认仓库";
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
namespace MiGu.DB.Domains.Wms;
|
||||
|
||||
/// <summary>
|
||||
/// WMS 状态/类型枚举。列以字符串存储(见约定 HasConversion<string>),成员名即库内合法值。
|
||||
/// 旧别名(Available/Idle/Occupied 等)不进入枚举,由 LegacyStatusNormalizationMigrator 刷库。
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
namespace MiGu.DB.Domains.Wms;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举辅助类(复数命名):承接 API/DTO 的字符串入参,解析为实体上的枚举。
|
||||
/// 与同名枚举分离,避免「属性 LocationKind 初始值引用实例属性」一类编译冲突。
|
||||
/// Server 侧通过 global using 别名(如 StorageStatuses)引用本文件类型。
|
||||
/// </summary>
|
||||
public static class AreaLayoutModes
|
||||
{
|
||||
public static readonly HashSet<AreaLayoutMode> All = new() { AreaLayoutMode.Flat, AreaLayoutMode.Grid };
|
||||
|
||||
public static AreaLayoutMode ParseOr(string? value, AreaLayoutMode fallback = AreaLayoutMode.Flat) =>
|
||||
Enum.TryParse<AreaLayoutMode>(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<LocationKind> All = new() { Grid, Station };
|
||||
|
||||
public static LocationKind ParseOr(string? value, LocationKind fallback = LocationKind.Station) =>
|
||||
Enum.TryParse<LocationKind>(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<StorageStatus> All = new()
|
||||
{ Empty, EmptyContainer, FullContainer, Disabled };
|
||||
|
||||
/// <summary>
|
||||
/// 写路径归一:兼容历史字符串 Available/Idle/Occupied。
|
||||
/// 库内残留旧值须靠 DataMigrator 刷掉,不能依赖 converter 读侧归一(WHERE 会漏行)。
|
||||
/// </summary>
|
||||
public static StorageStatus Normalize(string? status) => status switch
|
||||
{
|
||||
"Available" or "Idle" => Empty,
|
||||
"Occupied" => FullContainer,
|
||||
_ when Enum.TryParse<StorageStatus>(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<ContainerStatus> All = new() { EmptyMaterial, FullMaterial };
|
||||
|
||||
public static ContainerStatus ParseOr(string? value, ContainerStatus fallback = ContainerStatus.EmptyMaterial) =>
|
||||
Enum.TryParse<ContainerStatus>(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<ContainerLocationType> All = new() { Storage, Car };
|
||||
|
||||
public static bool IsDefined(string? value) =>
|
||||
Enum.TryParse<ContainerLocationType>(value, true, out var e) && All.Contains(e);
|
||||
|
||||
public static ContainerLocationType ParseOr(string? value, ContainerLocationType fallback = ContainerLocationType.Storage) =>
|
||||
Enum.TryParse<ContainerLocationType>(value, true, out var e) && All.Contains(e) ? e : fallback;
|
||||
|
||||
public static bool EqualsString(ContainerLocationType value, string? other) =>
|
||||
Enum.TryParse<ContainerLocationType>(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<ContainerLocationStatus> All = new() { Active, Locked, Exception };
|
||||
|
||||
public static bool IsDefined(string? value) =>
|
||||
Enum.TryParse<ContainerLocationStatus>(value, true, out var e) && All.Contains(e);
|
||||
|
||||
public static ContainerLocationStatus ParseOr(string? value, ContainerLocationStatus fallback = ContainerLocationStatus.Active) =>
|
||||
Enum.TryParse<ContainerLocationStatus>(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<ContainerMaterialStatus> 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<MaterialLifecycle> All = new() { Active, Archived };
|
||||
|
||||
public static MaterialLifecycle ParseOr(string? value, MaterialLifecycle fallback = MaterialLifecycle.Active) =>
|
||||
Enum.TryParse<MaterialLifecycle>(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;
|
||||
}
|
||||
@@ -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<Warehouse> Warehouses => Set<Warehouse>();
|
||||
public DbSet<WarehouseArea> WarehouseAreas => Set<WarehouseArea>();
|
||||
public DbSet<Storage> Storages => Set<Storage>();
|
||||
public DbSet<Container> Containers => Set<Container>();
|
||||
public DbSet<MaterialType> MaterialTypes => Set<MaterialType>();
|
||||
public DbSet<Material> Materials => Set<Material>();
|
||||
public DbSet<ContainerLocation> ContainerLocations => Set<ContainerLocation>();
|
||||
public DbSet<ContainerMaterial> ContainerMaterials => Set<ContainerMaterial>();
|
||||
public DbSet<StockEvent> StockEvents => Set<StockEvent>();
|
||||
public DbSet<ContainerLocationHistory> ContainerLocationHistories => Set<ContainerLocationHistory>();
|
||||
public DbSet<ContainerMaterialHistory> ContainerMaterialHistories => Set<ContainerMaterialHistory>();
|
||||
public DbSet<WmsTransportRule> WmsTransportRules => Set<WmsTransportRule>();
|
||||
public DbSet<WmsTransportTask> WmsTransportTasks => Set<WmsTransportTask>();
|
||||
public DbSet<WmsTransportReservation> WmsTransportReservations => Set<WmsTransportReservation>();
|
||||
public DbSet<WmsTransportTaskHistory> WmsTransportTaskHistories => Set<WmsTransportTaskHistory>();
|
||||
public DbSet<SimpleField> SimpleFields => Set<SimpleField>();
|
||||
public DbSet<UserDashboardShortcut> UserDashboardShortcuts => Set<UserDashboardShortcut>();
|
||||
}
|
||||
@@ -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<IEntityModule> _modules;
|
||||
|
||||
public MiGuDbContext(DbContextOptions<MiGuDbContext> options, IEnumerable<IEntityModule>? modules = null)
|
||||
: base(options)
|
||||
{
|
||||
_modules = modules ?? Array.Empty<IEntityModule>();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 全局模型约定:Guid/DateTimeOffset 字符串化、枚举存字符串、软删过滤、Version 并发令牌等。
|
||||
/// </summary>
|
||||
public static class ModelConventionExtensions
|
||||
{
|
||||
public static void ApplyMiGuConventions(ModelBuilder modelBuilder)
|
||||
{
|
||||
// 与历史 Sqlite TEXT 列兼容:Guid / DateTimeOffset 均以字符串落库
|
||||
var guid = new ValueConverter<Guid, string>(v => v.ToString("D"), v => Guid.Parse(v));
|
||||
var nullableGuid = new ValueConverter<Guid?, string?>(
|
||||
v => v.HasValue ? v.Value.ToString("D") : null,
|
||||
v => string.IsNullOrWhiteSpace(v) ? null : Guid.Parse(v));
|
||||
var dto = new ValueConverter<DateTimeOffset, string>(
|
||||
v => v.UtcDateTime.ToString("O"),
|
||||
v => DateTimeOffset.Parse(v));
|
||||
var nullableDto = new ValueConverter<DateTimeOffset?, string?>(
|
||||
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<string>().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<TEntity>(ModelBuilder modelBuilder)
|
||||
where TEntity : class, ISoftDeletable
|
||||
=> modelBuilder.Entity<TEntity>().HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
|
||||
public sealed class ActorContextAccessor : IActorContextAccessor
|
||||
{
|
||||
private static readonly AsyncLocal<IActorContext?> 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<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
|
||||
{
|
||||
Stamp(eventData.Context);
|
||||
return base.SavingChanges(eventData, result);
|
||||
}
|
||||
|
||||
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
|
||||
DbContextEventData eventData, InterceptionResult<int> 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<Guid> { Id: var id } && id == Guid.Empty)
|
||||
((IEntity<Guid>)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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using MiGu.DB.Kernel.Context;
|
||||
|
||||
namespace MiGu.DB.Kernel.Design;
|
||||
|
||||
/// <summary>
|
||||
/// design-time 工厂(dotnet ef migrations add)。指定 MigrationsAssembly,与运行时 SqliteProviderSetup 一致。
|
||||
/// </summary>
|
||||
public sealed class MiGuDbContextFactory : IDesignTimeDbContextFactory<MiGuDbContext>
|
||||
{
|
||||
public MiGuDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<MiGuDbContext>()
|
||||
.UseSqlite("Data Source=platform.db", o =>
|
||||
o.MigrationsAssembly(typeof(MiGuDbContext).Assembly.GetName().Name))
|
||||
.Options;
|
||||
return new MiGuDbContext(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using MiGu.DB.Abstractions.Entities;
|
||||
|
||||
namespace MiGu.DB.Kernel.Entities;
|
||||
|
||||
public abstract class Entity<TKey> : IEntity<TKey>
|
||||
{
|
||||
public TKey Id { get; set; } = default!;
|
||||
}
|
||||
|
||||
public abstract class AuditedEntity : Entity<Guid>, 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<Guid>, 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; } = "{}";
|
||||
}
|
||||
@@ -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<MiGuDbOptions>? 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<IActorContextAccessor, ActorContextAccessor>();
|
||||
services.TryAddScoped<IActorContext>(sp => sp.GetRequiredService<IActorContextAccessor>().Current);
|
||||
services.AddSingleton<AuditSaveChangesInterceptor>();
|
||||
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDbProviderSetup, SqliteProviderSetup>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDbProviderSetup, MySqlProviderSetup>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDbProviderSetup, NpgsqlProviderSetup>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDbProviderSetup, SqlServerProviderSetup>());
|
||||
|
||||
services.AddDbContext<MiGuDbContext>((sp, builder) =>
|
||||
{
|
||||
var opt = sp.GetRequiredService<MiGuDbOptions>();
|
||||
if (string.IsNullOrWhiteSpace(opt.ContentRootPath))
|
||||
{
|
||||
var env = sp.GetService<IHostEnvironment>();
|
||||
opt.ContentRootPath = env?.ContentRootPath ?? AppContext.BaseDirectory;
|
||||
}
|
||||
|
||||
var providerName = NormalizeProviderName(opt.Provider);
|
||||
var setup = sp.GetServices<IDbProviderSetup>()
|
||||
.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<AuditSaveChangesInterceptor>());
|
||||
});
|
||||
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
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<TModule>(this IServiceCollection services)
|
||||
where TModule : class, IEntityModule, new()
|
||||
{
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IEntityModule, TModule>());
|
||||
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<MiGuSchemaMode>(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
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动期数据库初始化:按 SchemaMode 建库/迁移,再可选执行 DataMigrator。
|
||||
/// </summary>
|
||||
public static async Task MigrateMiGuDbAsync(this IServiceProvider services, CancellationToken ct = default)
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<MiGuDbContext>();
|
||||
var options = scope.ServiceProvider.GetRequiredService<MiGuDbOptions>();
|
||||
var logger = scope.ServiceProvider.GetService<ILoggerFactory>()?.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<IDataMigrator>().OrderBy(m => m.Order))
|
||||
await migrator.MigrateAsync(db, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 已有表结构但无 __EFMigrationsHistory 时:将<strong>全部</strong> pending Migration 记为已应用。
|
||||
/// 前提:库由 EnsureCreated 按「当前模型 tip」建成,与最新 Snapshot 一致;否则应删库后走 Migrate,或手工对齐。
|
||||
/// </summary>
|
||||
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<IRelationalDatabaseCreator>();
|
||||
if (!await creator.ExistsAsync(ct) || !await creator.HasTablesAsync(ct))
|
||||
return;
|
||||
|
||||
var history = db.GetService<IHistoryRepository>();
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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<TEntity, TKey> : IRepository<TEntity, TKey>
|
||||
where TEntity : class, IEntity<TKey>
|
||||
{
|
||||
protected readonly MiGuDbContext Db;
|
||||
protected DbSet<TEntity> Set => Db.Set<TEntity>();
|
||||
|
||||
public Repository(MiGuDbContext db) => Db = db;
|
||||
|
||||
public virtual IQueryable<TEntity> Query(bool asNoTracking = true)
|
||||
=> asNoTracking ? Set.AsNoTracking() : Set.AsQueryable();
|
||||
|
||||
public virtual Task<TEntity?> 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<TEntity> : Repository<TEntity, Guid>, IEditableRepository<TEntity>
|
||||
where TEntity : class, IEntity<Guid>, ISoftDeletable, IVersioned, ILockable
|
||||
{
|
||||
private readonly IActorContextAccessor _actors;
|
||||
|
||||
public EditableRepository(MiGuDbContext db, IActorContextAccessor actors) : base(db)
|
||||
=> _actors = actors;
|
||||
|
||||
public async Task<TEntity> 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<Func<TEntity, bool>> predicate, string errorMessage, CancellationToken ct = default)
|
||||
{
|
||||
if (await Set.AnyAsync(predicate, ct))
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HistoryRepository<TEntity> : Repository<TEntity, Guid>, IHistoryRepository<TEntity>
|
||||
where TEntity : class, IEntity<Guid>, 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<int> 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<CancellationToken, Task> 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>MiGu.DB</RootNamespace>
|
||||
<AssemblyName>MiGu.DB</AssemblyName>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,754 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MiGu.DB.Migrations.Sqlite
|
||||
{
|
||||
/// <summary>
|
||||
/// Sqlite 初始 Schema(平台库全量表)。旧 EnsureCreated 库启动时会先基线本迁移名,再应用后续增量。
|
||||
/// </summary>
|
||||
public partial class InitialPlatform : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "simple_fields",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
car_type = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
field_type = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
key = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
value = table.Column<string>(type: "TEXT", nullable: false),
|
||||
data_type = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
chinese = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
english = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
other = table.Column<string>(type: "TEXT", maxLength: 512, nullable: false),
|
||||
is_default = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
create_time = table.Column<string>(type: "TEXT", maxLength: 19, nullable: false),
|
||||
update_time = table.Column<string>(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<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
scope = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
keys_json = table.Column<string>(type: "text", nullable: false),
|
||||
updated_at = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
WarehouseId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
Code = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
LayoutMode = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
State = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
FromLocationType = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
FromLocationId = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
ToLocationType = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
ToLocationId = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
RelationId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
EventType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
BeforeJson = table.Column<string>(type: "text", nullable: false),
|
||||
AfterJson = table.Column<string>(type: "text", nullable: false),
|
||||
ContainerId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
Operator = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
OperatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
Source = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Reason = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
ContainerId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
LocationType = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
LocationId = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
StorageId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
LocationCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
LocationName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Status = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
EnteredAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
MaterialId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
QuantityDelta = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
|
||||
RelationId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
EventType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
BeforeJson = table.Column<string>(type: "text", nullable: false),
|
||||
AfterJson = table.Column<string>(type: "text", nullable: false),
|
||||
ContainerId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
Operator = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
OperatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
Source = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Reason = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
ContainerId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
MaterialId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
Quantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: false),
|
||||
BatchNo = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
SerialNo = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Status = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
BoundAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
LoadedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UnloadedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
AreaId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
Code = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
ContainerType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Status = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Barcode = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Length = table.Column<double>(type: "REAL", nullable: false),
|
||||
Width = table.Column<double>(type: "REAL", nullable: false),
|
||||
Height = table.Column<double>(type: "REAL", nullable: false),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
Code = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Spec = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Unit = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Category = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
BarcodePrefix = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
Code = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
TypeCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Barcode = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Spec = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Unit = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Category = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
LifecycleStatus = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
UnboundAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
EventType = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
MaterialId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
MaterialCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
MaterialName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
MaterialBarcode = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
MaterialTypeCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
ContainerId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
ContainerCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
ContainerName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
StorageId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
StorageCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
StorageName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
AreaCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
FromStorageId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
FromStorageCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
FromStorageName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
ToStorageId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
ToStorageCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
ToStorageName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
RefType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
RefId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
RefCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Operator = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
OperatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
Reason = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
AreaId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
Code = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
StorageType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
LocationKind = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
ColumnNo = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
LevelNo = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DepthNo = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SiteId = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
SiteCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Barcode = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Capacity = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Status = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Usage = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Priority = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ZoneCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
AllowInbound = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowOutbound = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
TaskId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
ContainerId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
SourceStorageId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
TargetStorageId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
Status = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
ExpiresAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
Code = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
TriggerType = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Priority = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SourceSelectorJson = table.Column<string>(type: "text", nullable: false),
|
||||
TargetSelectorJson = table.Column<string>(type: "text", nullable: false),
|
||||
TaskOptionsJson = table.Column<string>(type: "text", nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
TaskId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
FromStatus = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
ToStatus = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Operator = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
OperatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
Reason = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
ErrorMessage = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
SnapshotJson = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
BusinessType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
RuleId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
SourceStorageId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
TargetStorageId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
ContainerId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
MaterialId = table.Column<string>(type: "TEXT", maxLength: 36, nullable: true),
|
||||
Quantity = table.Column<decimal>(type: "TEXT", precision: 18, scale: 4, nullable: true),
|
||||
Status = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
DispatchMissionId = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
DeliveryId = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
DispatchStatus = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||
Reason = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
SnapshotJson = table.Column<string>(type: "text", nullable: false),
|
||||
ErrorMessage = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
TaskPriority = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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<string>(type: "TEXT", maxLength: 36, nullable: false),
|
||||
Code = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
UpdatedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DeletedAt = table.Column<string>(type: "TEXT", maxLength: 40, nullable: true),
|
||||
DeletedBy = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Version = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
IsLock = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Remark = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
Extend = table.Column<string>(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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<string>()`(严格 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 <Name> `
|
||||
--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)。
|
||||
Binary file not shown.
Reference in New Issue
Block a user