diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixingProductionPlanController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixingProductionPlanController.java index 084bbb42..64cb31f3 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixingProductionPlanController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixingProductionPlanController.java @@ -13,6 +13,7 @@ import org.jeecg.common.system.base.controller.JeecgController; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.modules.xslmes.entity.MesXslMixingProductionPlan; import org.jeecg.modules.xslmes.service.IMesXslMixingProductionPlanService; +import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; import org.jeecg.modules.xslmes.vo.MesXslMixingProductionPlanOrderOptionVO; import org.jeecg.modules.xslmes.vo.MesXslMixingProductionPlanSaveAllVO; import org.springframework.web.bind.annotation.GetMapping; @@ -29,10 +30,13 @@ public class MesXslMixingProductionPlanController extends JeecgController { private final IMesXslMixingProductionPlanService mixingProductionPlanService; + private final MesXslStompNotifyService stompNotify; public MesXslMixingProductionPlanController( - IMesXslMixingProductionPlanService mixingProductionPlanService) { + IMesXslMixingProductionPlanService mixingProductionPlanService, + MesXslStompNotifyService stompNotify) { this.mixingProductionPlanService = mixingProductionPlanService; + this.stompNotify = stompNotify; } @Operation(summary = "密炼生产计划维护-分页列表查询") @@ -56,6 +60,9 @@ public class MesXslMixingProductionPlanController @PostMapping("/saveAll") public Result saveAll(@RequestBody MesXslMixingProductionPlanSaveAllVO req) { mixingProductionPlanService.saveAllRows(req == null ? null : req.getRows()); + //update-begin---author:jiangxh ---date:20260617 for:【密炼计划】整表保存后广播桌面端同步----------- + stompNotify.publishMixingProductionPlanChanged("saveAll", null); + //update-end---author:jiangxh ---date:20260617 for:【密炼计划】整表保存后广播桌面端同步----------- return Result.OK("保存成功"); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java index 6994c8f7..4aa9b8d5 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java @@ -99,6 +99,14 @@ public class MesXslStompNotifyService { } //update-end---author:jiangxh ---date:20260617 for:【快检实验标准】桌面端只读同步 STOMP----------- + //update-begin---author:jiangxh ---date:20260617 for:【密炼计划】桌面端只读同步 STOMP----------- + /** 广播密炼生产计划变更事件到 /topic/sync/mes-mixing-production-plans */ + public void publishMixingProductionPlanChanged(String action, String mixingProductionPlanId) { + publish("/topic/sync/mes-mixing-production-plans", "MIXING_PRODUCTION_PLAN_CHANGED", + "mixingProductionPlanId", mixingProductionPlanId, action); + } + //update-end---author:jiangxh ---date:20260617 for:【密炼计划】桌面端只读同步 STOMP----------- + // ─────────────────────────── 私有辅助 ──────────────────────────── private void publish(String topic, String cmd, String idKey, String idValue, String action) { diff --git a/jeecg-boot/scan_mixing_plan.json b/jeecg-boot/scan_mixing_plan.json new file mode 100644 index 00000000..8b85cf67 --- /dev/null +++ b/jeecg-boot/scan_mixing_plan.json @@ -0,0 +1,374 @@ +{ + "scanKeyword": "MesXslMixingProductionPlan", + "entityClass": "MesXslMixingProductionPlan", + "tableName": "mes_xsl_mixing_production_plan", + "javaEntityFile": "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\entity\\MesXslMixingProductionPlan.java", + "hasIzEnable": false, + "hasCodeUniqueness": false, + "uniquenessFields": [], + "backendArch": { + "unifiedAnonCtrl": "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\controller\\MesXslDesktopAnonController.java", + "registeredInAnonCtrl": true, + "anonEndpoints": [ + "list" + ], + "stompNotifySvc": "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\service\\MesXslStompNotifyService.java", + "registeredInStompSvc": false, + "bizCtrlFile": "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\controller\\MesXslMixingProductionPlanController.java", + "bizCtrlUsesSharedNotify": false, + "bizCtrlHasPrivatePublish": false + }, + "wpfRegistrationStatus": { + "syncModuleService": false, + "syncModuleCoordinator": false, + "navigationView": false, + "stompSubscribe": false, + "menuRegistered": false, + "tenantMenuRegistered": false, + "syncModuleFilePath": "yy-admin-master\\YY.Admin\\Module\\SyncModule.cs", + "navExtFilePath": "yy-admin-master\\YY.Admin\\Module\\NavigationExtensions.cs", + "stompWsFilePath": "yy-admin-master\\YY.Admin\\Infrastructure\\Hubs\\StompWebSocketService.cs", + "menuSeedFilePath": "yy-admin-master\\YY.Admin.Core\\SeedData\\SysMenuSeedData.cs", + "summary": "✗ 待完成: SyncModule服务注册, SyncModule协调器注册, NavigationExtensions视图注册, STOMP订阅, 菜单注册" + }, + "menuSuggestion": { + "parentMenuId": 1300150000101, + "parentMenuTitle": "基础资料", + "nextMenuId": 1300150011401, + "nextOrderNo": 113, + "menuIdPattern": "130015001{N}01,N 每次 +1(1→101,2→201...)", + "alreadyExists": false, + "existingMenuId": null + }, + "apiPrefix": "/xslmes/mesXslMixingProductionPlan", + "stompCmd": "MIXING_PRODUCTION_PLAN_CHANGED", + "stompTopic": "/topic/sync/mes-mixing-production-plans", + "stompSubscriptionId": "sub-mes-xsl-mixing-production-plan", + "syncMode": "B", + "syncModeReason": "有/anon/免密端点,适合模式B", + "filterFields": [ + "machineId", + "machineName", + "planNo", + "planType", + "materialName" + ], + "fields": [ + { + "javaName": "sortNo", + "csName": "SortNo", + "sqlName": "sort_no", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "machineId", + "csName": "MachineId", + "sqlName": "machine_id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": "id" + }, + { + "javaName": "machineName", + "csName": "MachineName", + "sqlName": "machine_name", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "shiftFlag", + "csName": "ShiftFlag", + "sqlName": "shift_flag", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planDate", + "csName": "PlanDate", + "sqlName": "plan_date", + "javaType": "Date", + "csType": "DateTime?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planNo", + "csName": "PlanNo", + "sqlName": "plan_no", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planId", + "csName": "PlanId", + "sqlName": "plan_id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planType", + "csName": "PlanType", + "sqlName": "plan_type", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "sourceOrderId", + "csName": "SourceOrderId", + "sqlName": "source_order_id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "materialId", + "csName": "MaterialId", + "sqlName": "material_id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "materialName", + "csName": "MaterialName", + "sqlName": "material_name", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "orderNo", + "csName": "OrderNo", + "sqlName": "order_no", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "orderDate", + "csName": "OrderDate", + "sqlName": "order_date", + "javaType": "Date", + "csType": "DateTime?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "formulaName", + "csName": "FormulaName", + "sqlName": "formula_name", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planWeight", + "csName": "PlanWeight", + "sqlName": "plan_weight", + "javaType": "BigDecimal", + "csType": "double?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "plannedCarCount", + "csName": "PlannedCarCount", + "sqlName": "planned_car_count", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "scheduledCarCount", + "csName": "ScheduledCarCount", + "sqlName": "scheduled_car_count", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "finishedCarCount", + "csName": "FinishedCarCount", + "sqlName": "finished_car_count", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "planCount", + "csName": "PlanCount", + "sqlName": "plan_count", + "javaType": "Integer", + "csType": "int?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + { + "javaName": "remark", + "csName": "Remark", + "sqlName": "remark", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": false, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + } + ], + "pkField": { + "javaName": "id", + "csName": "Id", + "sqlName": "id", + "javaType": "String", + "csType": "string?", + "comment": "", + "isPk": true, + "isAudit": false, + "isIzEnable": false, + "required": false, + "dictCode": null + }, + "auditFields": [ + "TenantId", + "SysOrgCode", + "CreateBy", + "CreateTime", + "UpdateBy", + "UpdateTime", + "DelFlag" + ], + "dbConfig": { + "url": "jdbc:mysql://localhost:3306/jeecg-boot-dev?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai", + "username": "root", + "configFile": "jeecg-boot\\jeecg-boot-module\\jeecg-boot-module-airag\\src\\main\\resources\\application.yml" + }, + "dbColumns": [], + "csEntityStub": "public class MesXslMixingProductionPlan\n{\n public string? Id { get; set; }\n public int? SortNo { get; set; }\n public string? MachineId { get; set; } [Dict:id]\n public string? MachineName { get; set; }\n public int? ShiftFlag { get; set; }\n public DateTime? PlanDate { get; set; }\n public string? PlanNo { get; set; }\n public string? PlanId { get; set; }\n public string? PlanType { get; set; }\n public string? SourceOrderId { get; set; }\n public string? MaterialId { get; set; }\n public string? MaterialName { get; set; }\n public string? OrderNo { get; set; }\n public DateTime? OrderDate { get; set; }\n public string? FormulaName { get; set; }\n public double? PlanWeight { get; set; }\n public int? PlannedCarCount { get; set; }\n public int? ScheduledCarCount { get; set; }\n public int? FinishedCarCount { get; set; }\n public int? PlanCount { get; set; }\n public string? Remark { get; set; }\n public int? TenantId { get; set; }\n public string? SysOrgCode { get; set; }\n public string? CreateBy { get; set; }\n public DateTime? CreateTime { get; set; }\n public string? UpdateBy { get; set; }\n public DateTime? UpdateTime { get; set; }\n public int? DelFlag { get; set; }\n // 只读显示属性:\n // public string StatusText => Status == \"1\" ? \"停用\" : \"启用\";\n}", + "generationHints": { + "eventClassName": "MesXslMixingProductionPlanChangedEvent", + "serviceInterface": "IMixingProductionPlanService", + "serviceImpl": "MixingProductionPlanService", + "syncCoordinator": "MixingProductionPlanSyncCoordinator", + "listViewModel": "MixingProductionPlanListViewModel", + "editDialogViewModel": "MixingProductionPlanEditDialogViewModel", + "listView": "MixingProductionPlanListView", + "editDialogView": "MixingProductionPlanEditDialogView", + "pendingOpsFile": "mes-xsl-mixing-production-plan-pending-ops.json", + "cacheFile": "mes-xsl-mixing-production-plan-cache.json", + "nextMenuId": 1300150011401, + "nextMenuOrderNo": 113, + "backendFilesToModify": [ + "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\controller\\MesXslDesktopAnonController.java", + "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\service\\MesXslStompNotifyService.java", + "jeecg-boot\\jeecg-boot-module\\jeecg-module-xslmes\\src\\main\\java\\org\\jeecg\\modules\\xslmes\\controller\\MesXslMixingProductionPlanController.java", + "jeecg-boot-base-core/.../ShiroConfig.java" + ], + "wpfFilesToModify": [ + "yy-admin-master\\YY.Admin\\Module\\SyncModule.cs", + "yy-admin-master\\YY.Admin\\Module\\NavigationExtensions.cs", + "yy-admin-master\\YY.Admin\\Infrastructure\\Hubs\\StompWebSocketService.cs", + "yy-admin-master\\YY.Admin.Core\\SeedData\\SysMenuSeedData.cs", + "YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs" + ] + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Core/Events/MixingProductionPlanChangedEvent.cs b/yy-admin-master/YY.Admin.Core/Core/Events/MixingProductionPlanChangedEvent.cs new file mode 100644 index 00000000..7539dd49 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Events/MixingProductionPlanChangedEvent.cs @@ -0,0 +1,11 @@ +using Prism.Events; + +namespace YY.Admin.Core.Events; + +public class MixingProductionPlanChangedPayload +{ + public string Action { get; set; } = string.Empty; + public string? MixingProductionPlanId { get; set; } +} + +public class MixingProductionPlanChangedEvent : PubSubEvent { } diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/IMixingProductionPlanService.cs b/yy-admin-master/YY.Admin.Core/Core/Services/IMixingProductionPlanService.cs new file mode 100644 index 00000000..fe6b6310 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Services/IMixingProductionPlanService.cs @@ -0,0 +1,28 @@ +using YY.Admin.Core.Entity; + +namespace YY.Admin.Core.Services; + +/// 密炼生产计划(MES 只读同步) +public interface IMixingProductionPlanService +{ + Task PageAsync( + int pageNo, int pageSize, + DateTime? planDateFrom = null, + DateTime? planDateTo = null, + string? machineName = null, + int? shiftFlag = null, + string? planNo = null, + string? materialName = null, + CancellationToken ct = default); + + Task> GetAllCachedAsync(CancellationToken ct = default); + + /// 本地缓存是否有变更(有差异才写入) + Task SyncFromRemoteAsync(CancellationToken ct = default); +} + +public record MixingProductionPlanPageResult( + List Records, + long Total, + int PageNo, + int PageSize); diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/IRubberQuickTestStdService.cs b/yy-admin-master/YY.Admin.Core/Core/Services/IRubberQuickTestStdService.cs index 6ce239f3..da3ba174 100644 --- a/yy-admin-master/YY.Admin.Core/Core/Services/IRubberQuickTestStdService.cs +++ b/yy-admin-master/YY.Admin.Core/Core/Services/IRubberQuickTestStdService.cs @@ -14,7 +14,8 @@ public interface IRubberQuickTestStdService Task GetByIdAsync(string id, CancellationToken ct = default); - Task SyncFromRemoteAsync(CancellationToken ct = default); + /// 本地缓存是否有变更(有差异才写入) + Task SyncFromRemoteAsync(CancellationToken ct = default); } public record RubberQuickTestStdPageResult( diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslMixingProductionPlan.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslMixingProductionPlan.cs index 2994f4cf..2ff7dffd 100644 --- a/yy-admin-master/YY.Admin.Core/Entity/MesXslMixingProductionPlan.cs +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslMixingProductionPlan.cs @@ -1,6 +1,6 @@ namespace YY.Admin.Core.Entity; -/// 密炼生产计划维护(桌面端快检记录筛选用) +/// 密炼生产计划维护(MES 数据源,桌面端只读同步) public class MesXslMixingProductionPlan { public string? Id { get; set; } @@ -8,21 +8,43 @@ public class MesXslMixingProductionPlan public string? MachineId { get; set; } public string? MachineName { get; set; } - public string? MorningPlanId { get; set; } - public string? MorningPlanType { get; set; } - public string? MorningOrderNo { get; set; } - public DateTime? MorningOrderDate { get; set; } - public string? MorningFormulaName { get; set; } + /// 班次标识:1早班 2中班 3晚班 + public int? ShiftFlag { get; set; } - public string? NoonPlanId { get; set; } - public string? NoonPlanType { get; set; } - public string? NoonOrderNo { get; set; } - public DateTime? NoonOrderDate { get; set; } - public string? NoonFormulaName { get; set; } + /// 计划日期(密炼日期) + public DateTime? PlanDate { get; set; } - public string? NightPlanId { get; set; } - public string? NightPlanType { get; set; } - public string? NightOrderNo { get; set; } - public DateTime? NightOrderDate { get; set; } - public string? NightFormulaName { get; set; } + public string? PlanNo { get; set; } + public string? PlanId { get; set; } + public string? PlanType { get; set; } + public string? SourceOrderId { get; set; } + public string? MaterialId { get; set; } + public string? MaterialName { get; set; } + public string? OrderNo { get; set; } + public DateTime? OrderDate { get; set; } + public string? FormulaName { get; set; } + public double? PlanWeight { get; set; } + public int? PlannedCarCount { get; set; } + public int? ScheduledCarCount { get; set; } + public int? FinishedCarCount { get; set; } + /// 计划数量(MES plan_count) + public int? PlanCount { get; set; } + public string? Remark { get; set; } + public int? TenantId { get; set; } + public string? SysOrgCode { get; set; } + public string? CreateBy { get; set; } + public DateTime? CreateTime { get; set; } + public string? UpdateBy { get; set; } + public DateTime? UpdateTime { get; set; } + public int? DelFlag { get; set; } + + public string ShiftFlagText => ShiftFlag switch + { + 1 => "早班", + 2 => "中班", + 3 => "晚班", + _ => ShiftFlag?.ToString() ?? string.Empty + }; + + public string PlanDateText => PlanDate?.ToString("yyyy-MM-dd") ?? string.Empty; } diff --git a/yy-admin-master/YY.Admin.Core/Helper/MesReadOnlyCacheMergeHelper.cs b/yy-admin-master/YY.Admin.Core/Helper/MesReadOnlyCacheMergeHelper.cs new file mode 100644 index 00000000..2757706a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Helper/MesReadOnlyCacheMergeHelper.cs @@ -0,0 +1,66 @@ +namespace YY.Admin.Core.Helper; + +/// MES 只读数据:远端列表与本地缓存按 Id 对比合并 +public static class MesReadOnlyCacheMergeHelper +{ + public sealed record MergeResult(int Added, int Updated, int Removed) + { + public bool HasChanges => Added > 0 || Updated > 0 || Removed > 0; + } + + /// + /// 对比远端与本地,返回合并后的列表及变更统计。 + /// 内容相同则保留本地副本; 可定制变更行的合并策略(如保留本地子表)。 + /// + public static (List Merged, MergeResult Stats) Merge( + IReadOnlyList local, + IReadOnlyList remote, + Func getId, + Func isContentEqual, + Func clone, + Func? mergeUpdated = null) + { + var localById = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var item in local) + { + var id = getId(item); + if (!string.IsNullOrWhiteSpace(id)) + localById[id] = item; + } + + var remoteIds = new HashSet(StringComparer.OrdinalIgnoreCase); + var merged = new List(remote.Count); + int added = 0, updated = 0; + + foreach (var remoteItem in remote) + { + var id = getId(remoteItem); + if (string.IsNullOrWhiteSpace(id)) + { + merged.Add(clone(remoteItem)); + added++; + continue; + } + + remoteIds.Add(id); + + if (!localById.TryGetValue(id, out var localItem)) + { + merged.Add(clone(remoteItem)); + added++; + } + else if (!isContentEqual(localItem, remoteItem)) + { + merged.Add(mergeUpdated != null ? mergeUpdated(localItem, remoteItem) : clone(remoteItem)); + updated++; + } + else + { + merged.Add(clone(localItem)); + } + } + + int removed = localById.Keys.Count(id => !remoteIds.Contains(id)); + return (merged, new MergeResult(added, updated, removed)); + } +} diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs index 674134e3..86d6061a 100644 --- a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs @@ -52,6 +52,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData new SysMenu{ Id=1300150011201, Pid=1300150000101, Title="快检记录", Path="/xslmes/rubberQuickTestOperation", Name="rubberQuickTestOperation", Component="RubberQuickTestOperationView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=111 }, // 胶料快检实验标准(桌面端只读) new SysMenu{ Id=1300150011301, Pid=1300150000101, Title="胶料快检实验标准", Path="/xslmes/mesXslRubberQuickTestStd", Name="mesXslRubberQuickTestStd", Component="RubberQuickTestStdListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=112 }, + // 密炼计划 + new SysMenu{ Id=1300150011401, Pid=1300150000101, Title="密炼计划", Path="/xslmes/mesXslMixingProductionPlan", Name="mesXslMixingProductionPlan", Component="MixingProductionPlanListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=113 }, #endregion diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs index 52f35768..cc3bb0bb 100644 --- a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs @@ -34,6 +34,7 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150011101}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150011201}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150011301}, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150011401}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012101}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012111}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012121}, diff --git a/yy-admin-master/YY.Admin.Services/Service/MixingProductionPlan/MixingProductionPlanService.cs b/yy-admin-master/YY.Admin.Services/Service/MixingProductionPlan/MixingProductionPlanService.cs new file mode 100644 index 00000000..e1cedc17 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/MixingProductionPlan/MixingProductionPlanService.cs @@ -0,0 +1,326 @@ +using Microsoft.Extensions.Configuration; +using Prism.Events; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Web; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Events; +using YY.Admin.Core.Helper; +using YY.Admin.Core.Services; +namespace YY.Admin.Services.Service.MixingProductionPlan; + +/// 密炼生产计划:MES 只读拉取 + 本地缓存,断网读缓存,联网刷新 +public class MixingProductionPlanService : IMixingProductionPlanService, ISingletonDependency +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly INetworkMonitor _networkMonitor; + private readonly IEventAggregator _eventAggregator; + private readonly ILoggerService _logger; + private readonly SemaphoreSlim _syncLock = new(1, 1); + private readonly object _cacheLock = new(); + private readonly string _cacheFilePath; + private List _localCache = new(); + + private static readonly JsonSerializerOptions _jsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new NullableDateTimeJsonConverter() } + }; + + public MixingProductionPlanService( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + INetworkMonitor networkMonitor, + IEventAggregator eventAggregator, + ILoggerService logger) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + _networkMonitor = networkMonitor; + _eventAggregator = eventAggregator; + _logger = logger; + + var appDataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "YY.Admin", "sync-cache"); + Directory.CreateDirectory(appDataDir); + _cacheFilePath = Path.Combine(appDataDir, "mes-xsl-mixing-production-plan-cache.json"); + + LoadCacheFromDisk(); + _logger.Information($"[密炼计划] 服务初始化,缓存={_localCache.Count},在线={_networkMonitor.IsOnline}"); + + _networkMonitor.StatusChanged += OnNetworkStatusChanged; + if (_networkMonitor.IsOnline) + _ = Task.Run(() => SyncFromRemoteAsync(CancellationToken.None)); + } + + private string BaseUrl => (_configuration.GetValue("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/'); + private int DefaultTenantId => (int?)_configuration.GetValue("JeecgIntegration:DefaultTenantId") ?? 1002; + private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi"); + + public async Task PageAsync( + int pageNo, int pageSize, + DateTime? planDateFrom = null, + DateTime? planDateTo = null, + string? machineName = null, + int? shiftFlag = null, + string? planNo = null, + string? materialName = null, + CancellationToken ct = default) + { + if (_networkMonitor.IsOnline) + { + try + { + await SyncFromRemoteAsync(ct).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Warning($"[密炼计划] 列表拉取失败,使用本地缓存:{ex.Message}"); + } + } + + List source; + lock (_cacheLock) + source = _localCache.Select(Clone).ToList(); + + var filtered = ApplyFilters(source, planDateFrom, planDateTo, machineName, shiftFlag, planNo, materialName); + var total = filtered.Count; + var records = filtered + .Skip(Math.Max(0, (pageNo - 1) * pageSize)) + .Take(pageSize) + .ToList(); + return new MixingProductionPlanPageResult(records, total, pageNo, pageSize); + } + + public async Task> GetAllCachedAsync(CancellationToken ct = default) + { + if (_networkMonitor.IsOnline) + { + try + { + await SyncFromRemoteAsync(ct).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Warning($"[密炼计划] 全量拉取失败,使用本地缓存:{ex.Message}"); + } + } + + lock (_cacheLock) + return _localCache.Select(Clone).ToList(); + } + + public async Task SyncFromRemoteAsync(CancellationToken ct = default) + { + await _syncLock.WaitAsync(ct).ConfigureAwait(false); + try + { + if (!_networkMonitor.IsOnline) + return false; + + var all = new List(); + int pageNo = 1; + const int pageSize = 500; + while (true) + { + var query = HttpUtility.ParseQueryString(string.Empty); + query["pageNo"] = pageNo.ToString(); + query["pageSize"] = pageSize.ToString(); + query["tenantId"] = DefaultTenantId.ToString(); + var url = $"{BaseUrl}/xslmes/mesXslMixingProductionPlan/anon/list?{query}"; + + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + resp.EnsureSuccessStatusCode(); + + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("result", out var resultEl)) break; + + var page = resultEl.GetProperty("records") + .Deserialize>(_jsonOpts) ?? new(); + all.AddRange(page); + + long total = 0; + if (resultEl.TryGetProperty("total", out var totalEl)) total = totalEl.GetInt64(); + if (all.Count >= total || page.Count < pageSize) break; + pageNo++; + } + + List localSnapshot; + lock (_cacheLock) + localSnapshot = _localCache.Select(Clone).ToList(); + + var (merged, stats) = MesReadOnlyCacheMergeHelper.Merge( + localSnapshot, + all, + x => x.Id, + IsPlanContentEqual, + Clone); + + if (!stats.HasChanges) + { + _logger.Information($"[密炼计划] 与 MES 对比无差异,跳过更新 count={merged.Count}"); + return false; + } + + lock (_cacheLock) + { + _localCache = merged; + SaveCacheToDiskUnsafe(); + } + _logger.Information( + $"[密炼计划] 差异同步完成 total={merged.Count} 新增={stats.Added} 变更={stats.Updated} 删除={stats.Removed}"); + return true; + } + catch (Exception ex) + { + _logger.Warning($"[密炼计划] 远程同步失败:{ex.Message}"); + throw; + } + finally + { + _syncLock.Release(); + } + } + + private void OnNetworkStatusChanged(bool isOnline) + { + if (!isOnline) return; + _ = Task.Run(async () => + { + try + { + if (!await SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false)) + return; + _eventAggregator.GetEvent() + .Publish(new MixingProductionPlanChangedPayload { Action = "reconnect" }); + } + catch (Exception ex) + { + _logger.Warning($"[密炼计划] 重连同步失败:{ex.Message}"); + } + }); + } + + private static bool IsPlanContentEqual(MesXslMixingProductionPlan a, MesXslMixingProductionPlan b) => + string.Equals(GetPlanFingerprint(a), GetPlanFingerprint(b), StringComparison.Ordinal); + + private static string GetPlanFingerprint(MesXslMixingProductionPlan x) => + JsonSerializer.Serialize(Clone(x), _jsonOpts); + private static List ApplyFilters( + List source, + DateTime? planDateFrom, + DateTime? planDateTo, + string? machineName, + int? shiftFlag, + string? planNo, + string? materialName) + { + IEnumerable q = source; + if (planDateFrom.HasValue) + q = q.Where(x => x.PlanDate?.Date >= planDateFrom.Value.Date); + if (planDateTo.HasValue) + q = q.Where(x => x.PlanDate?.Date <= planDateTo.Value.Date); + if (!string.IsNullOrWhiteSpace(machineName)) + q = q.Where(x => (x.MachineName ?? "").Contains(machineName.Trim(), StringComparison.OrdinalIgnoreCase)); + if (shiftFlag.HasValue && shiftFlag.Value > 0) + q = q.Where(x => x.ShiftFlag == shiftFlag.Value); + if (!string.IsNullOrWhiteSpace(planNo)) + q = q.Where(x => (x.PlanNo ?? "").Contains(planNo.Trim(), StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(materialName)) + q = q.Where(x => (x.MaterialName ?? "").Contains(materialName.Trim(), StringComparison.OrdinalIgnoreCase)); + + return q + .OrderByDescending(x => x.PlanDate ?? DateTime.MinValue) + .ThenBy(x => x.SortNo ?? int.MaxValue) + .ThenBy(x => x.MachineName) + .ToList(); + } + + private void LoadCacheFromDisk() + { + try + { + if (!File.Exists(_cacheFilePath)) return; + _localCache = JsonSerializer.Deserialize>( + File.ReadAllText(_cacheFilePath), _jsonOpts) ?? new(); + } + catch + { + _localCache = new(); + } + } + + private void SaveCacheToDiskUnsafe() => + File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts)); + + private static MesXslMixingProductionPlan Clone(MesXslMixingProductionPlan x) => new() + { + Id = x.Id, + SortNo = x.SortNo, + MachineId = x.MachineId, + MachineName = x.MachineName, + ShiftFlag = x.ShiftFlag, + PlanDate = x.PlanDate, + PlanNo = x.PlanNo, + PlanId = x.PlanId, + PlanType = x.PlanType, + SourceOrderId = x.SourceOrderId, + MaterialId = x.MaterialId, + MaterialName = x.MaterialName, + OrderNo = x.OrderNo, + OrderDate = x.OrderDate, + FormulaName = x.FormulaName, + PlanWeight = x.PlanWeight, + PlannedCarCount = x.PlannedCarCount, + ScheduledCarCount = x.ScheduledCarCount, + FinishedCarCount = x.FinishedCarCount, + PlanCount = x.PlanCount, + Remark = x.Remark, + TenantId = x.TenantId, + SysOrgCode = x.SysOrgCode, + CreateBy = x.CreateBy, + CreateTime = x.CreateTime, + UpdateBy = x.UpdateBy, + UpdateTime = x.UpdateTime, + DelFlag = x.DelFlag + }; + + private sealed class NullableDateTimeJsonConverter : JsonConverter + { + private static readonly string[] Formats = + [ + "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss.fff", + "yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm:ss.fff", + "yyyy-MM-ddTHH:mm:ssZ", "yyyy-MM-ddTHH:mm:ss.fffZ", + "yyyy-MM-dd" + ]; + + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) return null; + if (reader.TokenType == JsonTokenType.String) + { + var raw = reader.GetString(); + if (string.IsNullOrWhiteSpace(raw)) return null; + if (DateTime.TryParseExact(raw, Formats, System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.AssumeLocal, out var dt)) return dt; + if (DateTime.TryParse(raw, out var fb)) return fb; + } + throw new JsonException($"无法转换为 DateTime?,token={reader.TokenType}"); + } + + public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) + { + if (value.HasValue) writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss")); + else writer.WriteNullValue(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/MixingProductionPlan/MixingProductionPlanSyncCoordinator.cs b/yy-admin-master/YY.Admin.Services/Service/MixingProductionPlan/MixingProductionPlanSyncCoordinator.cs new file mode 100644 index 00000000..2e97e3c6 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/MixingProductionPlan/MixingProductionPlanSyncCoordinator.cs @@ -0,0 +1,63 @@ +using Prism.Events; +using System.Text.Json; +using YY.Admin.Core; +using YY.Admin.Core.Events; +using YY.Admin.Core.Services; + +namespace YY.Admin.Services.Service.MixingProductionPlan; + +public class MixingProductionPlanSyncCoordinator : ISingletonDependency +{ + private readonly IEventAggregator _eventAggregator; + private readonly ILoggerService _logger; + + public MixingProductionPlanSyncCoordinator( + IEventAggregator eventAggregator, + SyncPollManager pollManager, + ILoggerService logger) + { + _eventAggregator = eventAggregator; + _logger = logger; + + _eventAggregator.GetEvent() + .Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread); + + pollManager.Register("密炼计划", () => + { + _eventAggregator.GetEvent() + .Publish(new MixingProductionPlanChangedPayload { Action = "poll" }); + return Task.CompletedTask; + }); + + _logger.Information("[密炼计划] MixingProductionPlanSyncCoordinator 已启动"); + } + + private void OnRemoteCommand(RemoteCommandPayload payload) + { + try + { + var json = payload.CommandJson ?? string.Empty; + if (string.IsNullOrWhiteSpace(json)) return; + + using var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("cmd", out var cmdEl)) return; + if (!cmdEl.GetString()?.Equals("MIXING_PRODUCTION_PLAN_CHANGED", StringComparison.OrdinalIgnoreCase) ?? true) + return; + + doc.RootElement.TryGetProperty("action", out var actionEl); + doc.RootElement.TryGetProperty("mixingProductionPlanId", out var idEl); + + var changed = new MixingProductionPlanChangedPayload + { + Action = actionEl.GetString() ?? string.Empty, + MixingProductionPlanId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null + }; + _logger.Information($"[密炼计划] STOMP action={changed.Action}, id={changed.MixingProductionPlanId}"); + _eventAggregator.GetEvent().Publish(changed); + } + catch (Exception ex) + { + _logger.Warning($"[密炼计划] 处理 STOMP 命令失败:{ex.Message}"); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/RubberQuickTest/RubberQuickTestOperationService.cs b/yy-admin-master/YY.Admin.Services/Service/RubberQuickTest/RubberQuickTestOperationService.cs index d8c2cc94..e0484ac6 100644 --- a/yy-admin-master/YY.Admin.Services/Service/RubberQuickTest/RubberQuickTestOperationService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/RubberQuickTest/RubberQuickTestOperationService.cs @@ -14,6 +14,7 @@ public class RubberQuickTestOperationService : IRubberQuickTestOperationService, private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly INetworkMonitor _networkMonitor; + private readonly IMixingProductionPlanService _mixingProductionPlanService; private readonly ILoggerService _logger; private static readonly JsonSerializerOptions _jsonOpts = new() @@ -27,11 +28,13 @@ public class RubberQuickTestOperationService : IRubberQuickTestOperationService, IHttpClientFactory httpClientFactory, IConfiguration configuration, INetworkMonitor networkMonitor, + IMixingProductionPlanService mixingProductionPlanService, ILoggerService logger) { _httpClientFactory = httpClientFactory; _configuration = configuration; _networkMonitor = networkMonitor; + _mixingProductionPlanService = mixingProductionPlanService; _logger = logger; } @@ -40,32 +43,7 @@ public class RubberQuickTestOperationService : IRubberQuickTestOperationService, public async Task> GetMixingProductionPlansAsync(CancellationToken ct = default) { - if (!_networkMonitor.IsOnline) - throw new InvalidOperationException("网络未连接,无法加载密炼生产计划"); - - var result = new List(); - int pageNo = 1; - const int pageSize = 500; - while (true) - { - var url = $"{BaseUrl}/xslmes/mesXslMixingProductionPlan/anon/list?pageNo={pageNo}&pageSize={pageSize}&tenantId={DefaultTenantId}"; - using var client = _httpClientFactory.CreateClient("JeecgApi"); - var resp = await client.GetAsync(url, ct).ConfigureAwait(false); - resp.EnsureSuccessStatusCode(); - var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - using var doc = JsonDocument.Parse(json); - if (!doc.RootElement.TryGetProperty("result", out var resultEl)) break; - if (resultEl.TryGetProperty("records", out var recordsEl)) - { - var page = recordsEl.Deserialize>(_jsonOpts); - if (page != null) result.AddRange(page); - } - long total = 0; - if (resultEl.TryGetProperty("total", out var totalEl)) total = totalEl.GetInt64(); - if (result.Count >= total || (resultEl.TryGetProperty("records", out var r2) && r2.GetArrayLength() < pageSize)) break; - pageNo++; - } - + var result = await _mixingProductionPlanService.GetAllCachedAsync(ct).ConfigureAwait(false); _logger.Information($"[快检记录] 加载密炼生产计划 {result.Count} 条"); return result; } diff --git a/yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdService.cs b/yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdService.cs index 1890e2d1..628b8b65 100644 --- a/yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdService.cs @@ -8,6 +8,7 @@ using System.Web; using YY.Admin.Core; using YY.Admin.Core.Entity; using YY.Admin.Core.Events; +using YY.Admin.Core.Helper; using YY.Admin.Core.Services; namespace YY.Admin.Services.Service.RubberQuickTestStd; @@ -115,7 +116,7 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD var entity = resultEl.Deserialize(_jsonOpts); if (entity != null) { - UpsertLocalCacheMain(entity); + UpsertIfChanged(entity); return CloneMain(entity); } } @@ -134,14 +135,14 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD } } - public async Task SyncFromRemoteAsync(CancellationToken ct = default) + public async Task SyncFromRemoteAsync(CancellationToken ct = default) { await _syncLock.WaitAsync(ct).ConfigureAwait(false); try { if (!_networkMonitor.IsOnline) - return; + return false; var query = HttpUtility.ParseQueryString(string.Empty); query["pageNo"] = "1"; @@ -158,16 +159,37 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD var records = doc.RootElement.GetProperty("result").GetProperty("records") .Deserialize>(_jsonOpts) ?? new(); + List localSnapshot; + lock (_cacheLock) + localSnapshot = _localCache.Select(CloneMain).ToList(); + + var (merged, stats) = MesReadOnlyCacheMergeHelper.Merge( + localSnapshot, + records, + x => x.Id, + IsStdListContentEqual, + CloneMain, + MergeStdUpdated); + + if (!stats.HasChanges) + { + _logger.Information($"[快检实验标准] 与 MES 对比无差异,跳过更新 count={merged.Count}"); + return false; + } + lock (_cacheLock) { - _localCache = records.Select(CloneMain).ToList(); + _localCache = merged; SaveCacheToDiskUnsafe(); } - _logger.Information($"[快检实验标准] 同步完成 count={records.Count}"); + _logger.Information( + $"[快检实验标准] 差异同步完成 total={merged.Count} 新增={stats.Added} 变更={stats.Updated} 删除={stats.Removed}"); + return true; } catch (Exception ex) { _logger.Warning($"[快检实验标准] 远程同步失败:{ex.Message}"); + return false; } finally { @@ -180,7 +202,8 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD if (!isOnline) return; _ = Task.Run(async () => { - await SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false); + if (!await SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false)) + return; _eventAggregator.GetEvent() .Publish(new RubberQuickTestStdChangedPayload { Action = "reconnect" }); }); @@ -202,19 +225,67 @@ public class RubberQuickTestStdService : IRubberQuickTestStdService, ISingletonD return q.OrderByDescending(x => x.CreateTime ?? DateTime.MinValue).ToList(); } - private void UpsertLocalCacheMain(MesXslRubberQuickTestStd entity) + private bool UpsertIfChanged(MesXslRubberQuickTestStd entity) { - if (string.IsNullOrWhiteSpace(entity.Id)) return; + if (string.IsNullOrWhiteSpace(entity.Id)) return false; lock (_cacheLock) { var idx = _localCache.FindIndex(x => string.Equals(x.Id, entity.Id, StringComparison.OrdinalIgnoreCase)); - var copy = CloneMain(entity); - if (idx >= 0) _localCache[idx] = copy; - else _localCache.Insert(0, copy); + if (idx >= 0) + { + if (IsStdDetailContentEqual(_localCache[idx], entity)) + return false; + _localCache[idx] = CloneMain(entity); + } + else + { + _localCache.Insert(0, CloneMain(entity)); + } SaveCacheToDiskUnsafe(); + return true; } } + private static bool IsStdListContentEqual(MesXslRubberQuickTestStd a, MesXslRubberQuickTestStd b) => + string.Equals(GetStdListFingerprint(a), GetStdListFingerprint(b), StringComparison.Ordinal); + + private static bool IsStdDetailContentEqual(MesXslRubberQuickTestStd a, MesXslRubberQuickTestStd b) => + string.Equals(GetStdDetailFingerprint(a), GetStdDetailFingerprint(b), StringComparison.Ordinal); + + private static string GetStdListFingerprint(MesXslRubberQuickTestStd x) + { + var snap = CloneMain(x); + snap.LineList = null; + return JsonSerializer.Serialize(snap, _jsonOpts); + } + + private static string GetStdDetailFingerprint(MesXslRubberQuickTestStd x) => + JsonSerializer.Serialize(CloneMain(x), _jsonOpts); + + private static MesXslRubberQuickTestStd MergeStdUpdated(MesXslRubberQuickTestStd local, MesXslRubberQuickTestStd remote) + { + var copy = CloneMain(remote); + if ((copy.LineList == null || copy.LineList.Count == 0) && local.LineList is { Count: > 0 }) + { + copy.LineList = local.LineList.Select(l => new MesXslRubberQuickTestStdLine + { + Id = l.Id, + StdId = l.StdId, + DataPointId = l.DataPointId, + PointName = l.PointName, + LowerLimit = l.LowerLimit, + UpperLimit = l.UpperLimit, + LowerWarn = l.LowerWarn, + UpperWarn = l.UpperWarn, + TargetValue = l.TargetValue, + SortNo = l.SortNo + }).ToList(); + } + return copy; + } + + private void UpsertLocalCacheMain(MesXslRubberQuickTestStd entity) => UpsertIfChanged(entity); + private void LoadCacheFromDisk() { try diff --git a/yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdSyncCoordinator.cs b/yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdSyncCoordinator.cs index 4b62b365..b396f3d0 100644 --- a/yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdSyncCoordinator.cs +++ b/yy-admin-master/YY.Admin.Services/Service/RubberQuickTestStd/RubberQuickTestStdSyncCoordinator.cs @@ -2,7 +2,6 @@ using Prism.Events; using System.Text.Json; using YY.Admin.Core; using YY.Admin.Core.Events; -using YY.Admin.Core.Events; namespace YY.Admin.Services.Service.RubberQuickTestStd; @@ -21,8 +20,6 @@ public class RubberQuickTestStdSyncCoordinator : ISingletonDependency _eventAggregator.GetEvent() .Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread); - _eventAggregator.GetEvent() - .Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread); pollManager.Register("胶料快检实验标准", () => { @@ -34,14 +31,6 @@ public class RubberQuickTestStdSyncCoordinator : ISingletonDependency _logger.Information("[快检实验标准] RubberQuickTestStdSyncCoordinator 已启动"); } - private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload) - { - if (!payload.IsOnline) return; - _logger.Information("[快检实验标准] 网络恢复,触发补偿刷新"); - _eventAggregator.GetEvent() - .Publish(new RubberQuickTestStdChangedPayload { Action = "reconnect" }); - } - private void OnRemoteCommand(RemoteCommandPayload payload) { try diff --git a/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs b/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs index 7780f1a3..d5a809da 100644 --- a/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs +++ b/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs @@ -178,6 +178,10 @@ public class StompWebSocketService : ISignalRService await SendFrameAsync( BuildSubscribeFrame("sub-mes-rubber-quick-test-stds", "/topic/sync/mes-rubber-quick-test-stds"), cancellationToken).ConfigureAwait(false); + // 密炼生产计划变更:订阅 /topic/sync/mes-mixing-production-plans + await SendFrameAsync( + BuildSubscribeFrame("sub-mes-xsl-mixing-production-plan", "/topic/sync/mes-mixing-production-plans"), + cancellationToken).ConfigureAwait(false); // 订阅服务端 PONG 回复(应用层假在线检测) await SendFrameAsync( diff --git a/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs index f7bf38dd..e7e92f9e 100644 --- a/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs +++ b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs @@ -20,6 +20,7 @@ using YY.Admin.Views.Print; using YY.Admin.Views.MixerMaterialTareStrategy; using YY.Admin.Views.RubberQuickTest; using YY.Admin.Views.RubberQuickTestStd; +using YY.Admin.Views.MixingProductionPlan; namespace YY.Admin { @@ -101,6 +102,8 @@ namespace YY.Admin containerRegistry.RegisterForNavigation(); // 胶料快检实验标准(只读) containerRegistry.RegisterForNavigation(); + // 密炼计划(只读) + containerRegistry.RegisterForNavigation(); // 打印设置 containerRegistry.RegisterForNavigation(); // 打印模板列表 diff --git a/yy-admin-master/YY.Admin/Module/SyncModule.cs b/yy-admin-master/YY.Admin/Module/SyncModule.cs index 5e3de7f3..646aa9ec 100644 --- a/yy-admin-master/YY.Admin/Module/SyncModule.cs +++ b/yy-admin-master/YY.Admin/Module/SyncModule.cs @@ -29,6 +29,7 @@ using YY.Admin.Services.Service.WeightRecord; using YY.Admin.Services.Service.Print; using YY.Admin.Services.Service.RubberQuickTest; using YY.Admin.Services.Service.RubberQuickTestStd; +using YY.Admin.Services.Service.MixingProductionPlan; namespace YY.Admin.Module; @@ -95,6 +96,10 @@ public class SyncModule : IModule containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); + // 密炼计划(MES 只读同步) + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + var serviceCollection = new ServiceCollection(); serviceCollection.AddTransient(); serviceCollection.AddHttpClient("JeecgApi", (sp, client) => @@ -167,6 +172,8 @@ public class SyncModule : IModule _ = containerProvider.Resolve(); // 胶料快检实验标准只读同步协调器 _ = containerProvider.Resolve(); + // 密炼计划只读同步协调器 + _ = containerProvider.Resolve(); } private static IAsyncPolicy GetRetryPolicy() diff --git a/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs index a77d596e..256ee8ee 100644 --- a/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs @@ -162,6 +162,11 @@ namespace YY.Admin.ViewModels.Control ["/xslmes/mesXslRubberQuickTestStd"] = "RubberQuickTestStdListView", ["mesXslRubberQuickTestStd"] = "RubberQuickTestStdListView", + // 已实现页面:密炼计划(只读) + ["MixingProductionPlanListView"] = "MixingProductionPlanListView", + ["/xslmes/mesXslMixingProductionPlan"] = "MixingProductionPlanListView", + ["mesXslMixingProductionPlan"] = "MixingProductionPlanListView", + // 已实现页面:打印设置 ["PrintSettingsView"] = "PrintSettingsView", ["/system/printSettings"] = "PrintSettingsView", diff --git a/yy-admin-master/YY.Admin/ViewModels/MixingProductionPlan/MixingProductionPlanListViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/MixingProductionPlan/MixingProductionPlanListViewModel.cs new file mode 100644 index 00000000..1a94bb75 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/MixingProductionPlan/MixingProductionPlanListViewModel.cs @@ -0,0 +1,139 @@ +using HandyControl.Controls; +using Prism.Events; +using System.Collections.ObjectModel; +using System.Diagnostics; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Events; +using YY.Admin.Core.Helper; +using YY.Admin.Core.Services; +using YY.Admin.Services.Service; + +namespace YY.Admin.ViewModels.MixingProductionPlan; + +public class MixingProductionPlanListViewModel : BaseViewModel +{ + private readonly IMixingProductionPlanService _planService; + private SubscriptionToken? _changedToken; + + private ObservableCollection _items = new(); + public ObservableCollection Items + { + get => _items; + set => SetProperty(ref _items, value); + } + + private long _total; + public long Total { get => _total; set => SetProperty(ref _total, value); } + + private int _pageNo = 1; + public int PageNo { get => _pageNo; set => SetProperty(ref _pageNo, value); } + + private int _pageSize = 20; + public int PageSize { get => _pageSize; set => SetProperty(ref _pageSize, value); } + + private DateTime? _filterPlanDateFrom; + public DateTime? FilterPlanDateFrom { get => _filterPlanDateFrom; set => SetProperty(ref _filterPlanDateFrom, value); } + + private DateTime? _filterPlanDateTo; + public DateTime? FilterPlanDateTo { get => _filterPlanDateTo; set => SetProperty(ref _filterPlanDateTo, value); } + + private string? _filterMachineName; + public string? FilterMachineName { get => _filterMachineName; set => SetProperty(ref _filterMachineName, value); } + + private int? _filterShiftFlag; + public int? FilterShiftFlag { get => _filterShiftFlag; set => SetProperty(ref _filterShiftFlag, value); } + + private string? _filterPlanNo; + public string? FilterPlanNo { get => _filterPlanNo; set => SetProperty(ref _filterPlanNo, value); } + + private string? _filterMaterialName; + public string? FilterMaterialName { get => _filterMaterialName; set => SetProperty(ref _filterMaterialName, value); } + + public ObservableCollection> ShiftOptions { get; } = new() + { + new KeyValuePair("全部", null), + new KeyValuePair("早班", 1), + new KeyValuePair("中班", 2), + new KeyValuePair("晚班", 3) + }; + + public DelegateCommand SearchCommand { get; } + public DelegateCommand ResetCommand { get; } + public DelegateCommand PrevPageCommand { get; } + public DelegateCommand NextPageCommand { get; } + + public MixingProductionPlanListViewModel( + IMixingProductionPlanService planService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _planService = planService; + + SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); }); + ResetCommand = new DelegateCommand(async () => + { + FilterPlanDateFrom = null; + FilterPlanDateTo = null; + FilterMachineName = null; + FilterShiftFlag = null; + FilterPlanNo = null; + FilterMaterialName = null; + PageNo = 1; + await LoadAsync(); + }); + PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } }); + NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } }); + + _changedToken = _eventAggregator.GetEvent() + .Subscribe(async _ => await LoadAsync(), ThreadOption.UIThread); + + _ = InitializeAsync(); + } + + private async Task InitializeAsync() + { + try + { + await UIHelper.WaitForRenderAsync(); + await LoadAsync(); + } + catch (Exception ex) + { + Debug.WriteLine($"密炼计划列表初始化失败: {ex.Message}"); + } + } + + public async Task LoadAsync() + { + try + { + IsLoading = true; + var result = await _planService.PageAsync( + PageNo, PageSize, + FilterPlanDateFrom, FilterPlanDateTo, + FilterMachineName, FilterShiftFlag, + FilterPlanNo, FilterMaterialName); + Items = new ObservableCollection(result.Records); + Total = result.Total; + } + catch (Exception ex) + { + Growl.Error($"加载密炼计划失败:{ex.Message}"); + } + finally + { + IsLoading = false; + } + } + + protected override void CleanUp() + { + base.CleanUp(); + if (_changedToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_changedToken); + _changedToken = null; + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/RubberQuickTest/RubberQuickTestOperationViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/RubberQuickTest/RubberQuickTestOperationViewModel.cs index 115cf95a..095d81c6 100644 --- a/yy-admin-master/YY.Admin/ViewModels/RubberQuickTest/RubberQuickTestOperationViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/RubberQuickTest/RubberQuickTestOperationViewModel.cs @@ -25,9 +25,10 @@ public class MixingPlanShiftOption public string? PlanId { get; set; } public string? OrderNo { get; set; } public string? FormulaName { get; set; } + public string? MaterialName { get; set; } public string DisplayText => string.IsNullOrWhiteSpace(OrderNo) - ? FormulaName ?? string.Empty - : $"{OrderNo} | {FormulaName}"; + ? MaterialName ?? FormulaName ?? string.Empty + : $"{OrderNo} | {MaterialName ?? FormulaName}"; } public class QuickTestInspectCellViewModel : BindableBase @@ -217,7 +218,7 @@ public class RubberQuickTestOperationViewModel : BaseViewModel public string? ProductionOrderNo => _selectedPlan?.OrderNo; public string? MachineName => _selectedPlan?.MachineName ?? SelectedMachine; public string? WorkShiftDisplay => SelectedShift?.Name ?? string.Empty; - public string? RubberMaterialName => _selectedPlan?.FormulaName; + public string? RubberMaterialName => _selectedPlan?.MaterialName ?? _selectedPlan?.FormulaName; private string? _trainNo; public string? TrainNo @@ -286,32 +287,25 @@ public class RubberQuickTestOperationViewModel : BaseViewModel _allShiftOptions.Clear(); foreach (var row in _allPlans) { - AddShiftOption(row, "morning", "1", "早班", row.MorningPlanId, row.MorningOrderDate, row.MorningOrderNo, row.MorningFormulaName); - AddShiftOption(row, "noon", "2", "中班", row.NoonPlanId, row.NoonOrderDate, row.NoonOrderNo, row.NoonFormulaName); - AddShiftOption(row, "night", "3", "晚班", row.NightPlanId, row.NightOrderDate, row.NightOrderNo, row.NightFormulaName); + if (string.IsNullOrWhiteSpace(row.PlanId)) continue; + var shiftCode = row.ShiftFlag?.ToString() ?? string.Empty; + _allShiftOptions.Add(new MixingPlanShiftOption + { + PlanRowId = row.Id ?? string.Empty, + MachineId = row.MachineId, + MachineName = row.MachineName, + ShiftKey = shiftCode, + ShiftCode = shiftCode, + ShiftName = row.ShiftFlagText, + OrderDate = row.PlanDate, + PlanId = row.PlanId, + OrderNo = row.OrderNo, + FormulaName = row.FormulaName, + MaterialName = row.MaterialName + }); } } - private void AddShiftOption( - MesXslMixingProductionPlan row, string shiftKey, string shiftCode, string shiftName, - string? planId, DateTime? orderDate, string? orderNo, string? formulaName) - { - if (string.IsNullOrWhiteSpace(planId) || string.IsNullOrWhiteSpace(orderNo)) return; - _allShiftOptions.Add(new MixingPlanShiftOption - { - PlanRowId = row.Id ?? string.Empty, - MachineId = row.MachineId, - MachineName = row.MachineName, - ShiftKey = shiftKey, - ShiftCode = shiftCode, - ShiftName = shiftName, - OrderDate = orderDate, - PlanId = planId, - OrderNo = orderNo, - FormulaName = formulaName - }); - } - private IEnumerable FilteredByDate => _allShiftOptions.Where(o => o.OrderDate?.Date == MixingDate.Date); diff --git a/yy-admin-master/YY.Admin/Views/MixingProductionPlan/MixingProductionPlanListView.xaml b/yy-admin-master/YY.Admin/Views/MixingProductionPlan/MixingProductionPlanListView.xaml new file mode 100644 index 00000000..24752f33 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/MixingProductionPlan/MixingProductionPlanListView.xaml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +