桌面端新增密炼计划获取

This commit is contained in:
2026-06-17 17:52:31 +08:00
parent 816af5df6e
commit 372dc10be2
23 changed files with 1335 additions and 92 deletions

View File

@@ -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<MesXslMixingProductionPlan, IMesXslMixingProductionPlanService> {
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<String> 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("保存成功");
}

View File

@@ -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) {

View File

@@ -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}01N 每次 +11→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"
]
}
}

View File

@@ -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<MixingProductionPlanChangedPayload> { }

View File

@@ -0,0 +1,28 @@
using YY.Admin.Core.Entity;
namespace YY.Admin.Core.Services;
/// <summary>密炼生产计划MES 只读同步)</summary>
public interface IMixingProductionPlanService
{
Task<MixingProductionPlanPageResult> 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<List<MesXslMixingProductionPlan>> GetAllCachedAsync(CancellationToken ct = default);
/// <returns>本地缓存是否有变更(有差异才写入)</returns>
Task<bool> SyncFromRemoteAsync(CancellationToken ct = default);
}
public record MixingProductionPlanPageResult(
List<MesXslMixingProductionPlan> Records,
long Total,
int PageNo,
int PageSize);

View File

@@ -14,7 +14,8 @@ public interface IRubberQuickTestStdService
Task<MesXslRubberQuickTestStd?> GetByIdAsync(string id, CancellationToken ct = default);
Task SyncFromRemoteAsync(CancellationToken ct = default);
/// <returns>本地缓存是否有变更(有差异才写入)</returns>
Task<bool> SyncFromRemoteAsync(CancellationToken ct = default);
}
public record RubberQuickTestStdPageResult(

View File

@@ -1,6 +1,6 @@
namespace YY.Admin.Core.Entity;
/// <summary>密炼生产计划维护(桌面端快检记录筛选用</summary>
/// <summary>密炼生产计划维护(MES 数据源,桌面端只读同步</summary>
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; }
/// <summary>班次标识1早班 2中班 3晚班</summary>
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; }
/// <summary>计划日期(密炼日期)</summary>
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; }
/// <summary>计划数量MES plan_count</summary>
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;
}

View File

@@ -0,0 +1,66 @@
namespace YY.Admin.Core.Helper;
/// <summary>MES 只读数据:远端列表与本地缓存按 Id 对比合并</summary>
public static class MesReadOnlyCacheMergeHelper
{
public sealed record MergeResult(int Added, int Updated, int Removed)
{
public bool HasChanges => Added > 0 || Updated > 0 || Removed > 0;
}
/// <summary>
/// 对比远端与本地,返回合并后的列表及变更统计。
/// 内容相同则保留本地副本;<paramref name="mergeUpdated"/> 可定制变更行的合并策略(如保留本地子表)。
/// </summary>
public static (List<T> Merged, MergeResult Stats) Merge<T>(
IReadOnlyList<T> local,
IReadOnlyList<T> remote,
Func<T, string?> getId,
Func<T, T, bool> isContentEqual,
Func<T, T> clone,
Func<T, T, T>? mergeUpdated = null)
{
var localById = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
foreach (var item in local)
{
var id = getId(item);
if (!string.IsNullOrWhiteSpace(id))
localById[id] = item;
}
var remoteIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var merged = new List<T>(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));
}
}

View File

@@ -52,6 +52,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
new SysMenu{ Id=1300150011201, Pid=1300150000101, Title="快检记录", Path="/xslmes/rubberQuickTestOperation", Name="rubberQuickTestOperation", Component="RubberQuickTestOperationView", Icon="&#xe7de;", 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="&#xe7ce;", 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="&#xe7ce;", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=113 },
#endregion

View File

@@ -34,6 +34,7 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData<SysTenantMenu>
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},

View File

@@ -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;
/// <summary>密炼生产计划MES 只读拉取 + 本地缓存,断网读缓存,联网刷新</summary>
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<MesXslMixingProductionPlan> _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<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("JeecgIntegration:DefaultTenantId") ?? 1002;
private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi");
public async Task<MixingProductionPlanPageResult> 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<MesXslMixingProductionPlan> 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<List<MesXslMixingProductionPlan>> 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<bool> SyncFromRemoteAsync(CancellationToken ct = default)
{
await _syncLock.WaitAsync(ct).ConfigureAwait(false);
try
{
if (!_networkMonitor.IsOnline)
return false;
var all = new List<MesXslMixingProductionPlan>();
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<List<MesXslMixingProductionPlan>>(_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<MesXslMixingProductionPlan> 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<MixingProductionPlanChangedEvent>()
.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<MesXslMixingProductionPlan> ApplyFilters(
List<MesXslMixingProductionPlan> source,
DateTime? planDateFrom,
DateTime? planDateTo,
string? machineName,
int? shiftFlag,
string? planNo,
string? materialName)
{
IEnumerable<MesXslMixingProductionPlan> 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<List<MesXslMixingProductionPlan>>(
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<DateTime?>
{
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();
}
}
}

View File

@@ -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<RemoteCommandReceivedEvent>()
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
pollManager.Register("密炼计划", () =>
{
_eventAggregator.GetEvent<MixingProductionPlanChangedEvent>()
.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<MixingProductionPlanChangedEvent>().Publish(changed);
}
catch (Exception ex)
{
_logger.Warning($"[密炼计划] 处理 STOMP 命令失败:{ex.Message}");
}
}
}

View File

@@ -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<List<MesXslMixingProductionPlan>> GetMixingProductionPlansAsync(CancellationToken ct = default)
{
if (!_networkMonitor.IsOnline)
throw new InvalidOperationException("网络未连接,无法加载密炼生产计划");
var result = new List<MesXslMixingProductionPlan>();
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<List<MesXslMixingProductionPlan>>(_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;
}

View File

@@ -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<MesXslRubberQuickTestStd>(_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<bool> 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<List<MesXslRubberQuickTestStd>>(_jsonOpts) ?? new();
List<MesXslRubberQuickTestStd> 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<RubberQuickTestStdChangedEvent>()
.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

View File

@@ -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<RemoteCommandReceivedEvent>()
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
.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<RubberQuickTestStdChangedEvent>()
.Publish(new RubberQuickTestStdChangedPayload { Action = "reconnect" });
}
private void OnRemoteCommand(RemoteCommandPayload payload)
{
try

View File

@@ -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(

View File

@@ -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<RubberQuickTestOperationView>();
// 胶料快检实验标准(只读)
containerRegistry.RegisterForNavigation<RubberQuickTestStdListView>();
// 密炼计划(只读)
containerRegistry.RegisterForNavigation<MixingProductionPlanListView>();
// 打印设置
containerRegistry.RegisterForNavigation<PrintSettingsView>();
// 打印模板列表

View File

@@ -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<IRubberQuickTestStdService, RubberQuickTestStdService>();
containerRegistry.RegisterSingleton<RubberQuickTestStdSyncCoordinator>();
// 密炼计划MES 只读同步)
containerRegistry.RegisterSingleton<IMixingProductionPlanService, MixingProductionPlanService>();
containerRegistry.RegisterSingleton<MixingProductionPlanSyncCoordinator>();
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<DisconnectGuardHandler>();
serviceCollection.AddHttpClient("JeecgApi", (sp, client) =>
@@ -167,6 +172,8 @@ public class SyncModule : IModule
_ = containerProvider.Resolve<PrintBizTemplateBindSyncCoordinator>();
// 胶料快检实验标准只读同步协调器
_ = containerProvider.Resolve<RubberQuickTestStdSyncCoordinator>();
// 密炼计划只读同步协调器
_ = containerProvider.Resolve<MixingProductionPlanSyncCoordinator>();
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()

View File

@@ -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",

View File

@@ -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<MesXslMixingProductionPlan> _items = new();
public ObservableCollection<MesXslMixingProductionPlan> 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<KeyValuePair<string, int?>> ShiftOptions { get; } = new()
{
new KeyValuePair<string, int?>("全部", null),
new KeyValuePair<string, int?>("早班", 1),
new KeyValuePair<string, int?>("中班", 2),
new KeyValuePair<string, int?>("晚班", 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<MixingProductionPlanChangedEvent>()
.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<MesXslMixingProductionPlan>(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<MixingProductionPlanChangedEvent>().Unsubscribe(_changedToken);
_changedToken = null;
}
}
}

View File

@@ -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<MixingPlanShiftOption> FilteredByDate =>
_allShiftOptions.Where(o => o.OrderDate?.Date == MixingDate.Date);

View File

@@ -0,0 +1,133 @@
<UserControl x:Class="YY.Admin.Views.MixingProductionPlan.MixingProductionPlanListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<Grid Style="{StaticResource BaseViewStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" CornerRadius="4" Margin="0 0 -10 0">
<hc:Row>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:DatePicker SelectedDate="{Binding FilterPlanDateFrom}"
Margin="0 0 10 10"
hc:InfoElement.Title="密炼日期起"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="80"
hc:InfoElement.Placeholder="开始日期"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:DatePicker SelectedDate="{Binding FilterPlanDateTo}"
Margin="0 0 10 10"
hc:InfoElement.Title="密炼日期止"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="80"
hc:InfoElement.Placeholder="结束日期"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:TextBox Text="{Binding FilterMachineName, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="机台名称"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="80"
hc:InfoElement.Placeholder="机台名称"
hc:InfoElement.ShowClearButton="True"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:ComboBox SelectedValuePath="Value"
DisplayMemberPath="Key"
ItemsSource="{Binding ShiftOptions}"
SelectedValue="{Binding FilterShiftFlag}"
Margin="0 0 10 10"
hc:InfoElement.Title="班次"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="80"
hc:InfoElement.Placeholder="全部"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:TextBox Text="{Binding FilterPlanNo, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="计划号"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="80"
hc:InfoElement.Placeholder="计划号"
hc:InfoElement.ShowClearButton="True"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:TextBox Text="{Binding FilterMaterialName, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="胶料名称"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="80"
hc:InfoElement.Placeholder="胶料名称"
hc:InfoElement.ShowClearButton="True"/>
</hc:Col>
</hc:Row>
</Border>
<Border Grid.Row="1" Margin="0,10">
<hc:UniformSpacingPanel Spacing="10">
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="Search"/>
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="Refresh"/>
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
<TextBlock Text="数据来自 MES 密炼生产计划维护,桌面端只读;断网时显示本地缓存,联网后自动刷新"
VerticalAlignment="Center"
Foreground="{DynamicResource SecondaryTextBrush}"
FontSize="12"/>
</hc:UniformSpacingPanel>
</Border>
<DataGrid Grid.Row="2"
ItemsSource="{Binding Items}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="False"
SelectionMode="Single"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#FFEDEDED"
VerticalGridLinesBrush="Transparent"
HeadersVisibility="All"
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}"
Style="{StaticResource CusDataGridStyle}"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridTextColumn Header="密炼日期" Binding="{Binding PlanDateText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="110"/>
<DataGridTextColumn Header="机台名称" Binding="{Binding MachineName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
<DataGridTextColumn Header="班次" Binding="{Binding ShiftFlagText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="80"/>
<DataGridTextColumn Header="计划号" Binding="{Binding PlanNo}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
<DataGridTextColumn Header="计划数量" Binding="{Binding PlanCount}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="胶料名称" Binding="{Binding MaterialName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<TextBlock Text="{Binding Total, StringFormat=共 {0} 条}" VerticalAlignment="Center" Margin="0,0,16,0"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<Button Content="上一页" Command="{Binding PrevPageCommand}" Style="{StaticResource ButtonDefault}" Margin="0,0,4,0" Width="80"/>
<TextBlock Text="{Binding PageNo, StringFormat=第 {0} 页}" VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<Button Content="下一页" Command="{Binding NextPageCommand}" Style="{StaticResource ButtonDefault}" Width="80"/>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace YY.Admin.Views.MixingProductionPlan;
public partial class MixingProductionPlanListView : UserControl
{
public MixingProductionPlanListView()
{
InitializeComponent();
}
}