新增MES库区管理功能,包含免密接口、数据处理逻辑及相关控制器、服务和实体的实现。支持库区的增删改查操作,优化用户体验并增强系统的实时数据同步能力。

This commit is contained in:
geht
2026-05-12 14:06:07 +08:00
parent cffe32d896
commit b737dddb2a
74 changed files with 4937 additions and 174 deletions

View File

@@ -0,0 +1,13 @@
using Prism.Events;
namespace YY.Admin.Core.Events;
public class WarehouseAreaChangedPayload
{
public string Action { get; set; } = string.Empty;
public string? WarehouseAreaId { get; set; }
}
public class WarehouseAreaChangedEvent : PubSubEvent<WarehouseAreaChangedPayload>
{
}

View File

@@ -15,4 +15,12 @@ public interface IRawMaterialCardService
Task<bool> DeleteAsync(string id, CancellationToken ct = default);
Task<bool> DeleteBatchAsync(string ids, CancellationToken ct = default);
Task<bool> UpdatePriorityAsync(string id, string priorityPickup, CancellationToken ct = default);
/// <summary>
/// 按拆码明细 ID 列表批量删除原材料卡片。
/// </summary>
/// <param name="splitDetailIds">拆码明细行的 GUID 集合(自动 distinct空跳过</param>
/// <param name="dryRun">为 true 时仅返回匹配数量、不真正删除(用于「重新拆码」弹窗预提示)</param>
/// <returns>匹配/删除的卡片数量;失败返回 -1</returns>
Task<int> DeleteBySplitDetailIdsAsync(IEnumerable<string> splitDetailIds, bool dryRun = false, CancellationToken ct = default);
}

View File

@@ -23,4 +23,10 @@ public interface IRawMaterialEntryService
/// <summary>调用后端接口生成条码/批次号格式QH+物料编码+yyMMdd+序号)</summary>
Task<string?> GenerateBarcodeAsync(string materialCode, CancellationToken ct = default);
/// <summary>
/// 同步读取本地缓存的「全量入场记录」快照(深拷贝),不会触发远端拉取。
/// 主要用于「磅单已入场重量」等跨表实时聚合,且需要保持与后端相同口径的场景。
/// </summary>
IReadOnlyList<MesXslRawMaterialEntry> GetCachedSnapshot();
}

View File

@@ -0,0 +1,54 @@
using YY.Admin.Core.Entity;
namespace YY.Admin.Core.Services;
public record WarehouseAreaPageResult(List<MesXslWarehouseArea> Records, long Total, int PageNo, int PageSize);
/// <summary>
/// 库区管理服务CRUD + 状态切换 + 编码校验)。
/// 走 jeecg 免密接口 /xslmes/mesXslWarehouseArea/anon/*。
///
/// 注意:本接口契约不可随意改签名,被以下调用方引用:
/// - WarehouseAreaListViewModel列表/筛选/启停/删除)
/// - WarehouseAreaEditDialogViewModel新增/编辑/校验)
/// - WarehouseAreaPickerDialogViewModel拆码明细库位选择弹窗
/// </summary>
public interface IWarehouseAreaService
{
/// <summary>
/// 分页查询。参数顺序与 WarehouseAreaListViewModel 调用一致:
/// pageNo, pageSize, areaCode, areaName, warehouseId, status。
/// </summary>
/// <param name="status">"0" 启用 / "1" 停用 / "" 或 null 表示不限</param>
Task<WarehouseAreaPageResult> PageAsync(
int pageNo,
int pageSize,
string? areaCode = null,
string? areaName = null,
string? warehouseId = null,
string? status = null,
CancellationToken ct = default);
/// <summary>按 ID 查单条。</summary>
Task<MesXslWarehouseArea?> GetByIdAsync(string id, CancellationToken ct = default);
/// <summary>新增库区。后端会校验 areaCode 重复。</summary>
Task<bool> AddAsync(MesXslWarehouseArea area, CancellationToken ct = default);
/// <summary>编辑库区(按 ID。</summary>
Task<bool> EditAsync(MesXslWarehouseArea area, CancellationToken ct = default);
/// <summary>按 ID 删除。</summary>
Task<bool> DeleteAsync(string id, CancellationToken ct = default);
/// <summary>切换启停状态:传入目标状态 "0"=启用 / "1"=停用。</summary>
Task<bool> UpdateStatusAsync(string id, string newStatus, CancellationToken ct = default);
/// <summary>
/// 校验库区编码是否可用(不存在则可用)。
/// </summary>
/// <param name="areaCode">待校验的库区编码</param>
/// <param name="excludeId">编辑时排除自身 ID新增传 null 或空</param>
/// <returns>true=可用无重复false=已被占用</returns>
Task<bool> CheckAreaCodeAsync(string areaCode, string? excludeId, CancellationToken ct = default);
}

View File

@@ -0,0 +1,9 @@
using YY.Admin.Core.Entity;
namespace YY.Admin.Core.Services;
public interface IWarehouseService
{
/// <summary>获取全部仓库列表(在线时拉取远端并刷新缓存,离线时返回本地缓存)</summary>
Task<List<MesXslWarehouse>> GetAllAsync(CancellationToken ct = default);
}

View File

@@ -4,6 +4,9 @@ public class MesXslRawMaterialCard
{
public string? Id { get; set; }
public string? Barcode { get; set; }
// 关联的拆码明细行 IDGUID「生成原材料卡片」时由桌面端填入
// 「重新拆码」按入场记录的 PortionDetailIds 批量 IN 删除关联卡片。
public string? SplitDetailId { get; set; }
public string? BatchNo { get; set; }
public DateTime? EntryDate { get; set; }
public string? MaterialId { get; set; }

View File

@@ -23,6 +23,19 @@ public class MesXslRawMaterialEntry
public string? TotalPortions { get; set; }
public string? PortionWeight { get; set; }
public string? PortionPackages { get; set; }
// 拆码明细各行库位的拼接(以 / 分隔,末尾带 /,如 1F-A01/1F-A02/)。
// 与 WarehouseLocation基础资料整票级单值独立专供明细行回填。
public string? PortionWarehouseLocations { get; set; }
// 拆码明细每行的 GUID 拼接(以 / 分隔,末尾带 /),与其它 portion 字段行序对齐,
// 用于「重新拆码」按拆码明细 ID 反查并清除关联原材料卡片。
public string? PortionDetailIds { get; set; }
/// <summary>
/// 拆码明细行级「已生成卡片」标志拼接(以 / 分隔,末尾带 /1=已生成 0=未生成)。
/// 与 PortionDetailIds 行序对齐,作为「生成原材料卡片」过滤待生成行的唯一依据:
/// HasCard==true 的行不再参与生成(避免重复加卡 + 条码冲突HasCard==false 的行才参与续生成。
/// 历史记录留空时桌面端降级用 PrintFlag 推断(持久化 ID 非空 && PrintFlag=1 ⇒ 视为已生成)。
/// </summary>
public string? PortionCardFlags { get; set; }
/// <summary>检测结果0未检 1合格 2不合格</summary>
public string? TestResult { get; set; }

View File

@@ -0,0 +1,11 @@
namespace YY.Admin.Core.Entity;
public class MesXslWarehouse
{
public string? Id { get; set; }
public string? WarehouseCode { get; set; }
public string? WarehouseName { get; set; }
public string? WarehouseCategory { get; set; }
public string? Status { get; set; }
public int? TenantId { get; set; }
}

View File

@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
namespace YY.Admin.Core.Entity;
public class MesXslWarehouseArea
{
public string? Id { get; set; }
public string? AreaCode { get; set; }
public string? AreaName { get; set; }
public string? WarehouseId { get; set; }
public string? WarehouseName { get; set; }
public string? WarehouseCategory { get; set; }
/// <summary>仓库分类名称JeecgBoot @Dict 自动翻译JSON key = warehouseCategory_dictText</summary>
[JsonPropertyName("warehouseCategory_dictText")]
public string? WarehouseCategoryName { get; set; }
public int? MaxCapacity { get; set; }
public int? ActualCapacity { get; set; }
public string? Remark { get; set; }
/// <summary>状态0启用 1停用</summary>
public string? Status { get; set; }
public int? TenantId { get; set; }
public string? CreateBy { get; set; }
public DateTime? CreateTime { get; set; }
public string? UpdateBy { get; set; }
public DateTime? UpdateTime { get; set; }
public string StatusText => Status == "1" ? "停用" : "启用";
}

View File

@@ -34,6 +34,12 @@ public class MesXslWeightRecord
/// <summary>净重(KG)=毛重-皮重,自动计算</summary>
public double? NetWeight { get; set; }
/// <summary>
/// 已入场重量(KG) —— 后端实时计算,不入库。
/// 来源所有引用本榜单BillNo的原料入场记录拆码明细的 (份数×每份重量) 累计求和。
/// </summary>
public double? EnteredWeight { get; set; }
/// <summary>司机姓名</summary>
public string? DriverName { get; set; }

View File

@@ -44,6 +44,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
new SysMenu{ Id=1300150010801, Pid=1300150000101, Title="新增原料入场记录", Path="/xslmes/rawMaterialEntryOperation", Name="rawMaterialEntryOperation", Component="RawMaterialEntryOperationView", Icon="&#xe7de;", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=107 },
// 原材料卡片
new SysMenu{ Id=1300150010901, Pid=1300150000101, Title="原材料卡片", Path="/xslmes/mesXslRawMaterialCard", Name="mesXslRawMaterialCard", Component="RawMaterialCardListView", Icon="&#xe7ce;", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=108 },
// 库区管理
new SysMenu{ Id=1300150011001, Pid=1300150000101, Title="库区管理", Path="/xslmes/mesXslWarehouseArea", Name="mesXslWarehouseArea", Component="WarehouseAreaListView", Icon="&#xe7ce;", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=109 },
#endregion

View File

@@ -29,6 +29,8 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData<SysTenantMenu>
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010601},
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010701},
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010801},
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010901},
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150011001},
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012101},
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012111},
new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012121},

View File

@@ -0,0 +1,92 @@
using System.Globalization;
using YY.Admin.Core.Entity;
namespace YY.Admin.Core.Util;
/// <summary>
/// 「已入场重量」桌面端本地累计计算器。
/// 与后端 IMesXslRawMaterialEntryService.sumEnteredWeightByBillNos 保持同一口径:
/// ① 把 totalPortions / portionWeight 按 "x/y/z/" 拆分
/// ② 逐位 (份数 × 每份重量) 累加,得到该 entry 的小计
/// ③ 同一榜单BillNo下所有 entry 的小计再次累加
/// 任一位置解析失败或缺失:静默跳过,保证 UI 降级可用。
/// </summary>
public static class EnteredWeightCalculator
{
/// <summary>累计一条入场记录的小计。</summary>
public static double SumOneEntry(string? totalPortions, string? portionWeight)
{
var portions = SplitJoined(totalPortions);
var weights = SplitJoined(portionWeight);
if (portions.Length == 0 || weights.Length == 0)
{
return 0d;
}
var n = Math.Min(portions.Length, weights.Length);
double sum = 0d;
for (var i = 0; i < n; i++)
{
if (!TryParse(portions[i], out var p) || !TryParse(weights[i], out var w))
{
continue;
}
sum += p * w;
}
return sum;
}
/// <summary>按 BillNo 分组累计「已入场重量」。返回 Dictionary&lt;BillNo, 总和&gt;。</summary>
public static Dictionary<string, double> SumByBillNos(
IEnumerable<MesXslRawMaterialEntry> entries,
IEnumerable<string?> billNos)
{
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var b in billNos)
{
if (!string.IsNullOrWhiteSpace(b)) keys.Add(b!);
}
var result = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
if (keys.Count == 0)
{
return result;
}
foreach (var e in entries)
{
if (string.IsNullOrWhiteSpace(e.BillNo) || !keys.Contains(e.BillNo!))
{
continue;
}
var sub = SumOneEntry(e.TotalPortions, e.PortionWeight);
if (sub == 0d)
{
continue;
}
if (result.TryGetValue(e.BillNo!, out var acc))
{
result[e.BillNo!] = acc + sub;
}
else
{
result[e.BillNo!] = sub;
}
}
return result;
}
private static string[] SplitJoined(string? value)
{
// 兼容 "x/y/z" 与 "x/y/z/" 两种格式;过滤空段
if (string.IsNullOrWhiteSpace(value))
{
return Array.Empty<string>();
}
return value.Split('/')
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.ToArray();
}
private static bool TryParse(string s, out double v) =>
double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out v);
}