持久层重构为独立 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:
@@ -20,6 +20,8 @@ frontends/apps/simple-platform-vue/auto-imports.d.ts
|
|||||||
|
|
||||||
# 临时构建输出
|
# 临时构建输出
|
||||||
.tmp-build*/
|
.tmp-build*/
|
||||||
|
_build_out.txt
|
||||||
|
.tools/
|
||||||
|
|
||||||
# SimpleLite 运行目录
|
# SimpleLite 运行目录
|
||||||
/SimpleLite/
|
/SimpleLite/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
namespace MiGu.Server.Dashboard;
|
namespace MiGu.DB.Domains.Dashboard;
|
||||||
|
|
||||||
/// <summary>用户 Dashboard 快捷入口配置(按 user + scope 一行)。</summary>
|
|
||||||
public sealed class UserDashboardShortcut
|
public sealed class UserDashboardShortcut
|
||||||
{
|
{
|
||||||
public string UserId { get; set; } = "";
|
public string UserId { 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.
@@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiGu.Server", "MiGu.Server\MiGu.Server.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiGu.Server", "MiGu.Server\MiGu.Server.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiGu.DB", "MiGu.DB\MiGu.DB.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -13,5 +15,9 @@ Global
|
|||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using MiGu.DB.Abstractions.Exceptions;
|
||||||
using MiGu.Server.Wms;
|
using MiGu.Server.Wms;
|
||||||
|
|
||||||
namespace MiGu.Server.Controllers;
|
namespace MiGu.Server.Controllers;
|
||||||
@@ -276,15 +277,31 @@ public sealed class WmsExceptionFilter : IExceptionFilter
|
|||||||
{
|
{
|
||||||
public void OnException(ExceptionContext context)
|
public void OnException(ExceptionContext context)
|
||||||
{
|
{
|
||||||
if (context.Exception is not InvalidOperationException ex) return;
|
switch (context.Exception)
|
||||||
context.Result = new BadRequestObjectResult(new { message = ex.Message });
|
{
|
||||||
|
case ConcurrencyConflictException ex:
|
||||||
|
context.Result = new ObjectResult(new { message = ex.Message }) { StatusCode = StatusCodes.Status409Conflict };
|
||||||
|
break;
|
||||||
|
case EntityLockedException ex:
|
||||||
|
context.Result = new ObjectResult(new { message = ex.Message }) { StatusCode = StatusCodes.Status409Conflict };
|
||||||
|
break;
|
||||||
|
case EntityNotFoundException ex:
|
||||||
|
context.Result = new NotFoundObjectResult(new { message = ex.Message });
|
||||||
|
break;
|
||||||
|
case InvalidOperationException ex:
|
||||||
|
context.Result = new BadRequestObjectResult(new { message = ex.Message });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
context.ExceptionHandled = true;
|
context.ExceptionHandled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TransportRequestQuery
|
public sealed class TransportRequestQuery
|
||||||
{
|
{
|
||||||
public string TriggerType { get; set; } = WmsTransportTriggerTypes.MaterialCall;
|
public string TriggerType { get; set; } = nameof(WmsTransportTriggerType.MaterialCall);
|
||||||
public Guid? RuleId { get; set; }
|
public Guid? RuleId { get; set; }
|
||||||
public string? RequestSiteId { get; set; }
|
public string? RequestSiteId { get; set; }
|
||||||
public Guid? MaterialId { get; set; }
|
public Guid? MaterialId { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
global using PlatformDbContext = MiGu.DB.Kernel.Context.MiGuDbContext;
|
||||||
|
global using EntityBase = MiGu.DB.Kernel.Entities.AggregateRoot;
|
||||||
|
global using SimpleField = MiGu.DB.Domains.SimpleFields.SimpleField;
|
||||||
|
global using UserDashboardShortcut = MiGu.DB.Domains.Dashboard.UserDashboardShortcut;
|
||||||
|
global using Warehouse = MiGu.DB.Domains.Wms.Warehouse;
|
||||||
|
global using WarehouseArea = MiGu.DB.Domains.Wms.WarehouseArea;
|
||||||
|
global using Storage = MiGu.DB.Domains.Wms.Storage;
|
||||||
|
global using Container = MiGu.DB.Domains.Wms.Container;
|
||||||
|
global using MaterialType = MiGu.DB.Domains.Wms.MaterialType;
|
||||||
|
global using Material = MiGu.DB.Domains.Wms.Material;
|
||||||
|
global using ContainerLocation = MiGu.DB.Domains.Wms.ContainerLocation;
|
||||||
|
global using ContainerMaterial = MiGu.DB.Domains.Wms.ContainerMaterial;
|
||||||
|
global using StockEvent = MiGu.DB.Domains.Wms.StockEvent;
|
||||||
|
global using ContainerLocationHistory = MiGu.DB.Domains.Wms.ContainerLocationHistory;
|
||||||
|
global using ContainerMaterialHistory = MiGu.DB.Domains.Wms.ContainerMaterialHistory;
|
||||||
|
global using WmsTransportRule = MiGu.DB.Domains.Transport.WmsTransportRule;
|
||||||
|
global using WmsTransportTask = MiGu.DB.Domains.Transport.WmsTransportTask;
|
||||||
|
global using WmsTransportReservation = MiGu.DB.Domains.Transport.WmsTransportReservation;
|
||||||
|
global using WmsTransportTaskHistory = MiGu.DB.Domains.Transport.WmsTransportTaskHistory;
|
||||||
@@ -16,11 +16,14 @@
|
|||||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.2.0" />
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
|
</PackageReference>
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MiGu.DB\MiGu.DB.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace MiGu.Server.Persistence;
|
|
||||||
|
|
||||||
public abstract class EntityBase
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
|
||||||
public DateTimeOffset CreatedAt { get; set; }
|
|
||||||
public DateTimeOffset UpdatedAt { get; set; }
|
|
||||||
public bool IsDeleted { get; set; }
|
|
||||||
public DateTimeOffset? DeletedAt { get; set; }
|
|
||||||
public string DeletedBy { get; set; } = "";
|
|
||||||
public long Version { get; set; }
|
|
||||||
public bool IsLock { get; set; }
|
|
||||||
public string CreatedBy { get; set; } = "";
|
|
||||||
public string UpdatedBy { get; set; } = "";
|
|
||||||
public string Remark { get; set; } = "";
|
|
||||||
public string Extend { get; set; } = "{}";
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using MiGu.DB.Abstractions.Runtime;
|
||||||
|
using MiGu.DB.Kernel.Conventions;
|
||||||
|
|
||||||
|
namespace MiGu.Server.Persistence;
|
||||||
|
|
||||||
|
/// <summary>从 JWT Claims 解析当前操作者,写入 IActorContextAccessor。</summary>
|
||||||
|
public sealed class HttpActorContextMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public HttpActorContextMiddleware(RequestDelegate next) => _next = next;
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext http, IActorContextAccessor actors)
|
||||||
|
{
|
||||||
|
var name = http.User?.Identity?.Name
|
||||||
|
?? http.User?.FindFirst("sub")?.Value
|
||||||
|
?? http.User?.FindFirst("unique_name")?.Value
|
||||||
|
?? "system";
|
||||||
|
actors.Current = new HttpActorContext(name);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(http);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
actors.Current = SystemActorContext.Instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class HttpActorContext : IActorContext
|
||||||
|
{
|
||||||
|
public HttpActorContext(string name) => Name = name;
|
||||||
|
public string Name { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using MiGu.Server.Dashboard;
|
|
||||||
using MiGu.Server.Wms;
|
|
||||||
using MiGu.Server.SimpleFields;
|
|
||||||
|
|
||||||
namespace MiGu.Server.Persistence;
|
|
||||||
|
|
||||||
public sealed class PlatformDbContext : DbContext
|
|
||||||
{
|
|
||||||
public PlatformDbContext(DbContextOptions<PlatformDbContext> options) : base(options) { }
|
|
||||||
|
|
||||||
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>();
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
var guid = new ValueConverter<Guid, string>(
|
|
||||||
v => v.ToString("D"),
|
|
||||||
v => Guid.Parse(v));
|
|
||||||
var nullableGuid = new ValueConverter<Guid?, string?>(
|
|
||||||
v => v.HasValue ? v.Value.ToString("D") : null,
|
|
||||||
v => string.IsNullOrWhiteSpace(v) ? null : Guid.Parse(v));
|
|
||||||
var dateTimeOffset = new ValueConverter<DateTimeOffset, string>(
|
|
||||||
v => v.UtcDateTime.ToString("O"),
|
|
||||||
v => DateTimeOffset.Parse(v));
|
|
||||||
var nullableDateTimeOffset = new ValueConverter<DateTimeOffset?, string?>(
|
|
||||||
v => v.HasValue ? v.Value.UtcDateTime.ToString("O") : null,
|
|
||||||
v => string.IsNullOrWhiteSpace(v) ? null : DateTimeOffset.Parse(v));
|
|
||||||
|
|
||||||
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
|
||||||
{
|
|
||||||
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(Guid)))
|
|
||||||
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(guid).HasMaxLength(36);
|
|
||||||
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(Guid?)))
|
|
||||||
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(nullableGuid).HasMaxLength(36);
|
|
||||||
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)))
|
|
||||||
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(dateTimeOffset).HasMaxLength(40);
|
|
||||||
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset?)))
|
|
||||||
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(nullableDateTimeOffset).HasMaxLength(40);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigureEntityBase<Warehouse>(modelBuilder, "wms_warehouses");
|
|
||||||
ConfigureEntityBase<WarehouseArea>(modelBuilder, "wms_areas");
|
|
||||||
ConfigureEntityBase<Storage>(modelBuilder, "wms_storages");
|
|
||||||
ConfigureEntityBase<Container>(modelBuilder, "wms_containers");
|
|
||||||
ConfigureEntityBase<MaterialType>(modelBuilder, "wms_material_types");
|
|
||||||
ConfigureEntityBase<Material>(modelBuilder, "wms_materials");
|
|
||||||
ConfigureEntityBase<ContainerLocation>(modelBuilder, "wms_container_locations");
|
|
||||||
ConfigureEntityBase<ContainerMaterial>(modelBuilder, "wms_container_materials");
|
|
||||||
ConfigureEntityBase<WmsTransportRule>(modelBuilder, "wms_transport_rules");
|
|
||||||
ConfigureEntityBase<WmsTransportTask>(modelBuilder, "wms_transport_tasks");
|
|
||||||
ConfigureEntityBase<WmsTransportReservation>(modelBuilder, "wms_transport_reservations");
|
|
||||||
|
|
||||||
ConfigureHistory<ContainerLocationHistory>(modelBuilder, "wms_container_location_history");
|
|
||||||
ConfigureHistory<ContainerMaterialHistory>(modelBuilder, "wms_container_material_history");
|
|
||||||
ConfigureStockEvent(modelBuilder);
|
|
||||||
ConfigureTransportTaskHistory(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Warehouse>().HasIndex(x => x.Code).IsUnique();
|
|
||||||
modelBuilder.Entity<WarehouseArea>().HasIndex(x => x.Code).IsUnique();
|
|
||||||
modelBuilder.Entity<WarehouseArea>().HasIndex(x => x.WarehouseId);
|
|
||||||
modelBuilder.Entity<Storage>().HasIndex(x => x.Code).IsUnique();
|
|
||||||
modelBuilder.Entity<Storage>().HasIndex(x => x.AreaId);
|
|
||||||
modelBuilder.Entity<Storage>().HasIndex(x => x.Barcode);
|
|
||||||
modelBuilder.Entity<Container>().HasIndex(x => x.Code).IsUnique();
|
|
||||||
modelBuilder.Entity<MaterialType>().HasIndex(x => x.Code).IsUnique();
|
|
||||||
modelBuilder.Entity<Material>().HasIndex(x => x.Code).IsUnique();
|
|
||||||
modelBuilder.Entity<Material>().HasIndex(x => x.Barcode);
|
|
||||||
modelBuilder.Entity<Material>().HasIndex(x => new { x.LifecycleStatus, x.UpdatedAt });
|
|
||||||
modelBuilder.Entity<Material>().HasIndex(x => x.TypeCode);
|
|
||||||
modelBuilder.Entity<ContainerLocation>().HasIndex(x => x.ContainerId).IsUnique();
|
|
||||||
modelBuilder.Entity<ContainerLocation>().HasIndex(x => new { x.LocationType, x.LocationId });
|
|
||||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => x.MaterialId).IsUnique();
|
|
||||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => x.ContainerId);
|
|
||||||
|
|
||||||
modelBuilder.Entity<WmsTransportRule>().HasIndex(x => x.Code).IsUnique();
|
|
||||||
modelBuilder.Entity<WmsTransportRule>().HasIndex(x => new { x.TriggerType, x.Enabled, x.Priority });
|
|
||||||
modelBuilder.Entity<WmsTransportTask>().HasIndex(x => x.Status);
|
|
||||||
modelBuilder.Entity<WmsTransportTask>().HasIndex(x => x.ContainerId);
|
|
||||||
modelBuilder.Entity<WmsTransportTask>().HasIndex(x => x.TargetStorageId);
|
|
||||||
modelBuilder.Entity<WmsTransportReservation>().HasIndex(x => new { x.ContainerId, x.Status });
|
|
||||||
modelBuilder.Entity<WmsTransportReservation>().HasIndex(x => new { x.TargetStorageId, x.Status });
|
|
||||||
modelBuilder.Entity<WmsTransportTask>().Property(x => x.Quantity).HasPrecision(18, 4);
|
|
||||||
|
|
||||||
modelBuilder.Entity<ContainerMaterial>().Property(x => x.Quantity).HasPrecision(18, 4);
|
|
||||||
modelBuilder.Entity<ContainerMaterialHistory>().Property(x => x.QuantityDelta).HasPrecision(18, 4);
|
|
||||||
|
|
||||||
ConfigureSimpleField(modelBuilder);
|
|
||||||
ConfigureUserDashboardShortcut(modelBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureUserDashboardShortcut(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
var e = modelBuilder.Entity<UserDashboardShortcut>();
|
|
||||||
e.ToTable("user_dashboard_shortcuts");
|
|
||||||
e.HasKey(x => new { x.UserId, x.Scope });
|
|
||||||
e.Property(x => x.UserId).HasColumnName("user_id").HasMaxLength(64);
|
|
||||||
e.Property(x => x.Scope).HasColumnName("scope").HasMaxLength(32);
|
|
||||||
e.Property(x => x.KeysJson).HasColumnName("keys_json").HasColumnType("text");
|
|
||||||
var dateTime = new ValueConverter<DateTimeOffset, string>(
|
|
||||||
v => v.UtcDateTime.ToString("O"),
|
|
||||||
v => DateTimeOffset.Parse(v));
|
|
||||||
e.Property(x => x.UpdatedAt).HasColumnName("updated_at").HasConversion(dateTime).HasMaxLength(40);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureSimpleField(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
var e = modelBuilder.Entity<SimpleField>();
|
|
||||||
e.ToTable("simple_fields");
|
|
||||||
e.HasKey(x => x.Id);
|
|
||||||
e.Property(x => x.Id).HasColumnName("id");
|
|
||||||
e.Property(x => x.CarType).HasColumnName("car_type").HasMaxLength(64);
|
|
||||||
e.Property(x => x.FieldType).HasColumnName("field_type").HasMaxLength(64);
|
|
||||||
e.Property(x => x.Key).HasColumnName("key").HasMaxLength(128);
|
|
||||||
e.Property(x => x.Value).HasColumnName("value");
|
|
||||||
e.Property(x => x.DataType).HasColumnName("data_type").HasMaxLength(128);
|
|
||||||
e.Property(x => x.Chinese).HasColumnName("chinese").HasMaxLength(256).IsRequired(false);
|
|
||||||
e.Property(x => x.English).HasColumnName("english").HasMaxLength(256).IsRequired(false);
|
|
||||||
e.Property(x => x.Other).HasColumnName("other").HasMaxLength(512);
|
|
||||||
e.Property(x => x.IsDefault).HasColumnName("is_default");
|
|
||||||
var dateTime = new ValueConverter<DateTimeOffset, string>(
|
|
||||||
v => SimpleFieldDateTime.ToStorage(v),
|
|
||||||
v => SimpleFieldDateTime.FromStorage(v));
|
|
||||||
e.Property(x => x.CreateTime).HasColumnName("create_time").HasConversion(dateTime).HasMaxLength(19);
|
|
||||||
e.Property(x => x.UpdateTime).HasColumnName("update_time").HasConversion(dateTime).HasMaxLength(19);
|
|
||||||
e.HasIndex(x => new { x.CarType, x.FieldType, x.Key }).IsUnique();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
|
||||||
{
|
|
||||||
StampEntities();
|
|
||||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
StampEntities();
|
|
||||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StampEntities()
|
|
||||||
{
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
foreach (var e in ChangeTracker.Entries<EntityBase>())
|
|
||||||
{
|
|
||||||
if (e.State == EntityState.Added)
|
|
||||||
{
|
|
||||||
if (e.Entity.Id == Guid.Empty) e.Entity.Id = Guid.NewGuid();
|
|
||||||
e.Entity.CreatedAt = now;
|
|
||||||
e.Entity.UpdatedAt = now;
|
|
||||||
e.Entity.Version = Math.Max(1, e.Entity.Version);
|
|
||||||
if (string.IsNullOrWhiteSpace(e.Entity.Extend)) e.Entity.Extend = "{}";
|
|
||||||
}
|
|
||||||
else if (e.State == EntityState.Modified)
|
|
||||||
{
|
|
||||||
e.Entity.UpdatedAt = now;
|
|
||||||
e.Entity.Version += 1;
|
|
||||||
if (string.IsNullOrWhiteSpace(e.Entity.Extend)) e.Entity.Extend = "{}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureEntityBase<T>(ModelBuilder modelBuilder, string table) where T : EntityBase
|
|
||||||
{
|
|
||||||
var e = modelBuilder.Entity<T>();
|
|
||||||
e.ToTable(table);
|
|
||||||
e.HasKey(x => x.Id);
|
|
||||||
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
|
||||||
e.Property(x => x.UpdatedBy).HasMaxLength(128);
|
|
||||||
e.Property(x => x.DeletedBy).HasMaxLength(128);
|
|
||||||
e.Property(x => x.Remark).HasMaxLength(1000);
|
|
||||||
e.Property(x => x.Extend).HasColumnType("text");
|
|
||||||
e.HasQueryFilter(x => !x.IsDeleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureHistory<T>(ModelBuilder modelBuilder, string table) where T : WarehouseHistoryBase
|
|
||||||
{
|
|
||||||
var e = modelBuilder.Entity<T>();
|
|
||||||
e.ToTable(table);
|
|
||||||
e.HasKey(x => x.Id);
|
|
||||||
e.Property(x => x.EventType).HasMaxLength(64);
|
|
||||||
e.Property(x => x.BeforeJson).HasColumnType("text");
|
|
||||||
e.Property(x => x.AfterJson).HasColumnType("text");
|
|
||||||
e.Property(x => x.Operator).HasMaxLength(128);
|
|
||||||
e.Property(x => x.Source).HasMaxLength(32);
|
|
||||||
e.Property(x => x.Reason).HasMaxLength(500);
|
|
||||||
e.Property(x => x.Remark).HasMaxLength(1000);
|
|
||||||
e.Property(x => x.Extend).HasColumnType("text");
|
|
||||||
e.HasIndex(x => x.ContainerId);
|
|
||||||
e.HasIndex(x => x.OperatedAt);
|
|
||||||
e.HasIndex(x => x.EventType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureStockEvent(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
var e = modelBuilder.Entity<StockEvent>();
|
|
||||||
e.ToTable("wms_stock_events");
|
|
||||||
e.HasKey(x => x.Id);
|
|
||||||
e.Property(x => x.EventType).HasMaxLength(32);
|
|
||||||
e.Property(x => x.MaterialCode).HasMaxLength(64);
|
|
||||||
e.Property(x => x.MaterialName).HasMaxLength(128);
|
|
||||||
e.Property(x => x.MaterialBarcode).HasMaxLength(128);
|
|
||||||
e.Property(x => x.MaterialTypeCode).HasMaxLength(64);
|
|
||||||
e.Property(x => x.ContainerCode).HasMaxLength(64);
|
|
||||||
e.Property(x => x.ContainerName).HasMaxLength(128);
|
|
||||||
e.Property(x => x.StorageCode).HasMaxLength(64);
|
|
||||||
e.Property(x => x.StorageName).HasMaxLength(128);
|
|
||||||
e.Property(x => x.AreaCode).HasMaxLength(64);
|
|
||||||
e.Property(x => x.FromStorageCode).HasMaxLength(64);
|
|
||||||
e.Property(x => x.FromStorageName).HasMaxLength(128);
|
|
||||||
e.Property(x => x.ToStorageCode).HasMaxLength(64);
|
|
||||||
e.Property(x => x.ToStorageName).HasMaxLength(128);
|
|
||||||
e.Property(x => x.RefType).HasMaxLength(64);
|
|
||||||
e.Property(x => x.RefCode).HasMaxLength(64);
|
|
||||||
e.Property(x => x.Operator).HasMaxLength(128);
|
|
||||||
e.Property(x => x.Reason).HasMaxLength(500);
|
|
||||||
e.HasIndex(x => x.OperatedAt);
|
|
||||||
e.HasIndex(x => x.EventType);
|
|
||||||
e.HasIndex(x => x.MaterialId);
|
|
||||||
e.HasIndex(x => x.ContainerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureTransportTaskHistory(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
var e = modelBuilder.Entity<WmsTransportTaskHistory>();
|
|
||||||
e.ToTable("wms_transport_task_history");
|
|
||||||
e.HasKey(x => x.Id);
|
|
||||||
e.Property(x => x.FromStatus).HasMaxLength(32);
|
|
||||||
e.Property(x => x.ToStatus).HasMaxLength(32);
|
|
||||||
e.Property(x => x.Operator).HasMaxLength(128);
|
|
||||||
e.Property(x => x.Reason).HasMaxLength(500);
|
|
||||||
e.Property(x => x.ErrorMessage).HasMaxLength(1000);
|
|
||||||
e.Property(x => x.SnapshotJson).HasColumnType("text");
|
|
||||||
e.HasIndex(x => x.TaskId);
|
|
||||||
e.HasIndex(x => x.OperatedAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,464 +1,28 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using MiGu.DB.Kernel.Hosting;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MiGu.Server.Wms;
|
|
||||||
using MiGu.Server.SimpleFields;
|
|
||||||
|
|
||||||
namespace MiGu.Server.Persistence;
|
namespace MiGu.Server.Persistence;
|
||||||
|
|
||||||
|
/// <summary>兼容入口:转调 MiGu.DB Hosting,并注册本进程业务服务。</summary>
|
||||||
public static class PlatformPersistence
|
public static class PlatformPersistence
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddPlatformPersistence(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddPlatformPersistence(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddDbContext<PlatformDbContext>((sp, options) =>
|
services.AddMiGuDb(configuration);
|
||||||
{
|
|
||||||
var env = sp.GetRequiredService<IWebHostEnvironment>();
|
|
||||||
var provider = configuration["Database:Provider"] ?? "sqlite";
|
|
||||||
var connection = ResolveConnectionString(configuration, env, provider);
|
|
||||||
|
|
||||||
switch (provider.Trim().ToLowerInvariant())
|
services.AddScoped<Wms.WmsReferenceValidator>();
|
||||||
{
|
services.AddScoped<Wms.WmsService>();
|
||||||
case "sqlite":
|
services.AddScoped<Wms.WmsTransportRuleService>();
|
||||||
options.UseSqlite(connection);
|
services.AddScoped<Wms.WmsTransportPlanner>();
|
||||||
break;
|
services.AddScoped<Wms.WmsTransportTaskService>();
|
||||||
case "mysql":
|
services.AddScoped<Wms.IWmsDispatchAdapter, Wms.NoopWmsDispatchAdapter>();
|
||||||
options.UseMySql(connection, ServerVersion.AutoDetect(connection));
|
services.AddScoped<SimpleFields.SimpleFieldService>();
|
||||||
break;
|
services.AddScoped<Dashboard.DashboardShortcutService>();
|
||||||
case "postgres":
|
|
||||||
case "postgresql":
|
|
||||||
case "npgsql":
|
|
||||||
options.UseNpgsql(connection);
|
|
||||||
break;
|
|
||||||
case "sqlserver":
|
|
||||||
case "mssql":
|
|
||||||
options.UseSqlServer(connection);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"未知数据库 Provider: {provider}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddScoped<WmsReferenceValidator>();
|
|
||||||
services.AddScoped<WmsService>();
|
|
||||||
services.AddScoped<WmsTransportRuleService>();
|
|
||||||
services.AddScoped<WmsTransportPlanner>();
|
|
||||||
services.AddScoped<WmsTransportTaskService>();
|
|
||||||
services.AddScoped<IWmsDispatchAdapter, NoopWmsDispatchAdapter>();
|
|
||||||
services.AddScoped<SimpleFieldService>();
|
|
||||||
services.AddScoped<MiGu.Server.Dashboard.DashboardShortcutService>();
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task EnsurePlatformDatabaseAsync(this IServiceProvider services)
|
/// <summary>启动建库/迁移入口(内部即 MigrateMiGuDbAsync:Migrate + 基线 + DataMigrator)。</summary>
|
||||||
{
|
public static Task EnsurePlatformDatabaseAsync(this IServiceProvider services)
|
||||||
using var scope = services.CreateScope();
|
=> services.MigrateMiGuDbAsync();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<PlatformDbContext>();
|
|
||||||
await db.Database.EnsureCreatedAsync();
|
|
||||||
// EnsureCreated 只在「库文件不存在」时建表;已有 platform.db 时新增实体不会自动补表。
|
|
||||||
await EnsureSimpleFieldsTableAsync(db);
|
|
||||||
await EnsureUserDashboardShortcutsTableAsync(db);
|
|
||||||
await EnsureWmsTransportSchemaAsync(db);
|
|
||||||
await EnsureWmsStructureSchemaAsync(db);
|
|
||||||
await MigrateWmsLegacyAsync(scope.ServiceProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task MigrateWmsLegacyAsync(IServiceProvider sp)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var wms = sp.GetRequiredService<WmsService>();
|
|
||||||
await wms.MigrateLegacyAsync("system");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// 首次建库或缺列时忽略,后续请求可再触发
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>补建仓库/物料类型/库存事件及结构扩展列(幂等,SQLite)。</summary>
|
|
||||||
private static async Task EnsureWmsStructureSchemaAsync(PlatformDbContext db)
|
|
||||||
{
|
|
||||||
if (!db.Database.IsSqlite()) return;
|
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS wms_warehouses (
|
|
||||||
Id TEXT NOT NULL CONSTRAINT PK_wms_warehouses PRIMARY KEY,
|
|
||||||
Code TEXT NOT NULL,
|
|
||||||
Name TEXT NOT NULL,
|
|
||||||
Type TEXT NOT NULL,
|
|
||||||
Enabled INTEGER NOT NULL,
|
|
||||||
SortOrder INTEGER NOT NULL,
|
|
||||||
CreatedAt TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT NOT NULL,
|
|
||||||
IsDeleted INTEGER NOT NULL,
|
|
||||||
DeletedAt TEXT,
|
|
||||||
DeletedBy TEXT NOT NULL DEFAULT '',
|
|
||||||
Version INTEGER NOT NULL,
|
|
||||||
IsLock INTEGER NOT NULL,
|
|
||||||
CreatedBy TEXT NOT NULL DEFAULT '',
|
|
||||||
UpdatedBy TEXT NOT NULL DEFAULT '',
|
|
||||||
Remark TEXT NOT NULL DEFAULT '',
|
|
||||||
Extend TEXT NOT NULL DEFAULT '{{}}'
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_warehouses_Code ON wms_warehouses (Code);");
|
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS wms_material_types (
|
|
||||||
Id TEXT NOT NULL CONSTRAINT PK_wms_material_types PRIMARY KEY,
|
|
||||||
Code TEXT NOT NULL,
|
|
||||||
Name TEXT NOT NULL,
|
|
||||||
Spec TEXT NOT NULL DEFAULT '',
|
|
||||||
Unit TEXT NOT NULL DEFAULT 'pcs',
|
|
||||||
Category TEXT NOT NULL DEFAULT '',
|
|
||||||
BarcodePrefix TEXT NOT NULL DEFAULT '',
|
|
||||||
Enabled INTEGER NOT NULL,
|
|
||||||
CreatedAt TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT NOT NULL,
|
|
||||||
IsDeleted INTEGER NOT NULL,
|
|
||||||
DeletedAt TEXT,
|
|
||||||
DeletedBy TEXT NOT NULL DEFAULT '',
|
|
||||||
Version INTEGER NOT NULL,
|
|
||||||
IsLock INTEGER NOT NULL,
|
|
||||||
CreatedBy TEXT NOT NULL DEFAULT '',
|
|
||||||
UpdatedBy TEXT NOT NULL DEFAULT '',
|
|
||||||
Remark TEXT NOT NULL DEFAULT '',
|
|
||||||
Extend TEXT NOT NULL DEFAULT '{{}}'
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_material_types_Code ON wms_material_types (Code);");
|
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS wms_stock_events (
|
|
||||||
Id TEXT NOT NULL CONSTRAINT PK_wms_stock_events PRIMARY KEY,
|
|
||||||
EventType TEXT NOT NULL,
|
|
||||||
MaterialId TEXT,
|
|
||||||
MaterialCode TEXT NOT NULL DEFAULT '',
|
|
||||||
MaterialName TEXT NOT NULL DEFAULT '',
|
|
||||||
MaterialBarcode TEXT NOT NULL DEFAULT '',
|
|
||||||
MaterialTypeCode TEXT NOT NULL DEFAULT '',
|
|
||||||
ContainerId TEXT,
|
|
||||||
ContainerCode TEXT NOT NULL DEFAULT '',
|
|
||||||
ContainerName TEXT NOT NULL DEFAULT '',
|
|
||||||
StorageId TEXT,
|
|
||||||
StorageCode TEXT NOT NULL DEFAULT '',
|
|
||||||
StorageName TEXT NOT NULL DEFAULT '',
|
|
||||||
AreaCode TEXT NOT NULL DEFAULT '',
|
|
||||||
FromStorageId TEXT,
|
|
||||||
FromStorageCode TEXT NOT NULL DEFAULT '',
|
|
||||||
FromStorageName TEXT NOT NULL DEFAULT '',
|
|
||||||
ToStorageId TEXT,
|
|
||||||
ToStorageCode TEXT NOT NULL DEFAULT '',
|
|
||||||
ToStorageName TEXT NOT NULL DEFAULT '',
|
|
||||||
RefType TEXT NOT NULL DEFAULT 'Manual',
|
|
||||||
RefId TEXT,
|
|
||||||
RefCode TEXT NOT NULL DEFAULT '',
|
|
||||||
Operator TEXT NOT NULL DEFAULT '',
|
|
||||||
OperatedAt TEXT NOT NULL,
|
|
||||||
Reason TEXT NOT NULL DEFAULT ''
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_wms_stock_events_OperatedAt ON wms_stock_events (OperatedAt);");
|
|
||||||
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_areas", "WarehouseId", "TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_areas", "LayoutMode", "TEXT NOT NULL DEFAULT 'Flat'");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_areas", "State", "TEXT NOT NULL DEFAULT 'Default'");
|
|
||||||
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "LocationKind", "TEXT NOT NULL DEFAULT 'Station'");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "ColumnNo", "INTEGER NOT NULL DEFAULT 0");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "LevelNo", "INTEGER NOT NULL DEFAULT 1");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "DepthNo", "INTEGER NOT NULL DEFAULT 1");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "SiteCode", "TEXT NOT NULL DEFAULT ''");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
|
||||||
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_containers", "AreaId", "TEXT");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_containers", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_containers", "Length", "REAL NOT NULL DEFAULT 0");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_containers", "Width", "REAL NOT NULL DEFAULT 0");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_containers", "Height", "REAL NOT NULL DEFAULT 0");
|
|
||||||
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_materials", "TypeCode", "TEXT NOT NULL DEFAULT ''");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_materials", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_materials", "LifecycleStatus", "TEXT NOT NULL DEFAULT 'Active'");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_materials", "UnboundAt", "TEXT");
|
|
||||||
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_container_materials", "BoundAt", "TEXT NOT NULL DEFAULT ''");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>为已存在的数据库补建 simple_fields 表(幂等)。</summary>
|
|
||||||
private static async Task EnsureSimpleFieldsTableAsync(PlatformDbContext db)
|
|
||||||
{
|
|
||||||
if (db.Database.IsSqlite())
|
|
||||||
{
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS simple_fields (
|
|
||||||
id TEXT NOT NULL CONSTRAINT PK_simple_fields PRIMARY KEY,
|
|
||||||
car_type TEXT NOT NULL DEFAULT '',
|
|
||||||
field_type TEXT NOT NULL,
|
|
||||||
"key" TEXT NOT NULL,
|
|
||||||
value TEXT NOT NULL DEFAULT '',
|
|
||||||
data_type TEXT NOT NULL DEFAULT '',
|
|
||||||
chinese TEXT,
|
|
||||||
english TEXT,
|
|
||||||
other TEXT NOT NULL DEFAULT '',
|
|
||||||
is_default INTEGER NOT NULL,
|
|
||||||
create_time TEXT NOT NULL,
|
|
||||||
update_time TEXT NOT NULL
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
// 须先删旧索引 (field_type, other, key):把 other 清空为「其他语言」后会与旧唯一约束冲突。
|
|
||||||
await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS IX_simple_fields_field_type_other_key;");
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
UPDATE simple_fields SET car_type = other
|
|
||||||
WHERE (car_type IS NULL OR car_type = '') AND other <> '';
|
|
||||||
""");
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
UPDATE simple_fields SET other = ''
|
|
||||||
WHERE other <> '' AND other = car_type;
|
|
||||||
""");
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS IX_simple_fields_car_type_field_type_key
|
|
||||||
ON simple_fields (car_type, field_type, "key");
|
|
||||||
""");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非 SQLite:表不存在时尝试按当前模型创建(已有库不会走 EnsureCreated)。
|
|
||||||
if (!await TableExistsAsync(db, "simple_fields"))
|
|
||||||
{
|
|
||||||
var creator = db.GetService<Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator>();
|
|
||||||
await creator.CreateTablesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>为已存在的数据库补建 WMS 搬运规则/任务相关表及库位扩展列(幂等)。</summary>
|
|
||||||
private static async Task EnsureWmsTransportSchemaAsync(PlatformDbContext db)
|
|
||||||
{
|
|
||||||
if (!db.Database.IsSqlite()) return;
|
|
||||||
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "Status", "TEXT NOT NULL DEFAULT 'Available'");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "Usage", "TEXT NOT NULL DEFAULT ''");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "Priority", "INTEGER NOT NULL DEFAULT 0");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "ZoneCode", "TEXT NOT NULL DEFAULT ''");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "AllowInbound", "INTEGER NOT NULL DEFAULT 1");
|
|
||||||
await EnsureSqliteColumnAsync(db, "wms_storages", "AllowOutbound", "INTEGER NOT NULL DEFAULT 1");
|
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS wms_transport_rules (
|
|
||||||
Id TEXT NOT NULL CONSTRAINT PK_wms_transport_rules PRIMARY KEY,
|
|
||||||
CreatedAt TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT NOT NULL,
|
|
||||||
IsDeleted INTEGER NOT NULL,
|
|
||||||
DeletedAt TEXT,
|
|
||||||
DeletedBy TEXT,
|
|
||||||
Version INTEGER NOT NULL,
|
|
||||||
IsLock INTEGER NOT NULL,
|
|
||||||
CreatedBy TEXT NOT NULL,
|
|
||||||
UpdatedBy TEXT NOT NULL,
|
|
||||||
Remark TEXT NOT NULL,
|
|
||||||
Extend TEXT NOT NULL,
|
|
||||||
Code TEXT NOT NULL,
|
|
||||||
Name TEXT NOT NULL,
|
|
||||||
TriggerType TEXT NOT NULL,
|
|
||||||
Enabled INTEGER NOT NULL,
|
|
||||||
Priority INTEGER NOT NULL,
|
|
||||||
SourceSelectorJson TEXT NOT NULL,
|
|
||||||
TargetSelectorJson TEXT NOT NULL,
|
|
||||||
TaskOptionsJson TEXT NOT NULL
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_transport_rules_Code ON wms_transport_rules (Code);
|
|
||||||
""");
|
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS wms_transport_tasks (
|
|
||||||
Id TEXT NOT NULL CONSTRAINT PK_wms_transport_tasks PRIMARY KEY,
|
|
||||||
CreatedAt TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT NOT NULL,
|
|
||||||
IsDeleted INTEGER NOT NULL,
|
|
||||||
DeletedAt TEXT,
|
|
||||||
DeletedBy TEXT,
|
|
||||||
Version INTEGER NOT NULL,
|
|
||||||
IsLock INTEGER NOT NULL,
|
|
||||||
CreatedBy TEXT NOT NULL,
|
|
||||||
UpdatedBy TEXT NOT NULL,
|
|
||||||
Remark TEXT NOT NULL,
|
|
||||||
Extend TEXT NOT NULL,
|
|
||||||
BusinessType TEXT NOT NULL,
|
|
||||||
RuleId TEXT,
|
|
||||||
SourceStorageId TEXT NOT NULL,
|
|
||||||
TargetStorageId TEXT NOT NULL,
|
|
||||||
ContainerId TEXT NOT NULL,
|
|
||||||
MaterialId TEXT,
|
|
||||||
Quantity REAL,
|
|
||||||
Status TEXT NOT NULL,
|
|
||||||
DispatchMissionId TEXT NOT NULL DEFAULT '',
|
|
||||||
DeliveryId TEXT NOT NULL DEFAULT '',
|
|
||||||
DispatchStatus TEXT NOT NULL DEFAULT '',
|
|
||||||
Reason TEXT NOT NULL DEFAULT '',
|
|
||||||
SnapshotJson TEXT NOT NULL,
|
|
||||||
ErrorMessage TEXT NOT NULL DEFAULT '',
|
|
||||||
TaskPriority INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS wms_transport_reservations (
|
|
||||||
Id TEXT NOT NULL CONSTRAINT PK_wms_transport_reservations PRIMARY KEY,
|
|
||||||
CreatedAt TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT NOT NULL,
|
|
||||||
IsDeleted INTEGER NOT NULL,
|
|
||||||
DeletedAt TEXT,
|
|
||||||
DeletedBy TEXT,
|
|
||||||
Version INTEGER NOT NULL,
|
|
||||||
IsLock INTEGER NOT NULL,
|
|
||||||
CreatedBy TEXT NOT NULL,
|
|
||||||
UpdatedBy TEXT NOT NULL,
|
|
||||||
Remark TEXT NOT NULL,
|
|
||||||
Extend TEXT NOT NULL,
|
|
||||||
TaskId TEXT NOT NULL,
|
|
||||||
ContainerId TEXT NOT NULL,
|
|
||||||
SourceStorageId TEXT NOT NULL,
|
|
||||||
TargetStorageId TEXT NOT NULL,
|
|
||||||
Status TEXT NOT NULL,
|
|
||||||
ExpiresAt TEXT
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS wms_transport_task_history (
|
|
||||||
Id TEXT NOT NULL CONSTRAINT PK_wms_transport_task_history PRIMARY KEY,
|
|
||||||
TaskId TEXT NOT NULL,
|
|
||||||
FromStatus TEXT NOT NULL,
|
|
||||||
ToStatus TEXT NOT NULL,
|
|
||||||
Operator TEXT NOT NULL,
|
|
||||||
OperatedAt TEXT NOT NULL,
|
|
||||||
Reason TEXT NOT NULL,
|
|
||||||
ErrorMessage TEXT NOT NULL,
|
|
||||||
SnapshotJson TEXT NOT NULL
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task EnsureSqliteColumnAsync(PlatformDbContext db, string table, string column, string definition)
|
|
||||||
{
|
|
||||||
var conn = db.Database.GetDbConnection();
|
|
||||||
if (conn.State != System.Data.ConnectionState.Open)
|
|
||||||
await conn.OpenAsync();
|
|
||||||
await using var cmd = conn.CreateCommand();
|
|
||||||
cmd.CommandText = $"PRAGMA table_info({table});";
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync();
|
|
||||||
while (await reader.ReadAsync())
|
|
||||||
{
|
|
||||||
var name = reader.GetString(1);
|
|
||||||
if (string.Equals(name, column, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.Database.ExecuteSqlRawAsync($"ALTER TABLE {table} ADD COLUMN {column} {definition};");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>为已存在的数据库补建 user_dashboard_shortcuts 表(幂等)。</summary>
|
|
||||||
private static async Task EnsureUserDashboardShortcutsTableAsync(PlatformDbContext db)
|
|
||||||
{
|
|
||||||
if (db.Database.IsSqlite())
|
|
||||||
{
|
|
||||||
await db.Database.ExecuteSqlRawAsync("""
|
|
||||||
CREATE TABLE IF NOT EXISTS user_dashboard_shortcuts (
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
scope TEXT NOT NULL,
|
|
||||||
keys_json TEXT NOT NULL DEFAULT '[]',
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
CONSTRAINT PK_user_dashboard_shortcuts PRIMARY KEY (user_id, scope)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await TableExistsAsync(db, "user_dashboard_shortcuts"))
|
|
||||||
{
|
|
||||||
var creator = db.GetService<Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator>();
|
|
||||||
await creator.CreateTablesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 检查表是否存在
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="db">数据库上下文</param>
|
|
||||||
/// <param name="table">表名</param>
|
|
||||||
/// <returns>表是否存在</returns>
|
|
||||||
private static async Task<bool> TableExistsAsync(PlatformDbContext db, string table)
|
|
||||||
{
|
|
||||||
var conn = db.Database.GetDbConnection();
|
|
||||||
if (conn.State != System.Data.ConnectionState.Open)
|
|
||||||
await conn.OpenAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var cmd = conn.CreateCommand();
|
|
||||||
if (db.Database.IsSqlServer())
|
|
||||||
{
|
|
||||||
cmd.CommandText = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @t";
|
|
||||||
var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p);
|
|
||||||
}
|
|
||||||
else if (db.Database.IsNpgsql())
|
|
||||||
{
|
|
||||||
cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_name = @t";
|
|
||||||
var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p);
|
|
||||||
}
|
|
||||||
else if (db.Database.IsMySql())
|
|
||||||
{
|
|
||||||
cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = @t";
|
|
||||||
var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var result = await cmd.ExecuteScalarAsync();
|
|
||||||
return result != null;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (conn.State == System.Data.ConnectionState.Open)
|
|
||||||
await conn.CloseAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ResolveConnectionString(IConfiguration configuration, IWebHostEnvironment env, string provider)
|
|
||||||
{
|
|
||||||
var key = provider.Trim().ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
"postgres" or "postgresql" or "npgsql" => "PostgreSQL",
|
|
||||||
"mssql" => "SqlServer",
|
|
||||||
_ => provider
|
|
||||||
};
|
|
||||||
var configured = configuration.GetConnectionString(key) ?? configuration.GetConnectionString("Platform");
|
|
||||||
if (!string.IsNullOrWhiteSpace(configured))
|
|
||||||
{
|
|
||||||
return IsSqlite(provider) ? NormalizeSqliteConnection(configured, env) : configured;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dataDir = Path.Combine(env.ContentRootPath, "data");
|
|
||||||
Directory.CreateDirectory(dataDir);
|
|
||||||
return $"Data Source={Path.Combine(dataDir, "platform.db")}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSqlite(string provider) =>
|
|
||||||
string.Equals(provider.Trim(), "sqlite", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
private static string NormalizeSqliteConnection(string connection, IWebHostEnvironment env)
|
|
||||||
{
|
|
||||||
var builder = new SqliteConnectionStringBuilder(connection);
|
|
||||||
if (string.IsNullOrWhiteSpace(builder.DataSource)) return connection;
|
|
||||||
if (builder.DataSource is ":memory:") return connection;
|
|
||||||
if (!Path.IsPathRooted(builder.DataSource))
|
|
||||||
{
|
|
||||||
builder.DataSource = Path.Combine(env.ContentRootPath, builder.DataSource);
|
|
||||||
}
|
|
||||||
var dir = Path.GetDirectoryName(builder.DataSource);
|
|
||||||
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ app.UseStaticFiles();
|
|||||||
|
|
||||||
// 鉴权 / 授权管道必须放在 MapControllers 之前;CORS 之后。
|
// 鉴权 / 授权管道必须放在 MapControllers 之前;CORS 之后。
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
app.UseMiddleware<HttpActorContextMiddleware>();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|||||||
+85
-1
@@ -97,6 +97,88 @@ $env:SimpleLite__Enabled = "true"
|
|||||||
|
|
||||||
> 如果只想跑 MiGu.Server 单进程调试(不拉 SimpleLite),把 `appsettings.json:SimpleLite.Enabled` 改为 `false` 即可。
|
> 如果只想跑 MiGu.Server 单进程调试(不拉 SimpleLite),把 `appsettings.json:SimpleLite.Enabled` 改为 `false` 即可。
|
||||||
|
|
||||||
|
## 数据库启动流程
|
||||||
|
|
||||||
|
持久层在独立工程 **[MiGu.DB](../MiGu.DB/README.md)**;Server 通过 `PlatformPersistence` 接入。
|
||||||
|
|
||||||
|
### 启动顺序(`Program.cs`)
|
||||||
|
|
||||||
|
```text
|
||||||
|
AddPlatformPersistence(configuration) # 注册 AddMiGuDb + Wms/SimpleFields 等业务服务
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
builder.Build()
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
EnsurePlatformDatabaseAsync() # → MiGu.DB.MigrateMiGuDbAsync()
|
||||||
|
│ # Schema 初始化 + 可选 IDataMigrator
|
||||||
|
▼
|
||||||
|
… 其余中间件 …
|
||||||
|
UseMiddleware<HttpActorContextMiddleware> # 请求级写入 IActorContext(审计戳)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置(`appsettings*.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Database": {
|
||||||
|
"Provider": "sqlite",
|
||||||
|
"SchemaMode": "Migrate",
|
||||||
|
"ApplyDataMigratorsOnStartup": true
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Platform": "Data Source=data/platform.db"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 配置项 | 含义 | 取值 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `Provider` | 数据库种类 | `sqlite`(默认)/ `mysql` / `npgsql` / `sqlserver` |
|
||||||
|
| `SchemaMode` | 启动时如何初始化 Schema | 见下表 |
|
||||||
|
| `ApplyDataMigratorsOnStartup` | Schema 完成后是否跑数据修补 | `true`(默认)/ `false` |
|
||||||
|
|
||||||
|
当前仓库约定:
|
||||||
|
|
||||||
|
- `appsettings.json` → `"SchemaMode": "Migrate"`(发版/默认)
|
||||||
|
- `appsettings.Development.json` → `"SchemaMode": "EnsureCreated"`(本地开发)
|
||||||
|
|
||||||
|
也可用环境变量覆盖,例如:`$env:Database__SchemaMode = "Migrate"`。
|
||||||
|
|
||||||
|
### SchemaMode 行为
|
||||||
|
|
||||||
|
| 值 | 行为 | 适用 |
|
||||||
|
|----|------|------|
|
||||||
|
| **`Migrate`** | 旧库无 `__EFMigrationsHistory` 时先写 Initial 基线 → `MigrateAsync` →(可选)DataMigrator | 测试 / 预发 / 生产 |
|
||||||
|
| **`EnsureCreated`** | 仅 `EnsureCreated`(按**当前模型**建空库),**不**跑 Migrations →(可选)DataMigrator | 开发期改实体、暂不生成迁移 |
|
||||||
|
|
||||||
|
流程图:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SchemaMode = EnsureCreated
|
||||||
|
→ EnsureCreatedAsync(库已存在则不改结构)
|
||||||
|
→ IDataMigrator(ApplyDataMigratorsOnStartup=true 时)
|
||||||
|
|
||||||
|
SchemaMode = Migrate
|
||||||
|
→ 程序集无 Migration → EnsureCreated 兜底
|
||||||
|
→ 否则:基线(如需)→ MigrateAsync
|
||||||
|
→ IDataMigrator(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
**开发注意(EnsureCreated):**
|
||||||
|
|
||||||
|
- 改实体后结构不会自动升级;请删除 `MiGu.Server/data/platform.db*` 再启动。
|
||||||
|
- 不会写入迁移历史;以后改回 `Migrate` 时,对已有库会走基线再应用 Migrations。
|
||||||
|
|
||||||
|
**发版注意:**
|
||||||
|
|
||||||
|
- 现场务必使用 `Migrate`,并随包带上 `MiGu.DB/Migrations/Sqlite/`。
|
||||||
|
- 开发期若长期 `EnsureCreated` 改模型,发版前必须对**当前模型**执行 `dotnet ef migrations add`,再切回 `Migrate` 验证;否则空库只会落到旧 Migration,结构落后于代码。
|
||||||
|
- 从 EnsureCreated 库切到 `Migrate`:若无 History,启动会把**全部** pending Migration 写入基线(假定库已对齐模型 tip)。结构不对齐时应删库重建或手工处理。
|
||||||
|
|
||||||
|
### 相关文档
|
||||||
|
|
||||||
|
- 持久层总览与 SchemaMode 摘要:[MiGu.DB/README.md](../MiGu.DB/README.md)
|
||||||
|
- 如何生成/管理 Migration:[MiGu.DB/MIGRATIONS.md](../MiGu.DB/MIGRATIONS.md)
|
||||||
|
|
||||||
## 安全须知(生产部署必读)
|
## 安全须知(生产部署必读)
|
||||||
|
|
||||||
> **重要:以下默认值仅供本地开发,切勿原样用于生产环境。**
|
> **重要:以下默认值仅供本地开发,切勿原样用于生产环境。**
|
||||||
@@ -136,7 +218,7 @@ $env:SimpleLite__Enabled = "true"
|
|||||||
- 健康检查:`/api/health`;
|
- 健康检查:`/api/health`;
|
||||||
- Swagger:开发环境下 `/swagger`。
|
- Swagger:开发环境下 `/swagger`。
|
||||||
|
|
||||||
> 不在本轮范围:真实 SimpleLite WebAPI、SystemMission 拉起、真实 JWT/RBAC、SQLite/EF Core 持久层。
|
> 能力补充:平台库由 **MiGu.DB**(EF Core)承载,启动流程见「[数据库启动流程](#数据库启动流程)」。
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
@@ -242,3 +324,5 @@ YARP 路由配置见 `appsettings.json`:
|
|||||||
|
|
||||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — v1.6 总体架构(§4 进程拓扑、§6.3 YARP 配置、§9 配置中心、§10 交互序列、§17 视觉规范)
|
- [ARCHITECTURE.md](ARCHITECTURE.md) — v1.6 总体架构(§4 进程拓扑、§6.3 YARP 配置、§9 配置中心、§10 交互序列、§17 视觉规范)
|
||||||
- [(见 Simple 仓库)frontends/README.md]((见 Simple 仓库)frontends/README.md) — 前端启动与联调说明(含「迷毂」品牌与紫色主题约定)
|
- [(见 Simple 仓库)frontends/README.md]((见 Simple 仓库)frontends/README.md) — 前端启动与联调说明(含「迷毂」品牌与紫色主题约定)
|
||||||
|
- [MiGu.DB/README.md](../MiGu.DB/README.md) — 持久层框架;[MIGRATIONS.md](../MiGu.DB/MIGRATIONS.md) — 生成 Migration 手册
|
||||||
|
- 本文「[数据库启动流程](#数据库启动流程)」— SchemaMode / EnsureCreated / Migrate
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MiGu.Server.SimpleFields;
|
|
||||||
|
|
||||||
public static class SimpleFieldDateTime
|
|
||||||
{
|
|
||||||
public const string StorageFormat = "yyyy-MM-dd HH:mm:ss";
|
|
||||||
|
|
||||||
public static DateTimeOffset Now => DateTimeOffset.Now;
|
|
||||||
|
|
||||||
public static string ToStorage(DateTimeOffset value) =>
|
|
||||||
value.LocalDateTime.ToString(StorageFormat, CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
public static DateTimeOffset FromStorage(string value)
|
|
||||||
{
|
|
||||||
if (DateTime.TryParseExact(value, StorageFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var local))
|
|
||||||
return new DateTimeOffset(local);
|
|
||||||
|
|
||||||
return DateTimeOffset.Parse(value, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace MiGu.Server.SimpleFields;
|
namespace MiGu.Server.SimpleFields;
|
||||||
|
|
||||||
public sealed class SimpleField
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
|
||||||
|
|
||||||
/// <summary>车型唯一标识:assemblyName.shortName,如 StandardScene.QrLidar.Forklift。</summary>
|
|
||||||
[MaxLength(64)]
|
|
||||||
public string CarType { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 字段类型唯一标识:assemblyName.shortName,如 StandardScene.QrLidar.Forklift。
|
|
||||||
/// </summary>
|
|
||||||
[MaxLength(64)]
|
|
||||||
public string FieldType { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 字段唯一标识:key,如 Forklift.PositionX。
|
|
||||||
/// </summary>
|
|
||||||
[MaxLength(128)]
|
|
||||||
public string Key { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 字段值,如 10.0。
|
|
||||||
/// </summary>
|
|
||||||
public string Value { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 数据类型,如 System.String。
|
|
||||||
/// </summary>
|
|
||||||
[MaxLength(128)]
|
|
||||||
public string DataType { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 中文名称,如 位置 X。
|
|
||||||
/// </summary>
|
|
||||||
[MaxLength(256)]
|
|
||||||
public string? Chinese { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 英文名称,如 Position X。
|
|
||||||
/// </summary>
|
|
||||||
[MaxLength(256)]
|
|
||||||
public string? English { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 其他语言名称,如 位置 X。
|
|
||||||
/// </summary>
|
|
||||||
[MaxLength(512)]
|
|
||||||
public string Other { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否内置默认字段,如 true。
|
|
||||||
/// </summary>
|
|
||||||
public bool IsDefault { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建时间,如 2021-01-01 12:00:00。
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset CreateTime { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 更新时间,如 2021-01-01 12:00:00。
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset UpdateTime { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record SimpleFieldRequest(
|
public sealed record SimpleFieldRequest(
|
||||||
Guid? Id,
|
Guid? Id,
|
||||||
string CarType,
|
string CarType,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MiGu.Server.Persistence;
|
using MiGu.Server.Persistence;
|
||||||
|
using MiGu.DB.Domains.SimpleFields;
|
||||||
|
|
||||||
namespace MiGu.Server.SimpleFields;
|
namespace MiGu.Server.SimpleFields;
|
||||||
|
|
||||||
|
|||||||
+48
-342
@@ -1,358 +1,64 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using MiGu.Server.Persistence;
|
|
||||||
|
|
||||||
namespace MiGu.Server.Wms;
|
namespace MiGu.Server.Wms;
|
||||||
|
|
||||||
public abstract class WarehouseHistoryBase
|
// API / 读模型 DTO(留在 Server,不进 MiGu.DB)。请求字段中的状态仍为 string,由 Service 解析为枚举后写入实体。
|
||||||
{
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
|
||||||
public Guid? RelationId { get; set; }
|
|
||||||
public string EventType { get; set; } = "";
|
|
||||||
public string BeforeJson { get; set; } = "{}";
|
|
||||||
public string AfterJson { get; set; } = "{}";
|
|
||||||
public Guid? ContainerId { get; set; }
|
|
||||||
public string Operator { get; set; } = "";
|
|
||||||
public DateTimeOffset OperatedAt { get; set; }
|
|
||||||
public string Source { get; set; } = "Manual";
|
|
||||||
public string Reason { get; set; } = "";
|
|
||||||
public string Remark { get; set; } = "";
|
|
||||||
public string Extend { get; set; } = "{}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class Warehouse : EntityBase
|
public sealed record InventoryMaterialRow(
|
||||||
{
|
Guid MaterialId, string MaterialCode, string MaterialName, string MaterialBarcode, string MaterialTypeCode,
|
||||||
[MaxLength(64)] public string Code { get; set; } = "";
|
Guid ContainerId, string ContainerCode, string ContainerName,
|
||||||
[MaxLength(128)] public string Name { get; set; } = "";
|
Guid? StorageId, string StorageCode, string StorageName,
|
||||||
[MaxLength(64)] public string Type { get; set; } = "Default";
|
Guid? AreaId, string AreaCode, string AreaName,
|
||||||
public bool Enabled { get; set; } = true;
|
string LocationType, DateTimeOffset BoundAt);
|
||||||
public int SortOrder { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class WarehouseArea : EntityBase
|
public sealed record ContainerLocationSnapshot(
|
||||||
{
|
Guid Id, Guid ContainerId, string LocationType, string LocationId, string LocationCode, string LocationName,
|
||||||
public Guid WarehouseId { get; set; }
|
string Status, DateTimeOffset EnteredAt, long Version);
|
||||||
[MaxLength(64)] public string Code { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string Name { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string Type { get; set; } = "Storage";
|
|
||||||
[MaxLength(32)] public string LayoutMode { get; set; } = AreaLayoutModes.Flat;
|
|
||||||
[MaxLength(32)] public string State { get; set; } = "Default";
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
public int SortOrder { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class Storage : EntityBase
|
public sealed record ContainerMaterialSnapshot(
|
||||||
{
|
Guid Id, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo, string Status,
|
||||||
public Guid AreaId { get; set; }
|
DateTimeOffset BoundAt, DateTimeOffset LoadedAt, DateTimeOffset? UnloadedAt, long Version);
|
||||||
[MaxLength(64)] public string Code { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string Name { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string StorageType { get; set; } = "Storage";
|
|
||||||
[MaxLength(32)] public string LocationKind { get; set; } = LocationKinds.Station;
|
|
||||||
public int ColumnNo { get; set; }
|
|
||||||
public int LevelNo { get; set; } = 1;
|
|
||||||
public int DepthNo { get; set; } = 1;
|
|
||||||
[MaxLength(64)] public string SiteId { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string SiteCode { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
|
||||||
/// <summary>已废弃:一货位一容器,保留列兼容旧库。</summary>
|
|
||||||
public int Capacity { get; set; }
|
|
||||||
[MaxLength(32)] public string Status { get; set; } = StorageStatuses.Empty;
|
|
||||||
[MaxLength(64)] public string Usage { get; set; } = "";
|
|
||||||
public int Priority { get; set; }
|
|
||||||
[MaxLength(64)] public string ZoneCode { get; set; } = "";
|
|
||||||
public bool AllowInbound { get; set; } = true;
|
|
||||||
public bool AllowOutbound { get; set; } = true;
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class Container : EntityBase
|
public abstract record CommonRequest(Guid? Id, long? Version, bool IsLock, string Remark, string Extend);
|
||||||
{
|
|
||||||
public Guid? AreaId { get; set; }
|
|
||||||
[MaxLength(64)] public string Code { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string Name { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string ContainerType { get; set; } = "Box";
|
|
||||||
[MaxLength(64)] public string Status { get; set; } = ContainerStatuses.EmptyMaterial;
|
|
||||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
|
||||||
public double Length { get; set; }
|
|
||||||
public double Width { get; set; }
|
|
||||||
public double Height { get; set; }
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class MaterialType : EntityBase
|
public sealed record MasterDataRequest(
|
||||||
{
|
Guid? Id, long? Version, string Code, string Name, string Type, string Status, bool Enabled, int SortOrder,
|
||||||
[MaxLength(64)] public string Code { get; set; } = "";
|
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
[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 : EntityBase
|
public sealed record AreaRequest(
|
||||||
{
|
Guid? Id, long? Version, Guid? WarehouseId, string Code, string Name, string Type, string LayoutMode, string State,
|
||||||
[MaxLength(64)] public string Code { get; set; } = "";
|
bool Enabled, int SortOrder, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
[MaxLength(128)] public string Name { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string TypeCode { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string Spec { get; set; } = "";
|
|
||||||
[MaxLength(32)] public string Unit { get; set; } = "pcs";
|
|
||||||
[MaxLength(64)] public string Category { get; set; } = "";
|
|
||||||
[MaxLength(32)] public string LifecycleStatus { get; set; } = MaterialLifecycles.Active;
|
|
||||||
public DateTimeOffset? UnboundAt { get; set; }
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ContainerLocation : EntityBase
|
public sealed record StorageRequest(
|
||||||
{
|
Guid? Id, long? Version, Guid AreaId, string Code, string Name, string StorageType, string LocationKind,
|
||||||
public Guid ContainerId { get; set; }
|
int ColumnNo, int LevelNo, int DepthNo, string SiteId, string SiteCode, string Barcode, int Capacity,
|
||||||
[MaxLength(32)] public string LocationType { get; set; } = ContainerLocationTypes.Storage;
|
string Status, string Usage, int Priority, string ZoneCode, bool AllowInbound, bool AllowOutbound, bool Enabled,
|
||||||
[MaxLength(64)] public string LocationId { get; set; } = "";
|
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
[MaxLength(64)] public string LocationCode { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string LocationName { get; set; } = "";
|
|
||||||
[MaxLength(32)] public string Status { get; set; } = ContainerLocationStatuses.Active;
|
|
||||||
public DateTimeOffset EnteredAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>容器-物料绑定(无数量语义;Quantity 列仅兼容旧库,固定为 1)。</summary>
|
public sealed record GenerateBinsRequest(int ColumnFrom, int ColumnTo, int LevelFrom, int LevelTo, int DepthFrom, int DepthTo, string? CodePattern);
|
||||||
public sealed class ContainerMaterial : EntityBase
|
|
||||||
{
|
|
||||||
public Guid ContainerId { get; set; }
|
|
||||||
public Guid MaterialId { get; set; }
|
|
||||||
public decimal Quantity { get; set; } = 1;
|
|
||||||
[MaxLength(64)] public string BatchNo { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string SerialNo { get; set; } = "";
|
|
||||||
[MaxLength(32)] public string Status { get; set; } = ContainerMaterialStatuses.Bound;
|
|
||||||
public DateTimeOffset BoundAt { get; set; }
|
|
||||||
public DateTimeOffset LoadedAt { get; set; }
|
|
||||||
public DateTimeOffset? UnloadedAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class StockEvent
|
public sealed record ContainerRequest(
|
||||||
{
|
Guid? Id, long? Version, Guid? AreaId, string Code, string Name, string Type, string Status, string Barcode,
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
double Length, double Width, double Height, bool Enabled, bool IsLock, string Remark, string Extend)
|
||||||
[MaxLength(32)] public string EventType { get; set; } = "";
|
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
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 : WarehouseHistoryBase
|
public sealed record MaterialTypeRequest(
|
||||||
{
|
Guid? Id, long? Version, string Code, string Name, string Spec, string Unit, string Category, string BarcodePrefix,
|
||||||
[MaxLength(32)] public string FromLocationType { get; set; } = "";
|
bool Enabled, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
[MaxLength(64)] public string FromLocationId { get; set; } = "";
|
|
||||||
[MaxLength(32)] public string ToLocationType { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string ToLocationId { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ContainerMaterialHistory : WarehouseHistoryBase
|
public sealed record MaterialRequest(
|
||||||
{
|
Guid? Id, long? Version, string Code, string Name, string TypeCode, string Barcode, string Spec, string Unit,
|
||||||
public Guid? MaterialId { get; set; }
|
string Category, string LifecycleStatus, bool Enabled, bool IsLock, string Remark, string Extend)
|
||||||
public decimal QuantityDelta { get; set; }
|
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
}
|
|
||||||
|
|
||||||
public static class AreaLayoutModes
|
public sealed record ContainerLocationRequest(
|
||||||
{
|
Guid? Id, long? Version, Guid ContainerId, string LocationType, string LocationId, string Status,
|
||||||
public const string Flat = "Flat";
|
DateTimeOffset? EnteredAt, string Source, string Reason, bool IsLock, string Remark, string Extend)
|
||||||
public const string Grid = "Grid";
|
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Flat, Grid };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LocationKinds
|
public sealed record BindMaterialRequest(
|
||||||
{
|
Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, string Source, string Reason,
|
||||||
public const string Grid = "Grid";
|
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
public const string Station = "Station";
|
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Grid, Station };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ContainerLocationTypes
|
public sealed record ContainerMaterialRequest(
|
||||||
{
|
Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo,
|
||||||
public const string Storage = "Storage";
|
string Status, DateTimeOffset? LoadedAt, DateTimeOffset? UnloadedAt, string Source, string Reason,
|
||||||
public const string Car = "Car";
|
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Storage, Car };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ContainerLocationStatuses
|
|
||||||
{
|
|
||||||
public const string Active = "Active";
|
|
||||||
public const string Locked = "Locked";
|
|
||||||
public const string Exception = "Exception";
|
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Active, Locked, Exception };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ContainerMaterialStatuses
|
|
||||||
{
|
|
||||||
public const string Bound = "Bound";
|
|
||||||
// 兼容旧数据
|
|
||||||
public const string Loaded = "Loaded";
|
|
||||||
public const string Unloaded = "Unloaded";
|
|
||||||
public const string Adjusted = "Adjusted";
|
|
||||||
public const string Frozen = "Frozen";
|
|
||||||
public static readonly HashSet<string> ActiveBind = new(StringComparer.OrdinalIgnoreCase) { Bound, Loaded, Adjusted, Frozen };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ContainerStatuses
|
|
||||||
{
|
|
||||||
public const string EmptyMaterial = "EmptyMaterial";
|
|
||||||
public const string FullMaterial = "FullMaterial";
|
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { EmptyMaterial, FullMaterial };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class StorageStatuses
|
|
||||||
{
|
|
||||||
public const string Empty = "Empty";
|
|
||||||
public const string EmptyContainer = "EmptyContainer";
|
|
||||||
public const string FullContainer = "FullContainer";
|
|
||||||
public const string Disabled = "Disabled";
|
|
||||||
// 兼容旧值(读时映射,写时用新值)
|
|
||||||
public const string Available = "Available";
|
|
||||||
public const string Idle = "Idle";
|
|
||||||
public const string Occupied = "Occupied";
|
|
||||||
|
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{ Empty, EmptyContainer, FullContainer, Disabled };
|
|
||||||
|
|
||||||
public static string Normalize(string? status) => status switch
|
|
||||||
{
|
|
||||||
Available or Idle => Empty,
|
|
||||||
Occupied => FullContainer,
|
|
||||||
_ when string.IsNullOrWhiteSpace(status) => Empty,
|
|
||||||
_ => status
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class StorageTypes
|
|
||||||
{
|
|
||||||
public const string Storage = "Storage";
|
|
||||||
public const string LineSide = "LineSide";
|
|
||||||
public const string OfflinePoint = "OfflinePoint";
|
|
||||||
public const string Buffer = "Buffer";
|
|
||||||
public const string FinishedGoods = "FinishedGoods";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MaterialLifecycles
|
|
||||||
{
|
|
||||||
public const string Active = "Active";
|
|
||||||
public const string Archived = "Archived";
|
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Active, Archived };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class StockEventTypes
|
|
||||||
{
|
|
||||||
public const string Bind = "Bind";
|
|
||||||
public const string Unbind = "Unbind";
|
|
||||||
public const string ContainerMove = "ContainerMove";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WmsTransportTriggerTypes
|
|
||||||
{
|
|
||||||
public const string MaterialCall = "MaterialCall";
|
|
||||||
public const string FinishedGoodsOffline = "FinishedGoodsOffline";
|
|
||||||
public const string AutoTransfer = "AutoTransfer";
|
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{ MaterialCall, FinishedGoodsOffline, AutoTransfer };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WmsTransportTaskStatuses
|
|
||||||
{
|
|
||||||
public const string Pending = "Pending";
|
|
||||||
public const string Reserved = "Reserved";
|
|
||||||
public const string Dispatched = "Dispatched";
|
|
||||||
public const string InTransit = "InTransit";
|
|
||||||
public const string Completed = "Completed";
|
|
||||||
public const string Failed = "Failed";
|
|
||||||
public const string Cancelled = "Cancelled";
|
|
||||||
public static readonly HashSet<string> Active = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{ Pending, Reserved, Dispatched, InTransit };
|
|
||||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{ Pending, Reserved, Dispatched, InTransit, Completed, Failed, Cancelled };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WmsReservationStatuses
|
|
||||||
{
|
|
||||||
public const string Active = "Active";
|
|
||||||
public const string Released = "Released";
|
|
||||||
public const string Expired = "Expired";
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class WmsTransportRule : EntityBase
|
|
||||||
{
|
|
||||||
[MaxLength(64)] public string Code { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string Name { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string TriggerType { get; set; } = WmsTransportTriggerTypes.MaterialCall;
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
public int Priority { get; set; }
|
|
||||||
public string SourceSelectorJson { get; set; } = "{}";
|
|
||||||
public string TargetSelectorJson { get; set; } = "{}";
|
|
||||||
public string TaskOptionsJson { get; set; } = "{}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class WmsTransportTask : EntityBase
|
|
||||||
{
|
|
||||||
[MaxLength(64)] public string BusinessType { get; set; } = "";
|
|
||||||
public Guid? RuleId { get; set; }
|
|
||||||
public Guid SourceStorageId { get; set; }
|
|
||||||
public Guid TargetStorageId { get; set; }
|
|
||||||
public Guid ContainerId { get; set; }
|
|
||||||
public Guid? MaterialId { get; set; }
|
|
||||||
/// <summary>已废弃:单物料实体模型不再使用数量。</summary>
|
|
||||||
public decimal? Quantity { get; set; }
|
|
||||||
[MaxLength(32)] public string Status { get; set; } = WmsTransportTaskStatuses.Pending;
|
|
||||||
[MaxLength(64)] public string DispatchMissionId { get; set; } = "";
|
|
||||||
[MaxLength(64)] public string DeliveryId { get; set; } = "";
|
|
||||||
[MaxLength(32)] public string DispatchStatus { get; set; } = "";
|
|
||||||
[MaxLength(500)] public string Reason { get; set; } = "";
|
|
||||||
public string SnapshotJson { get; set; } = "{}";
|
|
||||||
[MaxLength(1000)] public string ErrorMessage { get; set; } = "";
|
|
||||||
public int TaskPriority { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class WmsTransportReservation : EntityBase
|
|
||||||
{
|
|
||||||
public Guid TaskId { get; set; }
|
|
||||||
public Guid ContainerId { get; set; }
|
|
||||||
public Guid SourceStorageId { get; set; }
|
|
||||||
public Guid TargetStorageId { get; set; }
|
|
||||||
[MaxLength(32)] public string Status { get; set; } = WmsReservationStatuses.Active;
|
|
||||||
public DateTimeOffset? ExpiresAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class WmsTransportTaskHistory
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
|
||||||
public Guid TaskId { get; set; }
|
|
||||||
[MaxLength(32)] public string FromStatus { get; set; } = "";
|
|
||||||
[MaxLength(32)] public string ToStatus { get; set; } = "";
|
|
||||||
[MaxLength(128)] public string Operator { get; set; } = "";
|
|
||||||
public DateTimeOffset OperatedAt { get; set; }
|
|
||||||
[MaxLength(500)] public string Reason { get; set; } = "";
|
|
||||||
[MaxLength(1000)] public string ErrorMessage { get; set; } = "";
|
|
||||||
public string SnapshotJson { get; set; } = "{}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WmsDefaults
|
|
||||||
{
|
|
||||||
public const string DefaultWarehouseCode = "DEFAULT";
|
|
||||||
public const string DefaultWarehouseName = "默认仓库";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ public sealed class WmsReferenceValidator
|
|||||||
|
|
||||||
public async Task<(string Code, string Name)> ResolveLocationSnapshotAsync(string locationType, string locationId)
|
public async Task<(string Code, string Name)> ResolveLocationSnapshotAsync(string locationType, string locationId)
|
||||||
{
|
{
|
||||||
if (!ContainerLocationTypes.All.Contains(locationType))
|
if (!ContainerLocationTypes.IsDefined(locationType))
|
||||||
throw new InvalidOperationException("位置类型无效");
|
throw new InvalidOperationException("位置类型无效");
|
||||||
if (string.IsNullOrWhiteSpace(locationId))
|
if (string.IsNullOrWhiteSpace(locationId))
|
||||||
throw new InvalidOperationException("位置 ID 不能为空");
|
throw new InvalidOperationException("位置 ID 不能为空");
|
||||||
|
|
||||||
if (string.Equals(locationType, ContainerLocationTypes.Storage, StringComparison.OrdinalIgnoreCase))
|
if (ContainerLocationTypes.EqualsString(ContainerLocationTypes.Storage, locationType))
|
||||||
{
|
{
|
||||||
if (!Guid.TryParse(locationId, out var id)) throw new InvalidOperationException("库位 ID 格式无效");
|
if (!Guid.TryParse(locationId, out var id)) throw new InvalidOperationException("库位 ID 格式无效");
|
||||||
var s = await _db.Storages.FirstOrDefaultAsync(x => x.Id == id);
|
var s = await _db.Storages.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|||||||
+133
-180
@@ -1,6 +1,9 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using MiGu.DB.Abstractions.Entities;
|
||||||
|
using MiGu.DB.Abstractions.Persistence;
|
||||||
using MiGu.Server.Persistence;
|
using MiGu.Server.Persistence;
|
||||||
|
|
||||||
namespace MiGu.Server.Wms;
|
namespace MiGu.Server.Wms;
|
||||||
@@ -8,12 +11,20 @@ namespace MiGu.Server.Wms;
|
|||||||
public sealed class WmsService
|
public sealed class WmsService
|
||||||
{
|
{
|
||||||
private readonly PlatformDbContext _db;
|
private readonly PlatformDbContext _db;
|
||||||
|
private readonly IUnitOfWork _uow;
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
private readonly WmsReferenceValidator _refs;
|
private readonly WmsReferenceValidator _refs;
|
||||||
private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
|
private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
public WmsService(PlatformDbContext db, WmsReferenceValidator refs)
|
public WmsService(
|
||||||
|
PlatformDbContext db,
|
||||||
|
IUnitOfWork uow,
|
||||||
|
IServiceProvider services,
|
||||||
|
WmsReferenceValidator refs)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_uow = uow;
|
||||||
|
_services = services;
|
||||||
_refs = refs;
|
_refs = refs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +42,10 @@ public sealed class WmsService
|
|||||||
{
|
{
|
||||||
var query = _db.Storages.AsNoTracking().AsQueryable();
|
var query = _db.Storages.AsNoTracking().AsQueryable();
|
||||||
if (areaId.HasValue) query = query.Where(x => x.AreaId == areaId.Value);
|
if (areaId.HasValue) query = query.Where(x => x.AreaId == areaId.Value);
|
||||||
if (!string.IsNullOrWhiteSpace(locationKind)) query = query.Where(x => x.LocationKind == locationKind);
|
if (!string.IsNullOrWhiteSpace(locationKind) &&
|
||||||
|
Enum.TryParse<LocationKind>(locationKind.Trim(), true, out var kind) &&
|
||||||
|
LocationKinds.All.Contains(kind))
|
||||||
|
query = query.Where(x => x.LocationKind == kind);
|
||||||
return FilterByKeyword(query.OrderBy(x => x.Code), q).ToListAsync();
|
return FilterByKeyword(query.OrderBy(x => x.Code), q).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +58,7 @@ public sealed class WmsService
|
|||||||
public Task<List<Material>> Materials(string? q = null, string? lifecycle = null, bool? onlyUnbound = null, bool? onlyBound = null)
|
public Task<List<Material>> Materials(string? q = null, string? lifecycle = null, bool? onlyUnbound = null, bool? onlyBound = null)
|
||||||
{
|
{
|
||||||
var query = _db.Materials.AsNoTracking().AsQueryable();
|
var query = _db.Materials.AsNoTracking().AsQueryable();
|
||||||
var life = string.IsNullOrWhiteSpace(lifecycle) ? MaterialLifecycles.Active : lifecycle.Trim();
|
var life = MaterialLifecycles.ParseOr(lifecycle);
|
||||||
query = query.Where(x => x.LifecycleStatus == life);
|
query = query.Where(x => x.LifecycleStatus == life);
|
||||||
if (onlyUnbound == true || onlyBound == true)
|
if (onlyUnbound == true || onlyBound == true)
|
||||||
{
|
{
|
||||||
@@ -58,7 +72,11 @@ public sealed class WmsService
|
|||||||
public Task<List<ContainerLocation>> ContainerLocations(string? locationType = null, string? q = null)
|
public Task<List<ContainerLocation>> ContainerLocations(string? locationType = null, string? q = null)
|
||||||
{
|
{
|
||||||
var query = _db.ContainerLocations.AsNoTracking().OrderBy(x => x.ContainerId).AsQueryable();
|
var query = _db.ContainerLocations.AsNoTracking().OrderBy(x => x.ContainerId).AsQueryable();
|
||||||
if (!string.IsNullOrWhiteSpace(locationType)) query = query.Where(x => x.LocationType == locationType);
|
if (!string.IsNullOrWhiteSpace(locationType) && ContainerLocationTypes.IsDefined(locationType))
|
||||||
|
{
|
||||||
|
var lt = ContainerLocationTypes.ParseOr(locationType);
|
||||||
|
query = query.Where(x => x.LocationType == lt);
|
||||||
|
}
|
||||||
return FilterByKeyword(query, q).ToListAsync();
|
return FilterByKeyword(query, q).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,61 +87,71 @@ public sealed class WmsService
|
|||||||
return FilterByKeyword(query.OrderBy(x => x.ContainerId), q).ToListAsync();
|
return FilterByKeyword(query.OrderBy(x => x.ContainerId), q).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 库存物料列表:筛选/排序下推到 SQL。
|
||||||
|
/// 库位:优先 ContainerLocation.StorageId;未回填时用 LocationId 与库位 Id 的存储字符串(Guid "D")匹配。
|
||||||
|
/// </summary>
|
||||||
public async Task<List<InventoryMaterialRow>> InventoryMaterials(Guid? areaId = null, Guid? storageId = null, string? q = null)
|
public async Task<List<InventoryMaterialRow>> InventoryMaterials(Guid? areaId = null, Guid? storageId = null, string? q = null)
|
||||||
{
|
{
|
||||||
var binds = await _db.ContainerMaterials.AsNoTracking().ToListAsync();
|
var query =
|
||||||
var materials = await _db.Materials.AsNoTracking().ToDictionaryAsync(x => x.Id);
|
from b in _db.ContainerMaterials.AsNoTracking()
|
||||||
var locations = await _db.ContainerLocations.AsNoTracking().ToDictionaryAsync(x => x.ContainerId);
|
join mat in _db.Materials.AsNoTracking() on b.MaterialId equals mat.Id
|
||||||
var storages = await _db.Storages.AsNoTracking().ToDictionaryAsync(x => x.Id);
|
from ctn in _db.Containers.AsNoTracking().Where(c => c.Id == b.ContainerId).DefaultIfEmpty()
|
||||||
var areas = await _db.WarehouseAreas.AsNoTracking().ToDictionaryAsync(x => x.Id);
|
from loc in _db.ContainerLocations.AsNoTracking().Where(l => l.ContainerId == b.ContainerId).DefaultIfEmpty()
|
||||||
var containers = await _db.Containers.AsNoTracking().ToDictionaryAsync(x => x.Id);
|
from st in _db.Storages.AsNoTracking().Where(s =>
|
||||||
|
loc != null &&
|
||||||
|
loc.LocationType == ContainerLocationType.Storage &&
|
||||||
|
(loc.StorageId == s.Id ||
|
||||||
|
(loc.StorageId == null && loc.LocationId == EF.Property<string>(s, nameof(Storage.Id))))).DefaultIfEmpty()
|
||||||
|
from area in _db.WarehouseAreas.AsNoTracking().Where(a => st != null && a.Id == st.AreaId).DefaultIfEmpty()
|
||||||
|
select new { b, mat, ctn, loc, st, area };
|
||||||
|
|
||||||
var rows = new List<InventoryMaterialRow>();
|
if (storageId.HasValue)
|
||||||
foreach (var b in binds)
|
query = query.Where(x => x.st != null && x.st.Id == storageId.Value);
|
||||||
|
if (areaId.HasValue)
|
||||||
|
query = query.Where(x => x.area != null && x.area.Id == areaId.Value);
|
||||||
|
if (!string.IsNullOrWhiteSpace(q))
|
||||||
{
|
{
|
||||||
if (!materials.TryGetValue(b.MaterialId, out var mat)) continue;
|
var s = q.Trim();
|
||||||
locations.TryGetValue(b.ContainerId, out var loc);
|
query = query.Where(x =>
|
||||||
Storage? storage = null;
|
x.mat.Code.Contains(s) || x.mat.Name.Contains(s) ||
|
||||||
WarehouseArea? area = null;
|
(x.ctn != null && (x.ctn.Code.Contains(s) || x.ctn.Name.Contains(s))) ||
|
||||||
if (loc != null && loc.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(loc.LocationId, out var sid))
|
(x.st != null && x.st.Code.Contains(s)));
|
||||||
storages.TryGetValue(sid, out storage);
|
|
||||||
if (storage != null) areas.TryGetValue(storage.AreaId, out area);
|
|
||||||
containers.TryGetValue(b.ContainerId, out var ctn);
|
|
||||||
|
|
||||||
if (storageId.HasValue && storage?.Id != storageId.Value) continue;
|
|
||||||
if (areaId.HasValue && area?.Id != areaId.Value) continue;
|
|
||||||
if (!string.IsNullOrWhiteSpace(q))
|
|
||||||
{
|
|
||||||
var s = q.Trim();
|
|
||||||
if (!(mat.Code.Contains(s) || mat.Name.Contains(s) || (ctn?.Code.Contains(s) ?? false) || (storage?.Code.Contains(s) ?? false)))
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.Add(new InventoryMaterialRow(
|
|
||||||
mat.Id, mat.Code, mat.Name, mat.Barcode, mat.TypeCode,
|
|
||||||
b.ContainerId, ctn?.Code ?? "", ctn?.Name ?? "",
|
|
||||||
storage?.Id, storage?.Code ?? "", storage?.Name ?? "",
|
|
||||||
area?.Id, area?.Code ?? "", area?.Name ?? "",
|
|
||||||
loc?.LocationType ?? "", b.BoundAt));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows.OrderBy(x => x.MaterialCode).ToList();
|
return await query
|
||||||
|
.OrderBy(x => x.mat.Code)
|
||||||
|
.Select(x => new InventoryMaterialRow(
|
||||||
|
x.mat.Id, x.mat.Code, x.mat.Name, x.mat.Barcode, x.mat.TypeCode,
|
||||||
|
x.b.ContainerId, x.ctn != null ? x.ctn.Code : "", x.ctn != null ? x.ctn.Name : "",
|
||||||
|
x.st != null ? x.st.Id : null, x.st != null ? x.st.Code : "", x.st != null ? x.st.Name : "",
|
||||||
|
x.area != null ? x.area.Id : null, x.area != null ? x.area.Code : "", x.area != null ? x.area.Name : "",
|
||||||
|
x.loc != null ? x.loc.LocationType.ToString() : "", x.b.BoundAt))
|
||||||
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 库存事件:条件与 OrderBy/Take(500) 均下推;非法 eventType 直接返回空,避免全表拉取后再过滤。
|
||||||
|
/// </summary>
|
||||||
public async Task<List<StockEvent>> StockEvents(string? eventType = null, Guid? materialId = null, Guid? containerId = null)
|
public async Task<List<StockEvent>> StockEvents(string? eventType = null, Guid? materialId = null, Guid? containerId = null)
|
||||||
{
|
{
|
||||||
var query = _db.StockEvents.AsNoTracking().AsQueryable();
|
var query = _db.StockEvents.AsNoTracking().AsQueryable();
|
||||||
if (!string.IsNullOrWhiteSpace(eventType)) query = query.Where(x => x.EventType == eventType);
|
if (!string.IsNullOrWhiteSpace(eventType))
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<StockEventType>(eventType.Trim(), true, out var et))
|
||||||
|
query = query.Where(x => x.EventType == et);
|
||||||
|
else
|
||||||
|
return [];
|
||||||
|
}
|
||||||
if (materialId.HasValue) query = query.Where(x => x.MaterialId == materialId.Value);
|
if (materialId.HasValue) query = query.Where(x => x.MaterialId == materialId.Value);
|
||||||
if (containerId.HasValue) query = query.Where(x => x.ContainerId == containerId.Value);
|
if (containerId.HasValue) query = query.Where(x => x.ContainerId == containerId.Value);
|
||||||
var rows = await query.ToListAsync();
|
return await query.OrderByDescending(x => x.OperatedAt).Take(500).ToListAsync();
|
||||||
return rows.OrderByDescending(x => x.OperatedAt).Take(500).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Warehouse> SaveWarehouse(MasterDataRequest req, string actor)
|
public async Task<Warehouse> SaveWarehouse(MasterDataRequest req, string actor)
|
||||||
{
|
{
|
||||||
Warehouse entity;
|
Warehouse entity;
|
||||||
if (req.Id.HasValue) entity = await FindEditable(_db.Warehouses, req.Id.Value, req.Version);
|
if (req.Id.HasValue) entity = await FindEditable<Warehouse>(req.Id.Value, req.Version);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entity = new Warehouse();
|
entity = new Warehouse();
|
||||||
@@ -137,7 +165,7 @@ public sealed class WmsService
|
|||||||
entity.Enabled = req.Enabled;
|
entity.Enabled = req.Enabled;
|
||||||
entity.SortOrder = req.SortOrder;
|
entity.SortOrder = req.SortOrder;
|
||||||
ApplyCommon(entity, req, actor);
|
ApplyCommon(entity, req, actor);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +178,7 @@ public sealed class WmsService
|
|||||||
await _refs.EnsureWarehouseAsync(warehouseId);
|
await _refs.EnsureWarehouseAsync(warehouseId);
|
||||||
|
|
||||||
WarehouseArea entity;
|
WarehouseArea entity;
|
||||||
if (req.Id.HasValue) entity = await FindEditable(_db.WarehouseAreas, req.Id.Value, req.Version);
|
if (req.Id.HasValue) entity = await FindEditable<WarehouseArea>(req.Id.Value, req.Version);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entity = new WarehouseArea();
|
entity = new WarehouseArea();
|
||||||
@@ -162,24 +190,24 @@ public sealed class WmsService
|
|||||||
entity.Code = req.Code.Trim();
|
entity.Code = req.Code.Trim();
|
||||||
entity.Name = req.Name.Trim();
|
entity.Name = req.Name.Trim();
|
||||||
entity.Type = req.Type.TrimOr("Storage");
|
entity.Type = req.Type.TrimOr("Storage");
|
||||||
entity.LayoutMode = AreaLayoutModes.All.Contains(req.LayoutMode) ? req.LayoutMode : AreaLayoutModes.Flat;
|
entity.LayoutMode = AreaLayoutModes.ParseOr(req.LayoutMode);
|
||||||
entity.State = req.State.TrimOr("Default");
|
entity.State = req.State.TrimOr("Default");
|
||||||
entity.Enabled = req.Enabled;
|
entity.Enabled = req.Enabled;
|
||||||
entity.SortOrder = req.SortOrder;
|
entity.SortOrder = req.SortOrder;
|
||||||
ApplyCommon(entity, req, actor);
|
ApplyCommon(entity, req, actor);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Storage> SaveStorage(StorageRequest req, string actor)
|
public async Task<Storage> SaveStorage(StorageRequest req, string actor)
|
||||||
{
|
{
|
||||||
await _refs.EnsureAreaAsync(req.AreaId);
|
await _refs.EnsureAreaAsync(req.AreaId);
|
||||||
var kind = LocationKinds.All.Contains(req.LocationKind) ? req.LocationKind : LocationKinds.Station;
|
var kind = LocationKinds.ParseOr(req.LocationKind);
|
||||||
if (kind == LocationKinds.Grid)
|
if (kind == LocationKinds.Grid)
|
||||||
await EnsureGridCoordUnique(req.AreaId, req.ColumnNo, req.LevelNo, req.DepthNo, req.Id);
|
await EnsureGridCoordUnique(req.AreaId, req.ColumnNo, req.LevelNo, req.DepthNo, req.Id);
|
||||||
|
|
||||||
Storage entity;
|
Storage entity;
|
||||||
if (req.Id.HasValue) entity = await FindEditable(_db.Storages, req.Id.Value, req.Version);
|
if (req.Id.HasValue) entity = await FindEditable<Storage>(req.Id.Value, req.Version);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entity = new Storage();
|
entity = new Storage();
|
||||||
@@ -199,7 +227,7 @@ public sealed class WmsService
|
|||||||
entity.SiteCode = req.SiteCode.TrimOr(entity.SiteId);
|
entity.SiteCode = req.SiteCode.TrimOr(entity.SiteId);
|
||||||
entity.Barcode = req.Barcode.TrimOr("");
|
entity.Barcode = req.Barcode.TrimOr("");
|
||||||
entity.Capacity = 1;
|
entity.Capacity = 1;
|
||||||
var status = StorageStatuses.Normalize(req.Status.TrimOr(StorageStatuses.Empty));
|
var status = StorageStatuses.Normalize(req.Status);
|
||||||
if (status == StorageStatuses.Disabled || req.Enabled == false)
|
if (status == StorageStatuses.Disabled || req.Enabled == false)
|
||||||
entity.Status = req.Enabled ? status : StorageStatuses.Disabled;
|
entity.Status = req.Enabled ? status : StorageStatuses.Disabled;
|
||||||
else if (!string.IsNullOrWhiteSpace(req.Status) && StorageStatuses.All.Contains(status))
|
else if (!string.IsNullOrWhiteSpace(req.Status) && StorageStatuses.All.Contains(status))
|
||||||
@@ -211,7 +239,7 @@ public sealed class WmsService
|
|||||||
entity.AllowOutbound = req.AllowOutbound;
|
entity.AllowOutbound = req.AllowOutbound;
|
||||||
entity.Enabled = req.Enabled;
|
entity.Enabled = req.Enabled;
|
||||||
ApplyCommon(entity, req, actor);
|
ApplyCommon(entity, req, actor);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
if (entity.Status != StorageStatuses.Disabled)
|
if (entity.Status != StorageStatuses.Disabled)
|
||||||
await SyncOccupancyStatus(storageId: entity.Id);
|
await SyncOccupancyStatus(storageId: entity.Id);
|
||||||
return entity;
|
return entity;
|
||||||
@@ -262,28 +290,28 @@ public sealed class WmsService
|
|||||||
_db.Storages.Add(entity);
|
_db.Storages.Add(entity);
|
||||||
created++;
|
created++;
|
||||||
}
|
}
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Storage> SetStorageLock(Guid id, bool isLock, long? version, string actor)
|
public async Task<Storage> SetStorageLock(Guid id, bool isLock, long? version, string actor)
|
||||||
{
|
{
|
||||||
var entity = await FindEditable(_db.Storages, id, version);
|
var entity = await FindEditable<Storage>(id, version);
|
||||||
entity.IsLock = isLock;
|
entity.IsLock = isLock;
|
||||||
entity.UpdatedBy = actor;
|
entity.UpdatedBy = actor;
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Storage> SetStorageEnabled(Guid id, bool enabled, long? version, string actor)
|
public async Task<Storage> SetStorageEnabled(Guid id, bool enabled, long? version, string actor)
|
||||||
{
|
{
|
||||||
var entity = await FindEditable(_db.Storages, id, version);
|
var entity = await FindEditable<Storage>(id, version);
|
||||||
entity.Enabled = enabled;
|
entity.Enabled = enabled;
|
||||||
entity.Status = enabled
|
entity.Status = enabled
|
||||||
? (entity.Status == StorageStatuses.Disabled ? StorageStatuses.Empty : entity.Status)
|
? (entity.Status == StorageStatuses.Disabled ? StorageStatuses.Empty : entity.Status)
|
||||||
: StorageStatuses.Disabled;
|
: StorageStatuses.Disabled;
|
||||||
entity.UpdatedBy = actor;
|
entity.UpdatedBy = actor;
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
if (enabled) await SyncOccupancyStatus(storageId: id);
|
if (enabled) await SyncOccupancyStatus(storageId: id);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@@ -291,7 +319,7 @@ public sealed class WmsService
|
|||||||
public async Task<Container> SaveContainer(ContainerRequest req, string actor)
|
public async Task<Container> SaveContainer(ContainerRequest req, string actor)
|
||||||
{
|
{
|
||||||
Container entity;
|
Container entity;
|
||||||
if (req.Id.HasValue) entity = await FindEditable(_db.Containers, req.Id.Value, req.Version);
|
if (req.Id.HasValue) entity = await FindEditable<Container>(req.Id.Value, req.Version);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entity = new Container();
|
entity = new Container();
|
||||||
@@ -303,15 +331,14 @@ public sealed class WmsService
|
|||||||
entity.Code = req.Code.Trim();
|
entity.Code = req.Code.Trim();
|
||||||
entity.Name = req.Name.Trim();
|
entity.Name = req.Name.Trim();
|
||||||
entity.ContainerType = req.Type.TrimOr("Box");
|
entity.ContainerType = req.Type.TrimOr("Box");
|
||||||
var status = req.Status.TrimOr(ContainerStatuses.EmptyMaterial);
|
entity.Status = ContainerStatuses.ParseOr(req.Status);
|
||||||
entity.Status = ContainerStatuses.All.Contains(status) ? status : ContainerStatuses.EmptyMaterial;
|
|
||||||
entity.Barcode = req.Barcode.TrimOr("");
|
entity.Barcode = req.Barcode.TrimOr("");
|
||||||
entity.Length = req.Length;
|
entity.Length = req.Length;
|
||||||
entity.Width = req.Width;
|
entity.Width = req.Width;
|
||||||
entity.Height = req.Height;
|
entity.Height = req.Height;
|
||||||
entity.Enabled = req.Enabled;
|
entity.Enabled = req.Enabled;
|
||||||
ApplyCommon(entity, req, actor);
|
ApplyCommon(entity, req, actor);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
await SyncOccupancyStatus(containerId: entity.Id);
|
await SyncOccupancyStatus(containerId: entity.Id);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@@ -319,7 +346,7 @@ public sealed class WmsService
|
|||||||
public async Task<MaterialType> SaveMaterialType(MaterialTypeRequest req, string actor)
|
public async Task<MaterialType> SaveMaterialType(MaterialTypeRequest req, string actor)
|
||||||
{
|
{
|
||||||
MaterialType entity;
|
MaterialType entity;
|
||||||
if (req.Id.HasValue) entity = await FindEditable(_db.MaterialTypes, req.Id.Value, req.Version);
|
if (req.Id.HasValue) entity = await FindEditable<MaterialType>(req.Id.Value, req.Version);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entity = new MaterialType();
|
entity = new MaterialType();
|
||||||
@@ -335,7 +362,7 @@ public sealed class WmsService
|
|||||||
entity.BarcodePrefix = req.BarcodePrefix.TrimOr("");
|
entity.BarcodePrefix = req.BarcodePrefix.TrimOr("");
|
||||||
entity.Enabled = req.Enabled;
|
entity.Enabled = req.Enabled;
|
||||||
ApplyCommon(entity, req, actor);
|
ApplyCommon(entity, req, actor);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +371,7 @@ public sealed class WmsService
|
|||||||
if (!string.IsNullOrWhiteSpace(req.TypeCode))
|
if (!string.IsNullOrWhiteSpace(req.TypeCode))
|
||||||
await _refs.EnsureMaterialTypeCodeAsync(req.TypeCode);
|
await _refs.EnsureMaterialTypeCodeAsync(req.TypeCode);
|
||||||
Material entity;
|
Material entity;
|
||||||
if (req.Id.HasValue) entity = await FindEditable(_db.Materials, req.Id.Value, req.Version);
|
if (req.Id.HasValue) entity = await FindEditable<Material>(req.Id.Value, req.Version);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entity = new Material();
|
entity = new Material();
|
||||||
@@ -361,23 +388,22 @@ public sealed class WmsService
|
|||||||
entity.Spec = req.Spec.TrimOr("");
|
entity.Spec = req.Spec.TrimOr("");
|
||||||
entity.Unit = req.Unit.TrimOr("pcs");
|
entity.Unit = req.Unit.TrimOr("pcs");
|
||||||
entity.Category = req.Category.TrimOr("");
|
entity.Category = req.Category.TrimOr("");
|
||||||
var life = req.LifecycleStatus.TrimOr(MaterialLifecycles.Active);
|
entity.LifecycleStatus = MaterialLifecycles.ParseOr(req.LifecycleStatus);
|
||||||
entity.LifecycleStatus = MaterialLifecycles.All.Contains(life) ? life : MaterialLifecycles.Active;
|
|
||||||
entity.Enabled = req.Enabled;
|
entity.Enabled = req.Enabled;
|
||||||
ApplyCommon(entity, req, actor);
|
ApplyCommon(entity, req, actor);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Material> ArchiveMaterial(Guid id, long? version, string actor)
|
public async Task<Material> ArchiveMaterial(Guid id, long? version, string actor)
|
||||||
{
|
{
|
||||||
var entity = await FindEditable(_db.Materials, id, version);
|
var entity = await FindEditable<Material>(id, version);
|
||||||
if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == id))
|
if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == id))
|
||||||
throw new InvalidOperationException("物料仍在绑定中,不能归档");
|
throw new InvalidOperationException("物料仍在绑定中,不能归档");
|
||||||
entity.LifecycleStatus = MaterialLifecycles.Archived;
|
entity.LifecycleStatus = MaterialLifecycles.Archived;
|
||||||
entity.UnboundAt ??= DateTimeOffset.UtcNow;
|
entity.UnboundAt ??= DateTimeOffset.UtcNow;
|
||||||
entity.UpdatedBy = actor;
|
entity.UpdatedBy = actor;
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,13 +422,12 @@ public sealed class WmsService
|
|||||||
entity.DeletedBy = actor;
|
entity.DeletedBy = actor;
|
||||||
entity.UpdatedBy = actor;
|
entity.UpdatedBy = actor;
|
||||||
}
|
}
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteEntity<T>(Guid id, long? version, string actor) where T : EntityBase
|
public async Task DeleteEntity<T>(Guid id, long? version, string actor) where T : EntityBase
|
||||||
{
|
{
|
||||||
var set = _db.Set<T>();
|
var entity = await FindEditable<T>(id, version);
|
||||||
var entity = await FindEditable(set, id, version);
|
|
||||||
if (typeof(T) == typeof(WarehouseArea))
|
if (typeof(T) == typeof(WarehouseArea))
|
||||||
{
|
{
|
||||||
if (await _db.Storages.AnyAsync(x => x.AreaId == id))
|
if (await _db.Storages.AnyAsync(x => x.AreaId == id))
|
||||||
@@ -423,19 +448,22 @@ public sealed class WmsService
|
|||||||
entity.DeletedAt = DateTimeOffset.UtcNow;
|
entity.DeletedAt = DateTimeOffset.UtcNow;
|
||||||
entity.DeletedBy = actor;
|
entity.DeletedBy = actor;
|
||||||
entity.UpdatedBy = actor;
|
entity.UpdatedBy = actor;
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ContainerLocation> BindOrTransferLocation(ContainerLocationRequest req, string actor)
|
public async Task<ContainerLocation> BindOrTransferLocation(ContainerLocationRequest req, string actor)
|
||||||
{
|
{
|
||||||
await _refs.EnsureContainerAsync(req.ContainerId);
|
await _refs.EnsureContainerAsync(req.ContainerId);
|
||||||
var (code, name) = await _refs.ResolveLocationSnapshotAsync(req.LocationType, req.LocationId);
|
var (code, name) = await _refs.ResolveLocationSnapshotAsync(req.LocationType, req.LocationId);
|
||||||
if (!ContainerLocationStatuses.All.Contains(req.Status))
|
if (!ContainerLocationStatuses.IsDefined(req.Status))
|
||||||
throw new InvalidOperationException("容器位置状态无效");
|
throw new InvalidOperationException("容器位置状态无效");
|
||||||
|
|
||||||
|
var locationType = ContainerLocationTypes.ParseOr(req.LocationType);
|
||||||
|
var locationStatus = ContainerLocationStatuses.ParseOr(req.Status);
|
||||||
|
|
||||||
Guid? fromStorageId = null;
|
Guid? fromStorageId = null;
|
||||||
Guid? toStorageId = null;
|
Guid? toStorageId = null;
|
||||||
if (string.Equals(req.LocationType, ContainerLocationTypes.Storage, StringComparison.OrdinalIgnoreCase) &&
|
if (locationType == ContainerLocationTypes.Storage &&
|
||||||
Guid.TryParse(req.LocationId, out var targetStorageId))
|
Guid.TryParse(req.LocationId, out var targetStorageId))
|
||||||
{
|
{
|
||||||
toStorageId = targetStorageId;
|
toStorageId = targetStorageId;
|
||||||
@@ -455,7 +483,9 @@ public sealed class WmsService
|
|||||||
|
|
||||||
var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == req.ContainerId);
|
var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == req.ContainerId);
|
||||||
var before = current == null ? null : Snapshot(current);
|
var before = current == null ? null : Snapshot(current);
|
||||||
if (before != null && before.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(before.LocationId, out var fs))
|
if (before != null &&
|
||||||
|
ContainerLocationTypes.EqualsString(ContainerLocationTypes.Storage, before.LocationType) &&
|
||||||
|
Guid.TryParse(before.LocationId, out var fs))
|
||||||
fromStorageId = fs;
|
fromStorageId = fs;
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
@@ -471,11 +501,12 @@ public sealed class WmsService
|
|||||||
EnsureUnlocked(current);
|
EnsureUnlocked(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
current.LocationType = req.LocationType;
|
current.LocationType = locationType;
|
||||||
current.LocationId = req.LocationId.Trim();
|
current.LocationId = req.LocationId.Trim();
|
||||||
|
current.StorageId = toStorageId;
|
||||||
current.LocationCode = code;
|
current.LocationCode = code;
|
||||||
current.LocationName = name;
|
current.LocationName = name;
|
||||||
current.Status = req.Status;
|
current.Status = locationStatus;
|
||||||
current.EnteredAt = req.EnteredAt ?? now;
|
current.EnteredAt = req.EnteredAt ?? now;
|
||||||
ApplyCommon(current, req, actor);
|
ApplyCommon(current, req, actor);
|
||||||
|
|
||||||
@@ -486,7 +517,7 @@ public sealed class WmsService
|
|||||||
EventType = before == null ? "Bind" : "Transfer",
|
EventType = before == null ? "Bind" : "Transfer",
|
||||||
FromLocationType = before?.LocationType ?? "",
|
FromLocationType = before?.LocationType ?? "",
|
||||||
FromLocationId = before?.LocationId ?? "",
|
FromLocationId = before?.LocationId ?? "",
|
||||||
ToLocationType = current.LocationType,
|
ToLocationType = current.LocationType.ToString(),
|
||||||
ToLocationId = current.LocationId,
|
ToLocationId = current.LocationId,
|
||||||
BeforeJson = before == null ? "{}" : JsonSerializer.Serialize(before, _json),
|
BeforeJson = before == null ? "{}" : JsonSerializer.Serialize(before, _json),
|
||||||
AfterJson = JsonSerializer.Serialize(Snapshot(current), _json),
|
AfterJson = JsonSerializer.Serialize(Snapshot(current), _json),
|
||||||
@@ -499,7 +530,7 @@ public sealed class WmsService
|
|||||||
});
|
});
|
||||||
|
|
||||||
await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now);
|
await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId);
|
await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId);
|
||||||
await SyncOccupancyStatus(storageId: toStorageId);
|
await SyncOccupancyStatus(storageId: toStorageId);
|
||||||
return current;
|
return current;
|
||||||
@@ -520,7 +551,7 @@ public sealed class WmsService
|
|||||||
RelationId = current.Id,
|
RelationId = current.Id,
|
||||||
ContainerId = current.ContainerId,
|
ContainerId = current.ContainerId,
|
||||||
EventType = "Unbind",
|
EventType = "Unbind",
|
||||||
FromLocationType = current.LocationType,
|
FromLocationType = current.LocationType.ToString(),
|
||||||
FromLocationId = current.LocationId,
|
FromLocationId = current.LocationId,
|
||||||
BeforeJson = JsonSerializer.Serialize(before, _json),
|
BeforeJson = JsonSerializer.Serialize(before, _json),
|
||||||
AfterJson = "{}",
|
AfterJson = "{}",
|
||||||
@@ -531,7 +562,7 @@ public sealed class WmsService
|
|||||||
});
|
});
|
||||||
await AddContainerMoveEventAsync(containerId, fromStorageId, null, actor, reason, now);
|
await AddContainerMoveEventAsync(containerId, fromStorageId, null, actor, reason, now);
|
||||||
_db.ContainerLocations.Remove(current);
|
_db.ContainerLocations.Remove(current);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
await SyncOccupancyStatus(containerId: containerId, storageId: fromStorageId);
|
await SyncOccupancyStatus(containerId: containerId, storageId: fromStorageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +609,7 @@ public sealed class WmsService
|
|||||||
|
|
||||||
material.UnboundAt = null;
|
material.UnboundAt = null;
|
||||||
await AddBindUnbindEventAsync(StockEventTypes.Bind, material, req.ContainerId, actor, req.Reason.TrimOr(""), now);
|
await AddBindUnbindEventAsync(StockEventTypes.Bind, material, req.ContainerId, actor, req.Reason.TrimOr(""), now);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
await SyncOccupancyStatus(containerId: req.ContainerId);
|
await SyncOccupancyStatus(containerId: req.ContainerId);
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
@@ -620,7 +651,7 @@ public sealed class WmsService
|
|||||||
}
|
}
|
||||||
|
|
||||||
_db.ContainerMaterials.Remove(current);
|
_db.ContainerMaterials.Remove(current);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
await SyncOccupancyStatus(containerId: containerId);
|
await SyncOccupancyStatus(containerId: containerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,7 +697,7 @@ public sealed class WmsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Warehouse> EnsureDefaultWarehouseAsync(string actor = "system")
|
public async Task<Warehouse> EnsureDefaultWarehouseAsync(string actor = "system")
|
||||||
@@ -683,7 +714,7 @@ public sealed class WmsService
|
|||||||
};
|
};
|
||||||
StampCreate(wh, actor);
|
StampCreate(wh, actor);
|
||||||
_db.Warehouses.Add(wh);
|
_db.Warehouses.Add(wh);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return wh;
|
return wh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,44 +723,26 @@ public sealed class WmsService
|
|||||||
var wh = await EnsureDefaultWarehouseAsync(actor);
|
var wh = await EnsureDefaultWarehouseAsync(actor);
|
||||||
var areas = await _db.WarehouseAreas.Where(x => x.WarehouseId == Guid.Empty).ToListAsync();
|
var areas = await _db.WarehouseAreas.Where(x => x.WarehouseId == Guid.Empty).ToListAsync();
|
||||||
foreach (var a in areas)
|
foreach (var a in areas)
|
||||||
{
|
|
||||||
a.WarehouseId = wh.Id;
|
a.WarehouseId = wh.Id;
|
||||||
if (string.IsNullOrWhiteSpace(a.LayoutMode)) a.LayoutMode = AreaLayoutModes.Flat;
|
|
||||||
}
|
|
||||||
|
|
||||||
var storages = await _db.Storages.ToListAsync();
|
var storages = await _db.Storages.ToListAsync();
|
||||||
foreach (var s in storages)
|
foreach (var s in storages)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(s.LocationKind))
|
|
||||||
s.LocationKind = LocationKinds.Station;
|
|
||||||
if (s.LevelNo <= 0) s.LevelNo = 1;
|
if (s.LevelNo <= 0) s.LevelNo = 1;
|
||||||
if (s.DepthNo <= 0) s.DepthNo = 1;
|
if (s.DepthNo <= 0) s.DepthNo = 1;
|
||||||
if (string.IsNullOrWhiteSpace(s.SiteCode)) s.SiteCode = s.SiteId;
|
if (string.IsNullOrWhiteSpace(s.SiteCode)) s.SiteCode = s.SiteId;
|
||||||
s.Status = StorageStatuses.Normalize(s.Status) switch
|
if (!StorageStatuses.All.Contains(s.Status))
|
||||||
{
|
s.Status = StorageStatuses.Empty;
|
||||||
StorageStatuses.Available or StorageStatuses.Idle => StorageStatuses.Empty,
|
|
||||||
StorageStatuses.Occupied => StorageStatuses.FullContainer,
|
|
||||||
var x => x
|
|
||||||
};
|
|
||||||
if (!s.Enabled) s.Status = StorageStatuses.Disabled;
|
if (!s.Enabled) s.Status = StorageStatuses.Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
var containers = await _db.Containers.ToListAsync();
|
var containers = await _db.Containers.ToListAsync();
|
||||||
foreach (var c in containers)
|
foreach (var c in containers)
|
||||||
{
|
{
|
||||||
c.Status = c.Status switch
|
if (!ContainerStatuses.All.Contains(c.Status))
|
||||||
{
|
c.Status = ContainerStatuses.EmptyMaterial;
|
||||||
"Idle" or "Empty" => ContainerStatuses.EmptyMaterial,
|
|
||||||
"Loaded" => ContainerStatuses.FullMaterial,
|
|
||||||
_ when ContainerStatuses.All.Contains(c.Status) => c.Status,
|
|
||||||
_ => ContainerStatuses.EmptyMaterial
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var materials = await _db.Materials.Where(x => string.IsNullOrWhiteSpace(x.LifecycleStatus)).ToListAsync();
|
|
||||||
foreach (var m in materials)
|
|
||||||
m.LifecycleStatus = MaterialLifecycles.Active;
|
|
||||||
|
|
||||||
var binds = await _db.ContainerMaterials.ToListAsync();
|
var binds = await _db.ContainerMaterials.ToListAsync();
|
||||||
foreach (var b in binds)
|
foreach (var b in binds)
|
||||||
{
|
{
|
||||||
@@ -739,7 +752,7 @@ public sealed class WmsService
|
|||||||
b.Status = ContainerMaterialStatuses.Bound;
|
b.Status = ContainerMaterialStatuses.Bound;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
|
|
||||||
foreach (var s in storages.Where(x => x.Status != StorageStatuses.Disabled))
|
foreach (var s in storages.Where(x => x.Status != StorageStatuses.Disabled))
|
||||||
await SyncOccupancyStatus(storageId: s.Id);
|
await SyncOccupancyStatus(storageId: s.Id);
|
||||||
@@ -773,13 +786,14 @@ public sealed class WmsService
|
|||||||
if (exists) throw new InvalidOperationException("同库区网格坐标已存在");
|
if (exists) throw new InvalidOperationException("同库区网格坐标已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddBindUnbindEventAsync(string eventType, Material material, Guid containerId, string actor, string reason, DateTimeOffset now)
|
private async Task AddBindUnbindEventAsync(StockEventType eventType, Material material, Guid containerId, string actor, string reason, DateTimeOffset now)
|
||||||
{
|
{
|
||||||
var ctn = await _db.Containers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == containerId);
|
var ctn = await _db.Containers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == containerId);
|
||||||
var loc = await _db.ContainerLocations.AsNoTracking().FirstOrDefaultAsync(x => x.ContainerId == containerId);
|
var loc = await _db.ContainerLocations.AsNoTracking().FirstOrDefaultAsync(x => x.ContainerId == containerId);
|
||||||
Storage? storage = null;
|
Storage? storage = null;
|
||||||
WarehouseArea? area = null;
|
WarehouseArea? area = null;
|
||||||
if (loc is { LocationType: ContainerLocationTypes.Storage } && Guid.TryParse(loc.LocationId, out var sid))
|
if (loc is { LocationType: ContainerLocationType.Storage } &&
|
||||||
|
(loc.StorageId is { } sid || Guid.TryParse(loc.LocationId, out sid)))
|
||||||
{
|
{
|
||||||
storage = await _db.Storages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == sid);
|
storage = await _db.Storages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == sid);
|
||||||
if (storage != null)
|
if (storage != null)
|
||||||
@@ -843,23 +857,21 @@ public sealed class WmsService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<T> FindEditable<T>(DbSet<T> set, Guid id, long? version) where T : EntityBase
|
private Task<T> FindEditable<T>(Guid id, long? version)
|
||||||
{
|
where T : class, IEntity<Guid>, ISoftDeletable, IVersioned, ILockable
|
||||||
var entity = await set.FirstOrDefaultAsync(x => x.Id == id) ?? throw new InvalidOperationException("数据不存在");
|
=> _services.GetRequiredService<IEditableRepository<T>>().GetEditableAsync(id, version);
|
||||||
EnsureVersion(entity, version);
|
|
||||||
EnsureUnlocked(entity);
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureVersion(EntityBase entity, long? version)
|
private static void EnsureVersion(EntityBase entity, long? version)
|
||||||
{
|
{
|
||||||
if (version.HasValue && entity.Version != version.Value)
|
if (version.HasValue && entity.Version != version.Value)
|
||||||
throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试");
|
throw new MiGu.DB.Abstractions.Exceptions.ConcurrencyConflictException(
|
||||||
|
entity.GetType().Name, entity.Id, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureUnlocked(EntityBase entity)
|
private static void EnsureUnlocked(EntityBase entity)
|
||||||
{
|
{
|
||||||
if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改");
|
if (entity.IsLock)
|
||||||
|
throw new MiGu.DB.Abstractions.Exceptions.EntityLockedException(entity.GetType().Name, entity.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void StampCreate(EntityBase entity, string actor)
|
private static void StampCreate(EntityBase entity, string actor)
|
||||||
@@ -909,73 +921,14 @@ public sealed class WmsService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ContainerLocationSnapshot Snapshot(ContainerLocation x) => new(
|
private static ContainerLocationSnapshot Snapshot(ContainerLocation x) => new(
|
||||||
x.Id, x.ContainerId, x.LocationType, x.LocationId, x.LocationCode, x.LocationName, x.Status, x.EnteredAt, x.Version);
|
x.Id, x.ContainerId, x.LocationType.ToString(), x.LocationId, x.LocationCode, x.LocationName,
|
||||||
|
x.Status.ToString(), x.EnteredAt, x.Version);
|
||||||
|
|
||||||
private static ContainerMaterialSnapshot Snapshot(ContainerMaterial x) => new(
|
private static ContainerMaterialSnapshot Snapshot(ContainerMaterial x) => new(
|
||||||
x.Id, x.ContainerId, x.MaterialId, x.Quantity, x.BatchNo, x.SerialNo, x.Status, x.BoundAt, x.LoadedAt, x.UnloadedAt, x.Version);
|
x.Id, x.ContainerId, x.MaterialId, x.Quantity, x.BatchNo, x.SerialNo, x.Status.ToString(),
|
||||||
|
x.BoundAt, x.LoadedAt, x.UnloadedAt, x.Version);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record InventoryMaterialRow(
|
|
||||||
Guid MaterialId, string MaterialCode, string MaterialName, string MaterialBarcode, string TypeCode,
|
|
||||||
Guid ContainerId, string ContainerCode, string ContainerName,
|
|
||||||
Guid? StorageId, string StorageCode, string StorageName,
|
|
||||||
Guid? AreaId, string AreaCode, string AreaName,
|
|
||||||
string LocationType, DateTimeOffset BoundAt);
|
|
||||||
|
|
||||||
public sealed record ContainerLocationSnapshot(
|
|
||||||
Guid Id, Guid ContainerId, string LocationType, string LocationId, string LocationCode, string LocationName,
|
|
||||||
string Status, DateTimeOffset EnteredAt, long Version);
|
|
||||||
|
|
||||||
public sealed record ContainerMaterialSnapshot(
|
|
||||||
Guid Id, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo, string Status,
|
|
||||||
DateTimeOffset BoundAt, DateTimeOffset LoadedAt, DateTimeOffset? UnloadedAt, long Version);
|
|
||||||
|
|
||||||
public abstract record CommonRequest(Guid? Id, long? Version, bool IsLock, string Remark, string Extend);
|
|
||||||
|
|
||||||
public sealed record MasterDataRequest(
|
|
||||||
Guid? Id, long? Version, string Code, string Name, string Type, string Status, bool Enabled, int SortOrder,
|
|
||||||
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public sealed record AreaRequest(
|
|
||||||
Guid? Id, long? Version, Guid? WarehouseId, string Code, string Name, string Type, string LayoutMode, string State,
|
|
||||||
bool Enabled, int SortOrder, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public sealed record StorageRequest(
|
|
||||||
Guid? Id, long? Version, Guid AreaId, string Code, string Name, string StorageType, string LocationKind,
|
|
||||||
int ColumnNo, int LevelNo, int DepthNo, string SiteId, string SiteCode, string Barcode, int Capacity,
|
|
||||||
string Status, string Usage, int Priority, string ZoneCode, bool AllowInbound, bool AllowOutbound, bool Enabled,
|
|
||||||
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public sealed record GenerateBinsRequest(int ColumnFrom, int ColumnTo, int LevelFrom, int LevelTo, int DepthFrom, int DepthTo, string? CodePattern);
|
|
||||||
|
|
||||||
public sealed record ContainerRequest(
|
|
||||||
Guid? Id, long? Version, Guid? AreaId, string Code, string Name, string Type, string Status, string Barcode,
|
|
||||||
double Length, double Width, double Height, bool Enabled, bool IsLock, string Remark, string Extend)
|
|
||||||
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public sealed record MaterialTypeRequest(
|
|
||||||
Guid? Id, long? Version, string Code, string Name, string Spec, string Unit, string Category, string BarcodePrefix,
|
|
||||||
bool Enabled, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public sealed record MaterialRequest(
|
|
||||||
Guid? Id, long? Version, string Code, string Name, string TypeCode, string Barcode, string Spec, string Unit,
|
|
||||||
string Category, string LifecycleStatus, bool Enabled, bool IsLock, string Remark, string Extend)
|
|
||||||
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public sealed record ContainerLocationRequest(
|
|
||||||
Guid? Id, long? Version, Guid ContainerId, string LocationType, string LocationId, string Status,
|
|
||||||
DateTimeOffset? EnteredAt, string Source, string Reason, bool IsLock, string Remark, string Extend)
|
|
||||||
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public sealed record BindMaterialRequest(
|
|
||||||
Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, string Source, string Reason,
|
|
||||||
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public sealed record ContainerMaterialRequest(
|
|
||||||
Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo,
|
|
||||||
string Status, DateTimeOffset? LoadedAt, DateTimeOffset? UnloadedAt, string Source, string Reason,
|
|
||||||
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
||||||
|
|
||||||
public static class WmsStringExtensions
|
public static class WmsStringExtensions
|
||||||
{
|
{
|
||||||
public static string TrimOr(this string? value, string fallback) =>
|
public static string TrimOr(this string? value, string fallback) =>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Server 侧状态别名:把 MiGu.DB 的复数辅助类导入为短名,兼容业务代码中的 StorageStatuses / LocationKinds 等写法。
|
||||||
|
// 枚举类型本身通过下方 global using Domains 命名空间直接可用(如 StorageStatus、LocationKind)。
|
||||||
|
global using MiGu.DB.Domains.Wms;
|
||||||
|
global using MiGu.DB.Domains.Transport;
|
||||||
|
global using AreaLayoutModes = MiGu.DB.Domains.Wms.AreaLayoutModes;
|
||||||
|
global using StorageTypeCodes = MiGu.DB.Domains.Wms.StorageTypeCodes;
|
||||||
|
global using LocationKinds = MiGu.DB.Domains.Wms.LocationKinds;
|
||||||
|
global using StorageStatuses = MiGu.DB.Domains.Wms.StorageStatuses;
|
||||||
|
global using ContainerStatuses = MiGu.DB.Domains.Wms.ContainerStatuses;
|
||||||
|
global using MaterialLifecycles = MiGu.DB.Domains.Wms.MaterialLifecycles;
|
||||||
|
global using ContainerLocationTypes = MiGu.DB.Domains.Wms.ContainerLocationTypes;
|
||||||
|
global using ContainerLocationStatuses = MiGu.DB.Domains.Wms.ContainerLocationStatuses;
|
||||||
|
global using ContainerMaterialStatuses = MiGu.DB.Domains.Wms.ContainerMaterialStatuses;
|
||||||
|
global using StockEventTypes = MiGu.DB.Domains.Wms.StockEventTypes;
|
||||||
|
global using WmsDefaults = MiGu.DB.Domains.Wms.WmsDefaults;
|
||||||
|
global using WmsTransportTaskStatuses = MiGu.DB.Domains.Transport.WmsTransportTaskStatuses;
|
||||||
|
global using WmsTransportTriggerTypes = MiGu.DB.Domains.Transport.WmsTransportTriggerTypes;
|
||||||
|
global using WmsReservationStatuses = MiGu.DB.Domains.Transport.WmsReservationStatuses;
|
||||||
|
global using WmsDispatchStatuses = MiGu.DB.Domains.Transport.WmsDispatchStatuses;
|
||||||
@@ -13,11 +13,12 @@ public sealed class WmsTransportPlanner
|
|||||||
|
|
||||||
public async Task<TransportCandidatePreview> PreviewAsync(WmsTransportRequest request)
|
public async Task<TransportCandidatePreview> PreviewAsync(WmsTransportRequest request)
|
||||||
{
|
{
|
||||||
if (!WmsTransportTriggerTypes.All.Contains(request.TriggerType))
|
if (!WmsTransportTriggerTypes.IsDefined(request.TriggerType))
|
||||||
throw new InvalidOperationException("触发类型无效");
|
throw new InvalidOperationException("触发类型无效");
|
||||||
|
|
||||||
|
var trigger = WmsTransportTriggerTypes.ParseOr(request.TriggerType);
|
||||||
var ctx = await BuildContextAsync();
|
var ctx = await BuildContextAsync();
|
||||||
var rules = await LoadRulesAsync(request);
|
var rules = await LoadRulesAsync(request, trigger);
|
||||||
var ruleResults = new List<RulePreviewResult>();
|
var ruleResults = new List<RulePreviewResult>();
|
||||||
var candidates = new List<TransportCandidatePair>();
|
var candidates = new List<TransportCandidatePair>();
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ public sealed class WmsTransportPlanner
|
|||||||
var sourceSelector = TransportSelectorParser.ParseSelector(rule.SourceSelectorJson);
|
var sourceSelector = TransportSelectorParser.ParseSelector(rule.SourceSelectorJson);
|
||||||
var targetSelector = TransportSelectorParser.ParseSelector(rule.TargetSelectorJson);
|
var targetSelector = TransportSelectorParser.ParseSelector(rule.TargetSelectorJson);
|
||||||
var options = TransportSelectorParser.ParseTaskOptions(rule.TaskOptionsJson);
|
var options = TransportSelectorParser.ParseTaskOptions(rule.TaskOptionsJson);
|
||||||
var ruleCandidates = BuildCandidates(ctx, request, rule, sourceSelector, targetSelector, options);
|
var ruleCandidates = BuildCandidates(ctx, request, trigger, rule, sourceSelector, targetSelector, options);
|
||||||
var rejectReasons = ruleCandidates.Count == 0
|
var rejectReasons = ruleCandidates.Count == 0
|
||||||
? new List<string> { "未找到满足条件的起点/终点组合" }
|
? new List<string> { "未找到满足条件的起点/终点组合" }
|
||||||
: new List<string>();
|
: new List<string>();
|
||||||
@@ -52,10 +53,10 @@ public sealed class WmsTransportPlanner
|
|||||||
return preview.Candidates.FirstOrDefault();
|
return preview.Candidates.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<WmsTransportRule>> LoadRulesAsync(WmsTransportRequest request)
|
private async Task<List<WmsTransportRule>> LoadRulesAsync(WmsTransportRequest request, WmsTransportTriggerType trigger)
|
||||||
{
|
{
|
||||||
var query = _db.WmsTransportRules.AsNoTracking()
|
var query = _db.WmsTransportRules.AsNoTracking()
|
||||||
.Where(x => x.Enabled && x.TriggerType == request.TriggerType);
|
.Where(x => x.Enabled && x.TriggerType == trigger);
|
||||||
if (request.RuleId.HasValue)
|
if (request.RuleId.HasValue)
|
||||||
query = query.Where(x => x.Id == request.RuleId.Value);
|
query = query.Where(x => x.Id == request.RuleId.Value);
|
||||||
return await query.OrderByDescending(x => x.Priority).ThenBy(x => x.Code).ToListAsync();
|
return await query.OrderByDescending(x => x.Priority).ThenBy(x => x.Code).ToListAsync();
|
||||||
@@ -94,17 +95,18 @@ public sealed class WmsTransportPlanner
|
|||||||
private List<TransportCandidatePair> BuildCandidates(
|
private List<TransportCandidatePair> BuildCandidates(
|
||||||
PlannerContext ctx,
|
PlannerContext ctx,
|
||||||
WmsTransportRequest request,
|
WmsTransportRequest request,
|
||||||
|
WmsTransportTriggerType trigger,
|
||||||
WmsTransportRule rule,
|
WmsTransportRule rule,
|
||||||
TransportSelector sourceSelector,
|
TransportSelector sourceSelector,
|
||||||
TransportSelector targetSelector,
|
TransportSelector targetSelector,
|
||||||
TransportTaskOptions options)
|
TransportTaskOptions options)
|
||||||
{
|
{
|
||||||
var results = new List<TransportCandidatePair>();
|
var results = new List<TransportCandidatePair>();
|
||||||
var sources = BuildSourceCandidates(ctx, request, sourceSelector, options);
|
var sources = BuildSourceCandidates(ctx, request, trigger, sourceSelector, options);
|
||||||
|
|
||||||
foreach (var src in sources)
|
foreach (var src in sources)
|
||||||
{
|
{
|
||||||
var targets = BuildTargetCandidates(ctx, request, targetSelector, options, src.StorageId);
|
var targets = BuildTargetCandidates(ctx, request, trigger, targetSelector, options, src.StorageId);
|
||||||
foreach (var tgt in targets)
|
foreach (var tgt in targets)
|
||||||
{
|
{
|
||||||
var score = rule.Priority * 1000 + src.Score + tgt.Score;
|
var score = rule.Priority * 1000 + src.Score + tgt.Score;
|
||||||
@@ -124,6 +126,7 @@ public sealed class WmsTransportPlanner
|
|||||||
private List<SourceCandidate> BuildSourceCandidates(
|
private List<SourceCandidate> BuildSourceCandidates(
|
||||||
PlannerContext ctx,
|
PlannerContext ctx,
|
||||||
WmsTransportRequest request,
|
WmsTransportRequest request,
|
||||||
|
WmsTransportTriggerType trigger,
|
||||||
TransportSelector selector,
|
TransportSelector selector,
|
||||||
TransportTaskOptions options)
|
TransportTaskOptions options)
|
||||||
{
|
{
|
||||||
@@ -145,7 +148,7 @@ public sealed class WmsTransportPlanner
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (request.RequestSiteId is { Length: > 0 } siteId &&
|
if (request.RequestSiteId is { Length: > 0 } siteId &&
|
||||||
string.Equals(request.TriggerType, WmsTransportTriggerTypes.FinishedGoodsOffline, StringComparison.OrdinalIgnoreCase) &&
|
trigger == WmsTransportTriggerTypes.FinishedGoodsOffline &&
|
||||||
!string.Equals(storage.SiteId, siteId, StringComparison.OrdinalIgnoreCase))
|
!string.Equals(storage.SiteId, siteId, StringComparison.OrdinalIgnoreCase))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -162,6 +165,7 @@ public sealed class WmsTransportPlanner
|
|||||||
private List<TargetCandidate> BuildTargetCandidates(
|
private List<TargetCandidate> BuildTargetCandidates(
|
||||||
PlannerContext ctx,
|
PlannerContext ctx,
|
||||||
WmsTransportRequest request,
|
WmsTransportRequest request,
|
||||||
|
WmsTransportTriggerType trigger,
|
||||||
TransportSelector selector,
|
TransportSelector selector,
|
||||||
TransportTaskOptions options,
|
TransportTaskOptions options,
|
||||||
Guid sourceStorageId)
|
Guid sourceStorageId)
|
||||||
@@ -176,7 +180,7 @@ public sealed class WmsTransportPlanner
|
|||||||
if (ctx.ReservedTargetStorages.Contains(storage.Id)) continue;
|
if (ctx.ReservedTargetStorages.Contains(storage.Id)) continue;
|
||||||
|
|
||||||
if (request.RequestSiteId is { Length: > 0 } siteId &&
|
if (request.RequestSiteId is { Length: > 0 } siteId &&
|
||||||
string.Equals(request.TriggerType, WmsTransportTriggerTypes.MaterialCall, StringComparison.OrdinalIgnoreCase) &&
|
trigger == WmsTransportTriggerTypes.MaterialCall &&
|
||||||
selector.Storage?.SiteIds is { Count: 0 } &&
|
selector.Storage?.SiteIds is { Count: 0 } &&
|
||||||
!string.Equals(storage.SiteId, siteId, StringComparison.OrdinalIgnoreCase))
|
!string.Equals(storage.SiteId, siteId, StringComparison.OrdinalIgnoreCase))
|
||||||
continue;
|
continue;
|
||||||
@@ -200,7 +204,8 @@ public sealed class WmsTransportPlanner
|
|||||||
{
|
{
|
||||||
if (request.MaterialId.HasValue && row.MaterialId != request.MaterialId.Value) continue;
|
if (request.MaterialId.HasValue && row.MaterialId != request.MaterialId.Value) continue;
|
||||||
if (filter?.MaterialIds is { Count: > 0 } ids && !ids.Contains(row.MaterialId.ToString("D"))) continue;
|
if (filter?.MaterialIds is { Count: > 0 } ids && !ids.Contains(row.MaterialId.ToString("D"))) continue;
|
||||||
if (filter?.StatusIn is { Count: > 0 } st && !st.Contains(row.Status, StringComparer.OrdinalIgnoreCase)) continue;
|
if (filter?.StatusIn is { Count: > 0 } st &&
|
||||||
|
!st.Contains(row.Status.ToString(), StringComparer.OrdinalIgnoreCase)) continue;
|
||||||
if (filter?.MinQuantity is { } min && row.Quantity < min) continue;
|
if (filter?.MinQuantity is { } min && row.Quantity < min) continue;
|
||||||
if (request.Quantity is { } reqQty && row.Quantity < reqQty) continue;
|
if (request.Quantity is { } reqQty && row.Quantity < reqQty) continue;
|
||||||
if (request.BatchNo is { Length: > 0 } batch && !string.Equals(row.BatchNo, batch, StringComparison.OrdinalIgnoreCase)) continue;
|
if (request.BatchNo is { Length: > 0 } batch && !string.Equals(row.BatchNo, batch, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
@@ -220,7 +225,8 @@ public sealed class WmsTransportPlanner
|
|||||||
if (filter.AreaIds is { Count: > 0 } && !filter.AreaIds.Contains(storage.AreaId.ToString("D"))) return false;
|
if (filter.AreaIds is { Count: > 0 } && !filter.AreaIds.Contains(storage.AreaId.ToString("D"))) return false;
|
||||||
if (filter.StorageTypes is { Count: > 0 } && !filter.StorageTypes.Contains(storage.StorageType, StringComparer.OrdinalIgnoreCase)) return false;
|
if (filter.StorageTypes is { Count: > 0 } && !filter.StorageTypes.Contains(storage.StorageType, StringComparer.OrdinalIgnoreCase)) return false;
|
||||||
if (filter.ZoneCodes is { Count: > 0 } && !filter.ZoneCodes.Contains(storage.ZoneCode, StringComparer.OrdinalIgnoreCase)) return false;
|
if (filter.ZoneCodes is { Count: > 0 } && !filter.ZoneCodes.Contains(storage.ZoneCode, StringComparer.OrdinalIgnoreCase)) return false;
|
||||||
if (filter.StatusIn is { Count: > 0 } && !filter.StatusIn.Contains(storage.Status, StringComparer.OrdinalIgnoreCase)) return false;
|
if (filter.StatusIn is { Count: > 0 } &&
|
||||||
|
!filter.StatusIn.Contains(storage.Status.ToString(), StringComparer.OrdinalIgnoreCase)) return false;
|
||||||
if (filter.SiteIds is { Count: > 0 } && !filter.SiteIds.Contains(storage.SiteId, StringComparer.OrdinalIgnoreCase)) return false;
|
if (filter.SiteIds is { Count: > 0 } && !filter.SiteIds.Contains(storage.SiteId, StringComparer.OrdinalIgnoreCase)) return false;
|
||||||
if (filter.AllowInbound == true && !storage.AllowInbound) return false;
|
if (filter.AllowInbound == true && !storage.AllowInbound) return false;
|
||||||
if (filter.AllowOutbound == true && !storage.AllowOutbound) return false;
|
if (filter.AllowOutbound == true && !storage.AllowOutbound) return false;
|
||||||
@@ -236,7 +242,8 @@ public sealed class WmsTransportPlanner
|
|||||||
{
|
{
|
||||||
if (filter == null) return true;
|
if (filter == null) return true;
|
||||||
if (filter.ContainerTypes is { Count: > 0 } && !filter.ContainerTypes.Contains(container.ContainerType, StringComparer.OrdinalIgnoreCase)) return false;
|
if (filter.ContainerTypes is { Count: > 0 } && !filter.ContainerTypes.Contains(container.ContainerType, StringComparer.OrdinalIgnoreCase)) return false;
|
||||||
if (filter.StatusIn is { Count: > 0 } && !filter.StatusIn.Contains(container.Status, StringComparer.OrdinalIgnoreCase)) return false;
|
if (filter.StatusIn is { Count: > 0 } &&
|
||||||
|
!filter.StatusIn.Contains(container.Status.ToString(), StringComparer.OrdinalIgnoreCase)) return false;
|
||||||
if (filter.ExcludeReserved &&
|
if (filter.ExcludeReserved &&
|
||||||
(ctx.ReservedContainers.Contains(container.Id) || ctx.TaskReservedContainers.Contains(container.Id)))
|
(ctx.ReservedContainers.Contains(container.Id) || ctx.TaskReservedContainers.Contains(container.Id)))
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MiGu.DB.Abstractions.Exceptions;
|
||||||
|
using MiGu.DB.Abstractions.Persistence;
|
||||||
using MiGu.Server.Persistence;
|
using MiGu.Server.Persistence;
|
||||||
|
|
||||||
namespace MiGu.Server.Wms;
|
namespace MiGu.Server.Wms;
|
||||||
@@ -6,14 +8,26 @@ namespace MiGu.Server.Wms;
|
|||||||
public sealed class WmsTransportRuleService
|
public sealed class WmsTransportRuleService
|
||||||
{
|
{
|
||||||
private readonly PlatformDbContext _db;
|
private readonly PlatformDbContext _db;
|
||||||
|
private readonly IEditableRepository<WmsTransportRule> _rules;
|
||||||
|
private readonly IUnitOfWork _uow;
|
||||||
|
|
||||||
public WmsTransportRuleService(PlatformDbContext db) => _db = db;
|
public WmsTransportRuleService(
|
||||||
|
PlatformDbContext db,
|
||||||
|
IEditableRepository<WmsTransportRule> rules,
|
||||||
|
IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_rules = rules;
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<List<WmsTransportRule>> ListAsync(string? triggerType = null, string? q = null)
|
public Task<List<WmsTransportRule>> ListAsync(string? triggerType = null, string? q = null)
|
||||||
{
|
{
|
||||||
var query = _db.WmsTransportRules.AsNoTracking().OrderByDescending(x => x.Priority).ThenBy(x => x.Code).AsQueryable();
|
var query = _db.WmsTransportRules.AsNoTracking().OrderByDescending(x => x.Priority).ThenBy(x => x.Code).AsQueryable();
|
||||||
if (!string.IsNullOrWhiteSpace(triggerType))
|
if (!string.IsNullOrWhiteSpace(triggerType) &&
|
||||||
query = query.Where(x => x.TriggerType == triggerType);
|
Enum.TryParse<WmsTransportTriggerType>(triggerType.Trim(), true, out var tt) &&
|
||||||
|
WmsTransportTriggerTypes.All.Contains(tt))
|
||||||
|
query = query.Where(x => x.TriggerType == tt);
|
||||||
if (!string.IsNullOrWhiteSpace(q))
|
if (!string.IsNullOrWhiteSpace(q))
|
||||||
{
|
{
|
||||||
var s = q.Trim();
|
var s = q.Trim();
|
||||||
@@ -24,12 +38,12 @@ public sealed class WmsTransportRuleService
|
|||||||
|
|
||||||
public async Task<WmsTransportRule> SaveAsync(TransportRuleRequest req, string actor)
|
public async Task<WmsTransportRule> SaveAsync(TransportRuleRequest req, string actor)
|
||||||
{
|
{
|
||||||
if (!WmsTransportTriggerTypes.All.Contains(req.TriggerType))
|
if (!WmsTransportTriggerTypes.IsDefined(req.TriggerType))
|
||||||
throw new InvalidOperationException("触发类型无效");
|
throw new InvalidOperationException("触发类型无效");
|
||||||
|
|
||||||
WmsTransportRule entity;
|
WmsTransportRule entity;
|
||||||
if (req.Id.HasValue)
|
if (req.Id.HasValue)
|
||||||
entity = await FindEditable(req.Id.Value, req.Version);
|
entity = await _rules.GetEditableAsync(req.Id.Value, req.Version);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
entity = new WmsTransportRule();
|
entity = new WmsTransportRule();
|
||||||
@@ -37,44 +51,24 @@ public sealed class WmsTransportRuleService
|
|||||||
_db.WmsTransportRules.Add(entity);
|
_db.WmsTransportRules.Add(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
await EnsureUniqueCode(req.Code, entity.Id);
|
await _rules.EnsureUniqueAsync(x => x.Code == req.Code.Trim() && x.Id != entity.Id, "规则编码已存在");
|
||||||
entity.Code = req.Code.Trim();
|
entity.Code = req.Code.Trim();
|
||||||
entity.Name = req.Name.Trim();
|
entity.Name = req.Name.Trim();
|
||||||
entity.TriggerType = req.TriggerType.Trim();
|
entity.TriggerType = WmsTransportTriggerTypes.ParseOr(req.TriggerType);
|
||||||
entity.Enabled = req.Enabled;
|
entity.Enabled = req.Enabled;
|
||||||
entity.Priority = req.Priority;
|
entity.Priority = req.Priority;
|
||||||
entity.SourceSelectorJson = TransportSelectorParser.NormalizeSelectorJson(req.SourceSelectorJson);
|
entity.SourceSelectorJson = TransportSelectorParser.NormalizeSelectorJson(req.SourceSelectorJson);
|
||||||
entity.TargetSelectorJson = TransportSelectorParser.NormalizeSelectorJson(req.TargetSelectorJson);
|
entity.TargetSelectorJson = TransportSelectorParser.NormalizeSelectorJson(req.TargetSelectorJson);
|
||||||
entity.TaskOptionsJson = TransportSelectorParser.NormalizeTaskOptionsJson(req.TaskOptionsJson);
|
entity.TaskOptionsJson = TransportSelectorParser.NormalizeTaskOptionsJson(req.TaskOptionsJson);
|
||||||
ApplyCommon(entity, req, actor);
|
ApplyCommon(entity, req, actor);
|
||||||
await _db.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(Guid id, long? version, string actor)
|
public async Task DeleteAsync(Guid id, long? version, string actor)
|
||||||
{
|
{
|
||||||
var entity = await FindEditable(id, version);
|
await _rules.SoftDeleteAsync(id, version);
|
||||||
entity.IsDeleted = true;
|
await _uow.SaveChangesAsync();
|
||||||
entity.DeletedAt = DateTimeOffset.UtcNow;
|
|
||||||
entity.DeletedBy = actor;
|
|
||||||
entity.UpdatedBy = actor;
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<WmsTransportRule> FindEditable(Guid id, long? version)
|
|
||||||
{
|
|
||||||
var entity = await _db.WmsTransportRules.FirstOrDefaultAsync(x => x.Id == id)
|
|
||||||
?? throw new InvalidOperationException("规则不存在");
|
|
||||||
if (version.HasValue && entity.Version != version.Value)
|
|
||||||
throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试");
|
|
||||||
if (entity.IsLock) throw new InvalidOperationException("规则已锁定,不能修改");
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureUniqueCode(string code, Guid id)
|
|
||||||
{
|
|
||||||
if (await _db.WmsTransportRules.AnyAsync(x => x.Code == code.Trim() && x.Id != id))
|
|
||||||
throw new InvalidOperationException("规则编码已存在");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void StampCreate(EntityBase entity, string actor)
|
private static void StampCreate(EntityBase entity, string actor)
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ public sealed class WmsTransportTaskService
|
|||||||
public async Task<List<WmsTransportTask>> ListAsync(string? status = null, string? triggerType = null)
|
public async Task<List<WmsTransportTask>> ListAsync(string? status = null, string? triggerType = null)
|
||||||
{
|
{
|
||||||
var query = _db.WmsTransportTasks.AsNoTracking().AsQueryable();
|
var query = _db.WmsTransportTasks.AsNoTracking().AsQueryable();
|
||||||
if (!string.IsNullOrWhiteSpace(status))
|
if (!string.IsNullOrWhiteSpace(status) &&
|
||||||
query = query.Where(x => x.Status == status);
|
Enum.TryParse<WmsTransportTaskStatus>(status.Trim(), true, out var st) &&
|
||||||
|
WmsTransportTaskStatuses.All.Contains(st))
|
||||||
|
query = query.Where(x => x.Status == st);
|
||||||
if (!string.IsNullOrWhiteSpace(triggerType))
|
if (!string.IsNullOrWhiteSpace(triggerType))
|
||||||
query = query.Where(x => x.BusinessType == triggerType);
|
query = query.Where(x => x.BusinessType == triggerType);
|
||||||
var rows = await query.ToListAsync();
|
return await query.OrderByDescending(x => x.CreatedAt).Take(500).ToListAsync();
|
||||||
return rows.OrderByDescending(x => x.CreatedAt).Take(500).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TransportCandidatePreview> PreviewCandidatesAsync(WmsTransportRequest request) =>
|
public Task<TransportCandidatePreview> PreviewCandidatesAsync(WmsTransportRequest request) =>
|
||||||
@@ -80,7 +81,7 @@ public sealed class WmsTransportTaskService
|
|||||||
if (status == WmsTransportTaskStatuses.Reserved)
|
if (status == WmsTransportTaskStatuses.Reserved)
|
||||||
await CreateReservationAsync(task, options, actor);
|
await CreateReservationAsync(task, options, actor);
|
||||||
|
|
||||||
await AppendHistoryAsync(task.Id, "", task.Status, actor, request.Reason, "", task.SnapshotJson);
|
await AppendHistoryAsync(task.Id, "", task.Status.ToString(), actor, request.Reason, "", task.SnapshotJson);
|
||||||
await tx.CommitAsync();
|
await tx.CommitAsync();
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
@@ -147,8 +148,8 @@ public sealed class WmsTransportTaskService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _wms.BindOrTransferLocation(new ContainerLocationRequest(
|
await _wms.BindOrTransferLocation(new ContainerLocationRequest(
|
||||||
null, null, task.ContainerId, ContainerLocationTypes.Storage,
|
null, null, task.ContainerId, ContainerLocationTypes.Storage.ToString(),
|
||||||
task.TargetStorageId.ToString("D"), ContainerLocationStatuses.Active,
|
task.TargetStorageId.ToString("D"), ContainerLocationStatuses.Active.ToString(),
|
||||||
DateTimeOffset.UtcNow, "TransportTask", reason, false, "", "{}"), actor);
|
DateTimeOffset.UtcNow, "TransportTask", reason, false, "", "{}"), actor);
|
||||||
|
|
||||||
await ReleaseReservationsAsync(task.Id);
|
await ReleaseReservationsAsync(task.Id);
|
||||||
@@ -245,13 +246,13 @@ public sealed class WmsTransportTaskService
|
|||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ChangeStatusAsync(WmsTransportTask task, string toStatus, string actor, string reason)
|
private async Task ChangeStatusAsync(WmsTransportTask task, WmsTransportTaskStatus toStatus, string actor, string reason)
|
||||||
{
|
{
|
||||||
var from = task.Status;
|
var from = task.Status;
|
||||||
task.Status = toStatus;
|
task.Status = toStatus;
|
||||||
task.UpdatedBy = actor;
|
task.UpdatedBy = actor;
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
await AppendHistoryAsync(task.Id, from, toStatus, actor, reason, task.ErrorMessage, task.SnapshotJson);
|
await AppendHistoryAsync(task.Id, from.ToString(), toStatus.ToString(), actor, reason, task.ErrorMessage, task.SnapshotJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AppendHistoryAsync(Guid taskId, string from, string to, string actor, string reason, string error, string snapshot)
|
private async Task AppendHistoryAsync(Guid taskId, string from, string to, string actor, string reason, string error, string snapshot)
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
"Yarp": "Debug"
|
"Yarp": "Debug"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Database": {
|
||||||
|
"SchemaMode": "EnsureCreated"
|
||||||
|
},
|
||||||
"SimpleLite": {
|
"SimpleLite": {
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"ExecutablePath": "D:\\Code\\Products\\MIGU2.0\\SimpleLite\\SimpleLite.exe",
|
"ExecutablePath": "D:\\Code\\Products\\MIGU2.0\\SimpleLite\\SimpleLite.exe",
|
||||||
|
|||||||
@@ -114,5 +114,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"Provider": "sqlite",
|
||||||
|
"SchemaMode": "Migrate",
|
||||||
|
"ApplyDataMigratorsOnStartup": true
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Platform": "Data Source=data/platform.db"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user