diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java index 1c57fb0..b10921b 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java @@ -201,6 +201,8 @@ public class ShiroConfig { filterChainDefinitionMap.put("/xslmes/mesXslCustomer/anon/**", "anon"); // MES供应商管理免密接口(供桌面端调用) filterChainDefinitionMap.put("/xslmes/mesXslSupplier/anon/**", "anon"); + // MES磅单管理免密接口(供桌面端调用) + filterChainDefinitionMap.put("/xslmes/mesXslWeightRecord/anon/**", "anon"); // 桌面端用户反同步批量上报(Outbox -> /sys/sync/batch) filterChainDefinitionMap.put("/sys/sync/batch", "anon"); diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java index 1fc3a9a..0a3deeb 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java @@ -16,9 +16,11 @@ import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus; import org.jeecg.modules.xslmes.entity.MesXslCustomer; import org.jeecg.modules.xslmes.entity.MesXslSupplier; import org.jeecg.modules.xslmes.entity.MesXslVehicle; +import org.jeecg.modules.xslmes.entity.MesXslWeightRecord; import org.jeecg.modules.xslmes.service.IMesXslCustomerService; import org.jeecg.modules.xslmes.service.IMesXslSupplierService; import org.jeecg.modules.xslmes.service.IMesXslVehicleService; +import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService; import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; import org.springframework.web.bind.annotation.*; @@ -42,6 +44,7 @@ public class MesXslDesktopAnonController { private final IMesXslVehicleService vehicleService; private final IMesXslCustomerService customerService; private final IMesXslSupplierService supplierService; + private final IMesXslWeightRecordService weightRecordService; private final MesXslStompNotifyService stompNotify; // ═══════════════════════════ 车辆管理 ═══════════════════════════ @@ -342,8 +345,97 @@ public class MesXslDesktopAnonController { return ok ? Result.OK("操作成功") : Result.error("操作失败"); } + // ═══════════════════════════ 磅单管理 ═══════════════════════════ + + @Operation(summary = "磅单-免密分页列表查询") + @GetMapping("/xslmes/mesXslWeightRecord/anon/list") + public Result> weightRecordAnonList( + MesXslWeightRecord mesXslWeightRecord, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper qw = QueryGenerator.initQueryWrapper(mesXslWeightRecord, req.getParameterMap()); + qw.orderByDesc("create_time"); + IPage page = weightRecordService.page(new Page<>(pageNo, pageSize), qw); + return Result.OK(page); + } + + @Operation(summary = "磅单-免密通过id查询") + @GetMapping("/xslmes/mesXslWeightRecord/anon/queryById") + public Result weightRecordAnonQueryById(@RequestParam(name = "id") String id) { + MesXslWeightRecord entity = weightRecordService.getById(id); + return entity != null ? Result.OK(entity) : Result.error("未找到对应数据"); + } + + @Operation(summary = "磅单-免密添加") + @PostMapping("/xslmes/mesXslWeightRecord/anon/add") + public Result weightRecordAnonAdd(@RequestBody MesXslWeightRecord mesXslWeightRecord) { + if (oConvertUtils.isEmpty(mesXslWeightRecord.getPlateNumber())) { + return Result.error("车牌号不能为空"); + } + // 净重自动计算 + if (mesXslWeightRecord.getGrossWeight() != null && mesXslWeightRecord.getTareWeight() != null) { + mesXslWeightRecord.setNetWeight( + mesXslWeightRecord.getGrossWeight().subtract(mesXslWeightRecord.getTareWeight())); + } + applyWeightBillType(mesXslWeightRecord); + weightRecordService.save(mesXslWeightRecord); + stompNotify.publishWeightRecordChanged("add", mesXslWeightRecord.getId()); + return Result.OK("添加成功!"); + } + + @Operation(summary = "磅单-免密编辑") + @RequestMapping(value = "/xslmes/mesXslWeightRecord/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result weightRecordAnonEdit(@RequestBody MesXslWeightRecord mesXslWeightRecord) { + if (oConvertUtils.isEmpty(mesXslWeightRecord.getId())) { + return Result.error("主键不能为空"); + } + // 净重自动计算 + if (mesXslWeightRecord.getGrossWeight() != null && mesXslWeightRecord.getTareWeight() != null) { + mesXslWeightRecord.setNetWeight( + mesXslWeightRecord.getGrossWeight().subtract(mesXslWeightRecord.getTareWeight())); + } + applyWeightBillType(mesXslWeightRecord); + boolean ok = weightRecordService.updateById(mesXslWeightRecord); + if (!ok) { + return Result.error("数据已被他人修改,请刷新后重试"); + } + stompNotify.publishWeightRecordChanged("edit", mesXslWeightRecord.getId()); + return Result.OK("编辑成功!"); + } + + @Operation(summary = "磅单-免密删除") + @DeleteMapping("/xslmes/mesXslWeightRecord/anon/delete") + public Result weightRecordAnonDelete(@RequestParam(name = "id") String id) { + weightRecordService.removeById(id); + stompNotify.publishWeightRecordChanged("delete", id); + return Result.OK("删除成功!"); + } + + @Operation(summary = "磅单-免密批量删除") + @DeleteMapping("/xslmes/mesXslWeightRecord/anon/deleteBatch") + public Result weightRecordAnonDeleteBatch(@RequestParam(name = "ids") String ids) { + weightRecordService.removeByIds(Arrays.asList(ids.split(","))); + stompNotify.publishWeightRecordChanged("batchDelete", ids); + return Result.OK("批量删除成功!"); + } + // ─────────────────────────── 车辆私有辅助 ──────────────────────────── + private void applyWeightBillType(MesXslWeightRecord record) { + if (record.getGrossWeight() != null && record.getTareWeight() != null) { + record.setBillType("2"); + return; + } + if (record.getGrossWeight() != null) { + record.setBillType("1"); + return; + } + if (record.getTareWeight() != null) { + record.setBillType("3"); + } + } + private void applyVehicleBelong(MesXslVehicle v) { if (oConvertUtils.isEmpty(v.getVehicleBelong())) { return; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java index 91c639c..3dc88b4 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java @@ -15,6 +15,7 @@ import org.jeecg.common.system.base.controller.JeecgController; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.modules.xslmes.entity.MesXslWeightRecord; import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService; +import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; @@ -36,6 +37,8 @@ public class MesXslWeightRecordController extends JeecgController edit(@RequestBody MesXslWeightRecord mesXslWeightRecord) { + computeBillType(mesXslWeightRecord); computeNetWeight(mesXslWeightRecord); mesXslWeightRecordService.updateById(mesXslWeightRecord); + stompNotifyService.publishWeightRecordChanged("edit", mesXslWeightRecord.getId()); return Result.OK("编辑成功!"); } @@ -82,6 +89,7 @@ public class MesXslWeightRecordController extends JeecgController delete(@RequestParam(name = "id", required = true) String id) { mesXslWeightRecordService.removeById(id); + stompNotifyService.publishWeightRecordChanged("delete", id); return Result.OK("删除成功!"); } @@ -91,6 +99,7 @@ public class MesXslWeightRecordController extends JeecgController deleteBatch(@RequestParam(name = "ids", required = true) String ids) { this.mesXslWeightRecordService.removeByIds(Arrays.asList(ids.split(","))); + stompNotifyService.publishWeightRecordChanged("deleteBatch", null); return Result.OK("批量删除成功!"); } @@ -124,4 +133,18 @@ public class MesXslWeightRecordController extends JeecgController= 0 ? net : BigDecimal.ZERO); } } + + private void computeBillType(MesXslWeightRecord record) { + if (record.getGrossWeight() != null && record.getTareWeight() != null) { + record.setBillType("2"); + return; + } + if (record.getGrossWeight() != null) { + record.setBillType("1"); + return; + } + if (record.getTareWeight() != null) { + record.setBillType("3"); + } + } } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java index a6d1474..da6bb49 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java @@ -81,6 +81,11 @@ public class MesXslWeightRecord extends JeecgEntity implements Serializable { @Schema(description = "手机号") private String driverPhone; + @Excel(name = "单据类型", width = 12, dicCode = "xslmes_weight_bill_type") + @Dict(dicCode = "xslmes_weight_bill_type") + @Schema(description = "单据类型:1已称毛重 2称重完成") + private String billType; + @Schema(description = "租户ID") private Integer tenantId; } 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 4e6c010..780e595 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 @@ -35,6 +35,11 @@ public class MesXslStompNotifyService { publish("/topic/sync/mes-suppliers", "MES_SUPPLIER_CHANGED", "supplierId", supplierId, action); } + /** 广播磅单数据变更事件到 /topic/sync/mes-weight-records */ + public void publishWeightRecordChanged(String action, String weightRecordId) { + publish("/topic/sync/mes-weight-records", "MES_WEIGHT_RECORD_CHANGED", "weightRecordId", weightRecordId, action); + } + // ─────────────────────────── 私有辅助 ──────────────────────────── private void publish(String topic, String cmd, String idKey, String idValue, String action) { diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_33__mes_xsl_weight_record_bill_type.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_33__mes_xsl_weight_record_bill_type.sql new file mode 100644 index 0000000..1782f5d --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_33__mes_xsl_weight_record_bill_type.sql @@ -0,0 +1,63 @@ +-- 磅单新增单据类型字段 + 字典(幂等) + +-- 1) 表结构:单据类型(1已称毛重 2称重完成 3已称皮重) +SET @bill_type_exists := ( + SELECT COUNT(1) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mes_xsl_weight_record' + AND COLUMN_NAME = 'bill_type' +); +SET @ddl_sql := IF( + @bill_type_exists = 0, + 'ALTER TABLE `mes_xsl_weight_record` ADD COLUMN `bill_type` varchar(10) DEFAULT NULL COMMENT ''单据类型(字典xslmes_weight_bill_type:1已称毛重 2称重完成 3已称皮重)'' AFTER `driver_phone`', + 'SELECT 1' +); +PREPARE stmt_bill_type FROM @ddl_sql; +EXECUTE stmt_bill_type; +DEALLOCATE PREPARE stmt_bill_type; + +-- 2) 初始化历史数据(按当前重量信息推导) +UPDATE `mes_xsl_weight_record` +SET `bill_type` = CASE + WHEN `gross_weight` IS NOT NULL AND `tare_weight` IS NOT NULL THEN '2' + WHEN `gross_weight` IS NOT NULL THEN '1' + WHEN `tare_weight` IS NOT NULL THEN '3' + ELSE `bill_type` +END +WHERE `bill_type` IS NULL OR `bill_type` = ''; + +-- 3) 字典主表 +INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`) +SELECT REPLACE(UUID(), '-', ''), 'MES磅单单据类型', 'xslmes_weight_bill_type', '磅单当前状态:已称毛重/称重完成/已称皮重', 0, 'admin', NOW(), 0, 0 +WHERE NOT EXISTS (SELECT 1 FROM `sys_dict` WHERE `dict_code` = 'xslmes_weight_bill_type' AND `del_flag` = 0); + +-- 4) 字典项:已称毛重 +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`) +SELECT REPLACE(UUID(), '-', ''), d.id, '已称毛重', '1', 1, 1, 'admin', NOW() +FROM `sys_dict` d +WHERE d.`dict_code` = 'xslmes_weight_bill_type' + AND NOT EXISTS ( + SELECT 1 FROM `sys_dict_item` i + WHERE i.`dict_id` = d.`id` AND i.`item_value` = '1' + ); + +-- 5) 字典项:称重完成 +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`) +SELECT REPLACE(UUID(), '-', ''), d.id, '称重完成', '2', 2, 1, 'admin', NOW() +FROM `sys_dict` d +WHERE d.`dict_code` = 'xslmes_weight_bill_type' + AND NOT EXISTS ( + SELECT 1 FROM `sys_dict_item` i + WHERE i.`dict_id` = d.`id` AND i.`item_value` = '2' + ); + +-- 6) 字典项:已称皮重 +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `sort_order`, `status`, `create_by`, `create_time`) +SELECT REPLACE(UUID(), '-', ''), d.id, '已称皮重', '3', 3, 1, 'admin', NOW() +FROM `sys_dict` d +WHERE d.`dict_code` = 'xslmes_weight_bill_type' + AND NOT EXISTS ( + SELECT 1 FROM `sys_dict_item` i + WHERE i.`dict_id` = d.`id` AND i.`item_value` = '3' + ); diff --git a/yy-admin-master/YY.Admin.Core/Core/Events/MesXslWeightRecordChangedEvent.cs b/yy-admin-master/YY.Admin.Core/Core/Events/MesXslWeightRecordChangedEvent.cs new file mode 100644 index 0000000..453fa89 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Events/MesXslWeightRecordChangedEvent.cs @@ -0,0 +1,11 @@ +using Prism.Events; + +namespace YY.Admin.Core.Events; + +public class MesXslWeightRecordChangedPayload +{ + public string Action { get; set; } = string.Empty; // add/edit/delete/reconnect/pull + public string? WeightRecordId { get; set; } +} + +public class MesXslWeightRecordChangedEvent : PubSubEvent { } diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/IWeightRecordService.cs b/yy-admin-master/YY.Admin.Core/Core/Services/IWeightRecordService.cs new file mode 100644 index 0000000..3015a36 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Services/IWeightRecordService.cs @@ -0,0 +1,23 @@ +using YY.Admin.Core.Entity; + +namespace YY.Admin.Core.Services; + +public interface IWeightRecordService +{ + Task PageAsync( + int pageNo, int pageSize, + string? filterBillNo = null, + string? filterPlateNumber = null, + string? filterInoutDirection = null, + string? filterGoodsName = null, + string? filterDriverName = null, + CancellationToken ct = default); + + Task GetByIdAsync(string id, CancellationToken ct = default); + Task AddAsync(MesXslWeightRecord entity, CancellationToken ct = default); + Task EditAsync(MesXslWeightRecord entity, CancellationToken ct = default); + Task DeleteAsync(string id, CancellationToken ct = default); + Task DeleteBatchAsync(string ids, CancellationToken ct = default); +} + +public record WeightRecordPageResult(List Records, long Total, int PageNo, int PageSize); diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs new file mode 100644 index 0000000..fc49b25 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs @@ -0,0 +1,61 @@ +namespace YY.Admin.Core.Entity; + +public class MesXslWeightRecord +{ + public string? Id { get; set; } + + /// 磅单号 + public string? BillNo { get; set; } + + /// 称重日期 + public DateTime? WeighDate { get; set; } + + /// 进出方向:1进厂 2出厂 + public string? InoutDirection { get; set; } + + /// 车辆档案ID(可选,由车牌反查) + public string? VehicleId { get; set; } + + /// 车牌号 + public string? PlateNumber { get; set; } + + /// 发货单位(进厂时为供应商名称) + public string? SenderUnit { get; set; } + + /// 收货单位(出厂时为客户简称) + public string? ReceiverUnit { get; set; } + + /// 货物名称 + public string? GoodsName { get; set; } + + /// 毛重(KG),实际称量 + public double? GrossWeight { get; set; } + + /// 皮重(KG),从车辆档案带出或单独采集 + public double? TareWeight { get; set; } + + /// 净重(KG)=毛重-皮重,自动计算 + public double? NetWeight { get; set; } + + /// 司机姓名 + public string? DriverName { get; set; } + + /// 司机手机号 + public string? DriverPhone { get; set; } + + /// 单据类型:1已称毛重 2称重完成 + public string? BillType { get; set; } + + public int? TenantId { get; set; } + public string? CreateBy { get; set; } + public DateTime? CreateTime { get; set; } + public string? UpdateBy { get; set; } + public DateTime? UpdateTime { get; set; } + public string? SysOrgCode { get; set; } + + /// 进出方向显示文本(由 ViewModel 填充) + public string InoutDirectionText { get; set; } = string.Empty; + + /// 单据类型显示文本(由 ViewModel 填充) + public string BillTypeText { get; set; } = string.Empty; +} diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs index ac6b358..c40756a 100644 --- a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs @@ -32,6 +32,10 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData new SysMenu{ Id=1300150010201, Pid=1300150000101, Title="客户管理", Path="/xslmes/mesXslCustomer", Name="mesXslCustomer", Component="CustomerListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=101 }, // 供应商管理 new SysMenu{ Id=1300150010301, Pid=1300150000101, Title="供应商管理", Path="/xslmes/mesXslSupplier", Name="mesXslSupplier", Component="SupplierListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=102 }, + // 磅单记录管理(标准CRUD) + new SysMenu{ Id=1300150010401, Pid=1300150000101, Title="磅单记录管理", Path="/xslmes/mesXslWeightRecord", Name="mesXslWeightRecord", Component="WeightRecordListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=103 }, + // 地磅称重操作(操作台大页面) + new SysMenu{ Id=1300150010501, Pid=1300150000101, Title="地磅称重操作", Path="/xslmes/weightRecordOperation", Name="weightRecordOperation", Component="WeightRecordOperationView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=104 }, #endregion diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs index d33b400..ab59737 100644 --- a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs @@ -24,6 +24,8 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150000101}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010101}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010201}, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010401}, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010501}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010701 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300100601 }, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200090401 }, diff --git a/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs b/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs new file mode 100644 index 0000000..37c20eb --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs @@ -0,0 +1,710 @@ +using Microsoft.Extensions.Configuration; +using System.Net.Http; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Web; +using Prism.Events; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Events; +using YY.Admin.Core.Services; + +namespace YY.Admin.Services.Service.WeightRecord; + +public class WeightRecordService : IWeightRecordService, 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 _pendingOpsFilePath; + private readonly string _cacheFilePath; + private List _pendingOps = new(); + private List _localCache = new(); + + private static readonly JsonSerializerOptions _jsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new NullableDateTimeJsonConverter() } + }; + + public WeightRecordService( + 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); + _pendingOpsFilePath = Path.Combine(appDataDir, "weight-record-pending-ops.json"); + _cacheFilePath = Path.Combine(appDataDir, "weight-record-cache.json"); + + LoadPendingOpsFromDisk(); + LoadCacheFromDisk(); + _logger.Information($"[磅单同步] 服务初始化完成,缓存={_localCache.Count},待上传={_pendingOps.Count},在线={_networkMonitor.IsOnline}"); + + _networkMonitor.StatusChanged += OnNetworkStatusChanged; + if (_networkMonitor.IsOnline) + _ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None)); + } + + private const int MaxPendingRetries = 5; + 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, + string? filterBillNo = null, + string? filterPlateNumber = null, + string? filterInoutDirection = null, + string? filterGoodsName = null, + string? filterDriverName = null, + CancellationToken ct = default) + { + List? source = null; + if (_networkMonitor.IsOnline) + { + try + { + source = await FetchRemoteListAsync(ct).ConfigureAwait(false); + lock (_cacheLock) + { + _localCache = source.Select(Clone).ToList(); + SaveCacheToDiskUnsafe(); + } + _logger.Information($"[磅单列表] 远端拉取成功 count={source.Count}"); + } + catch (Exception ex) + { + source = null; + _logger.Warning($"[磅单列表] 远端拉取失败,回退本地缓存:{ex.Message}"); + } + } + + lock (_cacheLock) + { + source ??= _localCache.Select(Clone).ToList(); + source = ApplyPendingOpsSnapshotUnsafe(source); + } + + var filtered = ApplyFilters(source, filterBillNo, filterPlateNumber, filterInoutDirection, filterGoodsName, filterDriverName); + var total = filtered.Count; + var records = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList(); + return new WeightRecordPageResult(records, total, pageNo, pageSize); + } + + public async Task GetByIdAsync(string id, CancellationToken ct = default) + { + if (_networkMonitor.IsOnline) + { + try + { + var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + if (!resp.IsSuccessStatusCode) return null; + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("result", out var resultEl)) return null; + return resultEl.Deserialize(_jsonOpts); + } + catch (Exception ex) + { + _logger.Warning($"[磅单详情] 远端查询异常 id={id}: {ex.Message}"); + } + } + + lock (_cacheLock) + { + return _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found + ? Clone(found) : null; + } + } + + public async Task AddAsync(MesXslWeightRecord entity, CancellationToken ct = default) + { + if (!entity.TenantId.HasValue || entity.TenantId.Value <= 0) + entity.TenantId = DefaultTenantId; + if (string.IsNullOrWhiteSpace(entity.BillNo)) + entity.BillNo = GenerateBillNo(); + if (string.IsNullOrWhiteSpace(entity.BillType)) + entity.BillType = ResolveBillType(entity); + + var local = Clone(entity); + if (string.IsNullOrWhiteSpace(local.Id)) + local.Id = $"local-{Guid.NewGuid():N}"; + + if (_networkMonitor.IsOnline) + { + try + { + _logger.Information($"[磅单新增] 尝试远端新增 id={local.Id}"); + var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false); + if (ok) { UpsertLocalCache(local); return true; } + _logger.Warning($"[磅单新增] 远端返回失败 id={local.Id}"); + return false; + } + catch (Exception ex) + { + _logger.Warning($"[磅单新增] 远端异常,转离线入队:{ex.Message}"); + } + } + + EnqueuePendingOperation(new WeightRecordPendingOperation + { + OpType = WeightRecordOperationType.Add, + WeightRecordId = local.Id, + Entity = local + }); + UpsertLocalCache(local); + return true; + } + + /// + /// 按后端规则生成磅单号:BDH-yyyyMMddHHmmss + 3位随机数 + /// + private static string GenerateBillNo() + { + var dateStr = DateTime.Now.ToString("yyyyMMddHHmmss"); + var seq = Random.Shared.Next(0, 1000).ToString("D3"); + return $"BDH-{dateStr}{seq}"; + } + + /// + /// 根据称重数据推导单据类型:仅毛重=已称毛重;仅皮重=已称皮重;毛重+皮重=称重完成。 + /// + private static string? ResolveBillType(MesXslWeightRecord entity) + { + if (entity.GrossWeight.HasValue && entity.TareWeight.HasValue) return "2"; + if (entity.GrossWeight.HasValue) return "1"; + if (entity.TareWeight.HasValue) return "3"; + return null; + } + + public async Task EditAsync(MesXslWeightRecord entity, CancellationToken ct = default) + { + if (!entity.TenantId.HasValue || entity.TenantId.Value <= 0) + entity.TenantId = DefaultTenantId; + if (string.IsNullOrWhiteSpace(entity.BillType)) + entity.BillType = ResolveBillType(entity); + + var local = Clone(entity); + if (IsLocalTempId(local.Id)) + { + // 本地临时ID表示该记录尚未上云:将“二次称重编辑”合并到同一条待上传新增,避免重连后产生冲突。 + if (TryMergeIntoPendingAdd(local)) + { + UpsertLocalCache(local); + return true; + } + + // 兜底:若找不到待上传新增,则按新增入队,确保本地修改不丢失。 + EnqueuePendingOperation(new WeightRecordPendingOperation + { + OpType = WeightRecordOperationType.Add, + WeightRecordId = local.Id, + Entity = local + }); + UpsertLocalCache(local); + return true; + } + + if (_networkMonitor.IsOnline) + { + try + { + _logger.Information($"[磅单修改] 尝试远端修改 id={local.Id}"); + var (ok, _) = await RemoteEditAsync(local, ct).ConfigureAwait(false); + if (ok) { UpsertLocalCache(local); return true; } + _logger.Warning($"[磅单修改] 远端返回失败 id={local.Id}"); + return false; + } + catch (Exception ex) + { + _logger.Warning($"[磅单修改] 远端异常,转离线入队:{ex.Message}"); + } + } + + EnqueuePendingOperation(new WeightRecordPendingOperation + { + OpType = WeightRecordOperationType.Edit, + WeightRecordId = local.Id, + Entity = local, + AnchorUpdateTime = local.UpdateTime + }); + UpsertLocalCache(local); + return true; + } + + private bool TryMergeIntoPendingAdd(MesXslWeightRecord local) + { + if (string.IsNullOrWhiteSpace(local.Id)) return false; + lock (_cacheLock) + { + var pendingAdd = _pendingOps + .Where(x => x.OpType == WeightRecordOperationType.Add) + .OrderByDescending(x => x.CreatedAt) + .FirstOrDefault(x => + string.Equals(x.WeightRecordId, local.Id, StringComparison.OrdinalIgnoreCase) || + string.Equals(x.Entity?.Id, local.Id, StringComparison.OrdinalIgnoreCase)); + if (pendingAdd == null) return false; + + pendingAdd.Entity = Clone(local); + pendingAdd.WeightRecordId = local.Id; + SavePendingOpsToDiskUnsafe(); + return true; + } + } + + public async Task DeleteAsync(string id, CancellationToken ct = default) + { + if (_networkMonitor.IsOnline) + { + try + { + var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false); + if (ok) { RemoveFromLocalCache(id); return true; } + return false; + } + catch (Exception ex) + { + _logger.Warning($"[磅单删除] 远端异常,转离线入队:{ex.Message}"); + } + } + + DateTime? anchor; + lock (_cacheLock) + anchor = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime; + + EnqueuePendingOperation(new WeightRecordPendingOperation + { + OpType = WeightRecordOperationType.Delete, + WeightRecordId = id, + AnchorUpdateTime = anchor + }); + RemoveFromLocalCache(id); + return true; + } + + public async Task DeleteBatchAsync(string ids, CancellationToken ct = default) + { + var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var allSuccess = true; + foreach (var id in idList) + allSuccess &= await DeleteAsync(id, ct).ConfigureAwait(false); + return allSuccess; + } + + // ─────────────────── 远端 HTTP ─────────────────── + + private async Task> FetchRemoteListAsync(CancellationToken ct) + { + var query = HttpUtility.ParseQueryString(string.Empty); + query["pageNo"] = "1"; + query["pageSize"] = "10000"; + query["tenantId"] = DefaultTenantId.ToString(); + var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/list?{query}"; + using var client = CreateClient(); + _logger.Information($"[磅单远端] GET {url}"); + 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); + var result = doc.RootElement.GetProperty("result"); + return result.GetProperty("records").Deserialize>(_jsonOpts) ?? new(); + } + + private async Task RemoteAddAsync(MesXslWeightRecord entity, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/add?tenantId={DefaultTenantId}"; + var payload = Clone(entity); + if (IsLocalTempId(payload.Id)) payload.Id = null; + return await PostJsonAsync(url, payload, ct).ConfigureAwait(false); + } + + private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslWeightRecord entity, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/edit?tenantId={DefaultTenantId}"; + return await PostJsonCheckVersionAsync(url, entity, ct).ConfigureAwait(false); + } + + private async Task RemoteDeleteAsync(string id, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslWeightRecord/anon/delete?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false); + return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false); + } + + private async Task PostJsonAsync(string url, object body, CancellationToken ct) + { + var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json"); + using var client = CreateClient(); + var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false); + return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false); + } + + private async Task<(bool Ok, bool IsVersionConflict)> PostJsonCheckVersionAsync(string url, object body, CancellationToken ct) + { + var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json"); + using var client = CreateClient(); + var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false); + if (!resp.IsSuccessStatusCode) return (false, false); + try + { + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + int code = 200; + if (doc.RootElement.TryGetProperty("code", out var codeEl)) code = codeEl.GetInt32(); + if (code == 200) return (true, false); + if (doc.RootElement.TryGetProperty("message", out var msgEl) && (msgEl.GetString() ?? "").Contains("已被他人修改")) + return (false, true); + return (false, false); + } + catch { return (true, false); } + } + + private static async Task IsSuccessResultAsync(HttpResponseMessage resp, CancellationToken ct) + { + try + { + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("code", out var code)) return code.GetInt32() == 200; + if (doc.RootElement.TryGetProperty("success", out var success)) return success.GetBoolean(); + return true; + } + catch { return true; } + } + + // ─────────────────── 重连同步 ─────────────────── + + private void OnNetworkStatusChanged(bool isOnline) + { + if (!isOnline) return; + _ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None)); + } + + private async Task SyncAfterReconnectAsync(CancellationToken ct) + { + _logger.Information("[磅单重连] 开始重连同步"); + var pushResult = await PushPendingOnReconnectAsync(ct).ConfigureAwait(false); + if (!_networkMonitor.IsOnline) return; + try + { + var remote = await FetchRemoteListAsync(ct).ConfigureAwait(false); + lock (_cacheLock) { _localCache = remote.Select(Clone).ToList(); SaveCacheToDiskUnsafe(); } + _eventAggregator.GetEvent().Publish(new MesXslWeightRecordChangedPayload { Action = "pull" }); + _logger.Information($"[磅单重连] 全量回拉成功 count={remote.Count}"); + } + catch (Exception ex) { _logger.Warning($"[磅单重连] 全量回拉失败:{ex.Message}"); } + + var hasActivity = pushResult.PushedCount > 0 || pushResult.ConflictCount > 0 || pushResult.NewRecordsPushed > 0; + if (hasActivity) + _eventAggregator.GetEvent().Publish(new SyncConflictPayload + { + EntityName = "磅单", + PushedCount = pushResult.PushedCount, + ConflictCount = pushResult.ConflictCount, + NewRecordsPushed = pushResult.NewRecordsPushed + }); + } + + private sealed record PendingReplayResult(bool Ok, bool IsConflict, string? EntityId); + + private async Task PushPendingOnReconnectAsync(CancellationToken ct) + { + if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false)) return new PushPendingResult(0, 0, 0); + try + { + List snapshot; + lock (_cacheLock) { snapshot = _pendingOps.OrderBy(x => x.CreatedAt).ToList(); } + _logger.Information($"[磅单推送] 开始推送 pending={snapshot.Count}"); + + int pushed = 0, conflicts = 0, newPushed = 0; + foreach (var op in snapshot) + { + if (!_networkMonitor.IsOnline) break; + lock (_cacheLock) { if (!_pendingOps.Any(x => x.Id == op.Id)) continue; } + + var result = await ExecutePendingOperationAsync(op, ct).ConfigureAwait(false); + if (!result.Ok) + { + lock (_cacheLock) + { + op.RetryCount++; + if (op.RetryCount >= MaxPendingRetries) + { + _pendingOps.RemoveAll(x => x.Id == op.Id); + SavePendingOpsToDiskUnsafe(); + continue; + } + SavePendingOpsToDiskUnsafe(); + } + break; + } + if (result.IsConflict) + { + conflicts++; + if (!string.IsNullOrWhiteSpace(result.EntityId)) RemovePendingOpsByEntityId(result.EntityId!); + continue; + } + lock (_cacheLock) + { + if (op.OpType == WeightRecordOperationType.Add) newPushed++; else pushed++; + _pendingOps.RemoveAll(x => x.Id == op.Id); + SavePendingOpsToDiskUnsafe(); + } + } + return new PushPendingResult(pushed, conflicts, newPushed); + } + finally { _syncLock.Release(); } + } + + private async Task ExecutePendingOperationAsync(WeightRecordPendingOperation op, CancellationToken ct) + { + try + { + switch (op.OpType) + { + case WeightRecordOperationType.Add: + { + var ok = op.Entity != null && await RemoteAddAsync(op.Entity, ct).ConfigureAwait(false); + return ok ? new PendingReplayResult(true, false, op.WeightRecordId) : new PendingReplayResult(false, false, null); + } + case WeightRecordOperationType.Edit: + { + if (op.Entity?.Id == null) return new PendingReplayResult(false, false, null); + var remote = await GetByIdAsync(op.Entity.Id, ct).ConfigureAwait(false); + if (remote != null && op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime) + { + UpsertLocalCache(remote); + return new PendingReplayResult(true, true, op.Entity.Id); + } + var (ok, isConflict) = await RemoteEditAsync(op.Entity, ct).ConfigureAwait(false); + if (isConflict) + { + var fresh = await GetByIdAsync(op.Entity.Id, ct).ConfigureAwait(false); + if (fresh != null) UpsertLocalCache(fresh); + return new PendingReplayResult(true, true, op.Entity.Id); + } + return ok ? new PendingReplayResult(true, false, op.Entity.Id) : new PendingReplayResult(false, false, null); + } + case WeightRecordOperationType.Delete: + { + if (string.IsNullOrWhiteSpace(op.WeightRecordId)) return new PendingReplayResult(false, false, null); + var id = op.WeightRecordId!; + var remote = await GetByIdAsync(id, ct).ConfigureAwait(false); + if (remote == null) return new PendingReplayResult(true, false, id); + if (op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime) + { + UpsertLocalCache(remote); + return new PendingReplayResult(true, true, id); + } + var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false); + return ok ? new PendingReplayResult(true, false, id) : new PendingReplayResult(false, false, null); + } + default: return new PendingReplayResult(true, false, null); + } + } + catch (Exception ex) + { + _logger.Warning($"[磅单推送] 执行异常 op={op.OpType}: {ex.Message}"); + return new PendingReplayResult(false, false, null); + } + } + + // ─────────────────── 过滤 / 缓存辅助 ─────────────────── + + private static List ApplyFilters( + List source, + string? billNo, string? plateNumber, string? inoutDirection, string? goodsName, string? driverName) + { + IEnumerable q = source; + if (!string.IsNullOrWhiteSpace(billNo)) + q = q.Where(v => (v.BillNo ?? "").Contains(billNo, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(plateNumber)) + q = q.Where(v => (v.PlateNumber ?? "").Contains(plateNumber, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(inoutDirection)) + q = q.Where(v => string.Equals(v.InoutDirection, inoutDirection, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(goodsName)) + q = q.Where(v => (v.GoodsName ?? "").Contains(goodsName, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(driverName)) + q = q.Where(v => (v.DriverName ?? "").Contains(driverName, StringComparison.OrdinalIgnoreCase)); + return q.OrderByDescending(v => v.CreateTime ?? DateTime.MinValue).ToList(); + } + + private List ApplyPendingOpsSnapshotUnsafe(List source) + { + var map = source.Where(v => !string.IsNullOrWhiteSpace(v.Id)) + .ToDictionary(v => v.Id!, Clone, StringComparer.OrdinalIgnoreCase); + + foreach (var op in _pendingOps.OrderBy(x => x.CreatedAt)) + { + switch (op.OpType) + { + case WeightRecordOperationType.Add: + case WeightRecordOperationType.Edit: + if (op.Entity?.Id != null) map[op.Entity.Id] = Clone(op.Entity); + break; + case WeightRecordOperationType.Delete: + if (!string.IsNullOrWhiteSpace(op.WeightRecordId)) map.Remove(op.WeightRecordId); + break; + } + } + return map.Values.ToList(); + } + + private void EnqueuePendingOperation(WeightRecordPendingOperation op) + { + lock (_cacheLock) { _pendingOps.Add(op); SavePendingOpsToDiskUnsafe(); } + } + + private void UpsertLocalCache(MesXslWeightRecord entity) + { + lock (_cacheLock) + { + var idx = _localCache.FindIndex(v => string.Equals(v.Id, entity.Id, StringComparison.OrdinalIgnoreCase)); + if (idx >= 0) _localCache[idx] = Clone(entity); else _localCache.Insert(0, Clone(entity)); + SaveCacheToDiskUnsafe(); + } + } + + private void RemoveFromLocalCache(string id) + { + lock (_cacheLock) { _localCache.RemoveAll(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)); SaveCacheToDiskUnsafe(); } + } + + private void RemovePendingOpsByEntityId(string id) + { + lock (_cacheLock) + { + _pendingOps.RemoveAll(x => + (!string.IsNullOrWhiteSpace(x.WeightRecordId) && string.Equals(x.WeightRecordId, id, StringComparison.OrdinalIgnoreCase)) || + (x.Entity?.Id != null && string.Equals(x.Entity.Id, id, StringComparison.OrdinalIgnoreCase))); + SavePendingOpsToDiskUnsafe(); + } + } + + private void LoadPendingOpsFromDisk() + { + try + { + if (!File.Exists(_pendingOpsFilePath)) return; + var data = JsonSerializer.Deserialize>(File.ReadAllText(_pendingOpsFilePath), _jsonOpts); + _pendingOps = data ?? new(); + } + catch { _pendingOps = new(); } + } + + private void LoadCacheFromDisk() + { + try + { + if (!File.Exists(_cacheFilePath)) return; + var data = JsonSerializer.Deserialize>(File.ReadAllText(_cacheFilePath), _jsonOpts); + _localCache = data ?? new(); + } + catch { _localCache = new(); } + } + + private void SavePendingOpsToDiskUnsafe() => + File.WriteAllText(_pendingOpsFilePath, JsonSerializer.Serialize(_pendingOps, _jsonOpts)); + + private void SaveCacheToDiskUnsafe() => + File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts)); + + private static MesXslWeightRecord Clone(MesXslWeightRecord input) => new() + { + Id = input.Id, + BillNo = input.BillNo, + WeighDate = input.WeighDate, + InoutDirection = input.InoutDirection, + VehicleId = input.VehicleId, + PlateNumber = input.PlateNumber, + SenderUnit = input.SenderUnit, + ReceiverUnit = input.ReceiverUnit, + GoodsName = input.GoodsName, + GrossWeight = input.GrossWeight, + TareWeight = input.TareWeight, + NetWeight = input.NetWeight, + DriverName = input.DriverName, + DriverPhone = input.DriverPhone, + BillType = input.BillType, + TenantId = input.TenantId, + CreateBy = input.CreateBy, + CreateTime = input.CreateTime, + UpdateBy = input.UpdateBy, + UpdateTime = input.UpdateTime, + SysOrgCode = input.SysOrgCode, + InoutDirectionText = input.InoutDirectionText, + BillTypeText = input.BillTypeText + }; + + private static bool IsLocalTempId(string? id) => + !string.IsNullOrWhiteSpace(id) && id.StartsWith("local-", StringComparison.OrdinalIgnoreCase); + + // ─────────────────── 内部数据结构 ─────────────────── + + private sealed class WeightRecordPendingOperation + { + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + public WeightRecordOperationType OpType { get; set; } + public string? WeightRecordId { get; set; } + public MesXslWeightRecord? Entity { get; set; } + public DateTime? AnchorUpdateTime { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public int RetryCount { get; set; } = 0; + } + + private enum WeightRecordOperationType { Add = 1, Edit = 2, Delete = 3 } + + private sealed class NullableDateTimeJsonConverter : JsonConverter + { + private static readonly string[] SupportedFormats = + [ + "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, SupportedFormats, + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.AssumeLocal, out var exact)) return exact; + if (DateTime.TryParse(raw, out var fallback)) return fallback; + } + 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")); + else writer.WriteNullValue(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordSyncCoordinator.cs b/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordSyncCoordinator.cs new file mode 100644 index 0000000..f9de1a7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordSyncCoordinator.cs @@ -0,0 +1,73 @@ +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.WeightRecord; + +/// +/// 监听 STOMP 收到的磅单变更信号,转发为桌面端 Prism 事件,触发列表刷新。 +/// +public class WeightRecordSyncCoordinator : ISingletonDependency +{ + private readonly IEventAggregator _eventAggregator; + private readonly ILoggerService _logger; + private SubscriptionToken? _remoteCommandToken; + private SubscriptionToken? _networkStatusToken; + + public WeightRecordSyncCoordinator(IEventAggregator eventAggregator, ILoggerService logger) + { + _eventAggregator = eventAggregator; + _logger = logger; + _remoteCommandToken = _eventAggregator + .GetEvent() + .Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread); + _networkStatusToken = _eventAggregator + .GetEvent() + .Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread); + _logger.Information("[磅单推送] WeightRecordSyncCoordinator 已启动"); + } + + private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload) + { + if (!payload.IsOnline) return; + _logger.Information("[磅单推送] 网络恢复,触发补偿刷新"); + _eventAggregator.GetEvent().Publish( + new MesXslWeightRecordChangedPayload { Action = "reconnect" }); + } + + 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; + + var cmd = cmdEl.GetString() ?? string.Empty; + if (!cmd.Equals("MES_WEIGHT_RECORD_CHANGED", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + doc.RootElement.TryGetProperty("action", out var actionEl); + doc.RootElement.TryGetProperty("weightRecordId", out var idEl); + + var changedPayload = new MesXslWeightRecordChangedPayload + { + Action = actionEl.GetString() ?? string.Empty, + WeightRecordId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null + }; + + _logger.Information($"[磅单推送] 收到变更信号 action={changedPayload.Action}, id={changedPayload.WeightRecordId}"); + _eventAggregator.GetEvent().Publish(changedPayload); + } + catch (Exception ex) + { + _logger.Warning($"[磅单推送] 处理 STOMP 信号失败: {ex.Message}"); + } + } +} diff --git a/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs b/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs index b6f1b7d..bdea801 100644 --- a/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs +++ b/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs @@ -134,6 +134,10 @@ public class StompWebSocketService : ISignalRService await SendFrameAsync( BuildSubscribeFrame("sub-mes-suppliers", "/topic/sync/mes-suppliers"), cancellationToken).ConfigureAwait(false); + // 磅单数据变更:订阅 /topic/sync/mes-weight-records + await SendFrameAsync( + BuildSubscribeFrame("sub-mes-weight-records", "/topic/sync/mes-weight-records"), + 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 52a676a..9e76d49 100644 --- a/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs +++ b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs @@ -11,6 +11,7 @@ using YY.Admin.Views.Customer; using YY.Admin.Views.Supplier; using YY.Admin.ViewModels.Vehicle; using YY.Admin.Views.Vehicle; +using YY.Admin.Views.WeightRecord; namespace YY.Admin { @@ -61,6 +62,10 @@ namespace YY.Admin containerRegistry.RegisterForNavigation(); // 供应商管理 containerRegistry.RegisterForNavigation(); + // 磅单记录管理(标准CRUD列表) + containerRegistry.RegisterForNavigation(); + // 地磅称重操作(大页面操作台) + containerRegistry.RegisterForNavigation(); } } public class DialogWindow : Window, IDialogWindow diff --git a/yy-admin-master/YY.Admin/Module/SyncModule.cs b/yy-admin-master/YY.Admin/Module/SyncModule.cs index dfd5642..2311dfa 100644 --- a/yy-admin-master/YY.Admin/Module/SyncModule.cs +++ b/yy-admin-master/YY.Admin/Module/SyncModule.cs @@ -16,6 +16,7 @@ using YY.Admin.Infrastructure.Sync; using YY.Admin.Services.Service.Customer; using YY.Admin.Services.Service.Supplier; using YY.Admin.Services.Service.Vehicle; +using YY.Admin.Services.Service.WeightRecord; namespace YY.Admin.Module; @@ -42,6 +43,9 @@ public class SyncModule : IModule // 供应商管理:免密 API 直连 + STOMP 实时通知 containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); + // 磅单管理:免密 API 直连 + STOMP 实时通知 + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); var serviceCollection = new ServiceCollection(); serviceCollection.AddTransient(); @@ -92,6 +96,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 ff98b5a..4ae2f29 100644 --- a/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs @@ -98,7 +98,17 @@ namespace YY.Admin.ViewModels.Control ["SupplierListView"] = "SupplierListView", ["/xslmes/mesXslSupplier"] = "SupplierListView", ["/xslmes/supplier"] = "SupplierListView", - ["mesXslSupplier"] = "SupplierListView" + ["mesXslSupplier"] = "SupplierListView", + + // 已实现页面:磅单记录管理 + ["WeightRecordListView"] = "WeightRecordListView", + ["/xslmes/mesXslWeightRecord"] = "WeightRecordListView", + ["mesXslWeightRecord"] = "WeightRecordListView", + + // 已实现页面:地磅称重操作 + ["WeightRecordOperationView"] = "WeightRecordOperationView", + ["/xslmes/weightRecordOperation"] = "WeightRecordOperationView", + ["weightRecordOperation"] = "WeightRecordOperationView" }; private MenuItem? _selectedMenuItem; diff --git a/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs index 51b33d3..14ae828 100644 --- a/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs @@ -382,8 +382,9 @@ namespace YY.Admin.ViewModels } else { - var exMsg = result.Exception?.Message; - _logger.Error($"导航失败: {viewName}, Region={regionName}, Exception={exMsg}"); + var exMsg = result.Exception?.ToString() ?? "null"; + var innerExMsg = result.Exception?.InnerException?.ToString() ?? "null"; + _logger.Error($"导航失败: {viewName}, Region={regionName}, Exception={exMsg}, InnerException={innerExMsg}"); tcs.SetResult(false); } }, parameters); diff --git a/yy-admin-master/YY.Admin/ViewModels/WeightRecord/CustomerPickerDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/CustomerPickerDialogViewModel.cs new file mode 100644 index 0000000..95e12d0 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/CustomerPickerDialogViewModel.cs @@ -0,0 +1,84 @@ +using HandyControl.Tools.Extension; +using System.Collections.ObjectModel; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Services; + +namespace YY.Admin.ViewModels.WeightRecord; + +public class CustomerPickerDialogViewModel : BaseViewModel, IDialogResultable +{ + private readonly ICustomerService _customerService; + + private string? _searchText; + public string? SearchText { get => _searchText; set => SetProperty(ref _searchText, value); } + + public ObservableCollection Customers { get; } = new(); + + private MesXslCustomer? _selectedCustomer; + public MesXslCustomer? SelectedCustomer + { + get => _selectedCustomer; + set + { + SetProperty(ref _selectedCustomer, value); + ConfirmCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged(nameof(SelectedCustomerDisplay)); + RaisePropertyChanged(nameof(HasSelectedCustomer)); + } + } + + public string SelectedCustomerDisplay => _selectedCustomer != null + ? $"[{_selectedCustomer.CustomerCode}] {_selectedCustomer.CustomerName}" + : "选中客户后点击「确认选择」"; + + public bool HasSelectedCustomer => _selectedCustomer != null; + + private bool _result; + public bool Result { get => _result; set => SetProperty(ref _result, value); } + public Action? CloseAction { get; set; } + + public DelegateCommand SearchCommand { get; } + public DelegateCommand ConfirmCommand { get; } + public DelegateCommand CancelCommand { get; } + + public CustomerPickerDialogViewModel( + ICustomerService customerService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _customerService = customerService; + SearchCommand = new DelegateCommand(async () => await LoadAsync()); + ConfirmCommand = new DelegateCommand(Confirm, () => SelectedCustomer != null); + CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); + _ = LoadAsync(); + } + + private async Task LoadAsync() + { + try + { + IsLoading = true; + var keyword = SearchText?.Trim(); + var result = await _customerService.PageAsync(1, 200, customerName: keyword); + Customers.Clear(); + foreach (var c in result.Records) + Customers.Add(c); + } + catch + { + Customers.Clear(); + } + finally + { + IsLoading = false; + } + } + + private void Confirm() + { + if (SelectedCustomer == null) return; + Result = true; + CloseAction?.Invoke(); + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/WeightRecord/SupplierPickerDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/SupplierPickerDialogViewModel.cs new file mode 100644 index 0000000..06a58ba --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/SupplierPickerDialogViewModel.cs @@ -0,0 +1,84 @@ +using HandyControl.Tools.Extension; +using System.Collections.ObjectModel; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Services; + +namespace YY.Admin.ViewModels.WeightRecord; + +public class SupplierPickerDialogViewModel : BaseViewModel, IDialogResultable +{ + private readonly ISupplierService _supplierService; + + private string? _searchText; + public string? SearchText { get => _searchText; set => SetProperty(ref _searchText, value); } + + public ObservableCollection Suppliers { get; } = new(); + + private MesXslSupplier? _selectedSupplier; + public MesXslSupplier? SelectedSupplier + { + get => _selectedSupplier; + set + { + SetProperty(ref _selectedSupplier, value); + ConfirmCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged(nameof(SelectedSupplierDisplay)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + } + } + + public string SelectedSupplierDisplay => _selectedSupplier != null + ? $"[{_selectedSupplier.SupplierCode}] {_selectedSupplier.SupplierName}" + : "选中供应商后点击「确认选择」"; + + public bool HasSelectedSupplier => _selectedSupplier != null; + + private bool _result; + public bool Result { get => _result; set => SetProperty(ref _result, value); } + public Action? CloseAction { get; set; } + + public DelegateCommand SearchCommand { get; } + public DelegateCommand ConfirmCommand { get; } + public DelegateCommand CancelCommand { get; } + + public SupplierPickerDialogViewModel( + ISupplierService supplierService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _supplierService = supplierService; + SearchCommand = new DelegateCommand(async () => await LoadAsync()); + ConfirmCommand = new DelegateCommand(Confirm, () => SelectedSupplier != null); + CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); + _ = LoadAsync(); + } + + private async Task LoadAsync() + { + try + { + IsLoading = true; + var keyword = SearchText?.Trim(); + var result = await _supplierService.PageAsync(1, 200, supplierName: keyword); + Suppliers.Clear(); + foreach (var s in result.Records) + Suppliers.Add(s); + } + catch + { + Suppliers.Clear(); + } + finally + { + IsLoading = false; + } + } + + private void Confirm() + { + if (SelectedSupplier == null) return; + Result = true; + CloseAction?.Invoke(); + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordEditDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordEditDialogViewModel.cs new file mode 100644 index 0000000..26486d5 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordEditDialogViewModel.cs @@ -0,0 +1,145 @@ +using HandyControl.Controls; +using HandyControl.Tools.Extension; +using System.Collections.ObjectModel; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Services; +using YY.Admin.Services.Service; + +namespace YY.Admin.ViewModels.WeightRecord; + +public class WeightRecordEditDialogViewModel : BaseViewModel, IDialogResultable +{ + private readonly IWeightRecordService _weightRecordService; + private readonly IJeecgDictSyncService _dictSyncService; + + private MesXslWeightRecord? _record; + public MesXslWeightRecord? Record + { + get => _record; + set => SetProperty(ref _record, value); + } + + public bool IsAddMode => string.IsNullOrWhiteSpace(Record?.Id); + public string DialogTitle => IsAddMode ? "新增磅单" : "编辑磅单"; + + public ObservableCollection> InoutDirectionOptions { get; } = new(); + + private bool _result; + public bool Result { get => _result; set => SetProperty(ref _result, value); } + public Action? CloseAction { get; set; } + + public DelegateCommand SaveCommand { get; } + public DelegateCommand CancelCommand { get; } + + public WeightRecordEditDialogViewModel( + IWeightRecordService weightRecordService, + IJeecgDictSyncService dictSyncService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _weightRecordService = weightRecordService; + _dictSyncService = dictSyncService; + SaveCommand = new DelegateCommand(async () => await SaveAsync()); + CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); + _ = LoadDictOptionsAsync(); + } + + private async Task LoadDictOptionsAsync() + { + try + { + var options = await _dictSyncService.GetDictOptionsAsync("xslmes_inout_direction"); + InoutDirectionOptions.Clear(); + foreach (var item in options) InoutDirectionOptions.Add(item); + if (InoutDirectionOptions.Count == 0) AddDefaultDirectionOptions(); + } + catch { AddDefaultDirectionOptions(); } + } + + private void AddDefaultDirectionOptions() + { + InoutDirectionOptions.Clear(); + InoutDirectionOptions.Add(new KeyValuePair("进厂", "1")); + InoutDirectionOptions.Add(new KeyValuePair("出厂", "2")); + } + + public void InitializeForAdd() + { + Record = new MesXslWeightRecord + { + WeighDate = DateTime.Today, + InoutDirection = "1" + }; + RaisePropertyChanged(nameof(IsAddMode)); + RaisePropertyChanged(nameof(DialogTitle)); + } + + public void InitializeForEdit(MesXslWeightRecord record) + { + Record = new MesXslWeightRecord + { + Id = record.Id, + BillNo = record.BillNo, + WeighDate = record.WeighDate, + InoutDirection = record.InoutDirection, + VehicleId = record.VehicleId, + PlateNumber = record.PlateNumber, + SenderUnit = record.SenderUnit, + ReceiverUnit = record.ReceiverUnit, + GoodsName = record.GoodsName, + GrossWeight = record.GrossWeight, + TareWeight = record.TareWeight, + NetWeight = record.NetWeight, + DriverName = record.DriverName, + DriverPhone = record.DriverPhone, + BillType = record.BillType, + TenantId = record.TenantId, + UpdateTime = record.UpdateTime + }; + RaisePropertyChanged(nameof(IsAddMode)); + RaisePropertyChanged(nameof(DialogTitle)); + } + + private async Task SaveAsync() + { + if (Record == null) return; + + if (string.IsNullOrWhiteSpace(Record.PlateNumber)) + { + HandyControl.Controls.MessageBox.Warning("车牌号不能为空!"); + return; + } + if (string.IsNullOrWhiteSpace(Record.InoutDirection)) + { + HandyControl.Controls.MessageBox.Warning("进出方向不能为空!"); + return; + } + + // 净重自动计算 + if (Record.GrossWeight.HasValue && Record.TareWeight.HasValue) + Record.NetWeight = Math.Round(Record.GrossWeight.Value - Record.TareWeight.Value, 2); + + try + { + bool ok; + if (IsAddMode) + { + ok = await _weightRecordService.AddAsync(Record); + if (ok) HandyControl.Controls.MessageBox.Success("新增磅单成功!"); + else { HandyControl.Controls.MessageBox.Error("新增磅单失败!"); return; } + } + else + { + ok = await _weightRecordService.EditAsync(Record); + if (!ok) { HandyControl.Controls.MessageBox.Error("编辑磅单失败!"); return; } + } + Result = ok; + CloseAction?.Invoke(); + } + catch (Exception ex) + { + HandyControl.Controls.MessageBox.Error($"操作失败:{ex.Message}"); + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordListViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordListViewModel.cs new file mode 100644 index 0000000..b9f9640 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordListViewModel.cs @@ -0,0 +1,232 @@ +using HandyControl.Controls; +using HandyControl.Tools.Extension; +using Prism.Events; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Windows; +using YY.Admin.Core; +using YY.Admin.Core.Events; +using YY.Admin.Core.Helper; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Services; +using YY.Admin.Services.Service; +using YY.Admin.Views.WeightRecord; + +namespace YY.Admin.ViewModels.WeightRecord; + +public class WeightRecordListViewModel : BaseViewModel +{ + private readonly IWeightRecordService _weightRecordService; + private readonly IJeecgDictSyncService _dictSyncService; + private readonly IDialogService _dialogService; + private SubscriptionToken? _changedToken; + private SubscriptionToken? _conflictToken; + + private ObservableCollection _records = new(); + public ObservableCollection Records + { + get => _records; + set => SetProperty(ref _records, 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 string? _filterBillNo; + public string? FilterBillNo { get => _filterBillNo; set => SetProperty(ref _filterBillNo, value); } + + private string? _filterPlateNumber; + public string? FilterPlateNumber { get => _filterPlateNumber; set => SetProperty(ref _filterPlateNumber, value); } + + private string? _filterInoutDirection; + public string? FilterInoutDirection { get => _filterInoutDirection; set => SetProperty(ref _filterInoutDirection, value); } + + private string? _filterGoodsName; + public string? FilterGoodsName { get => _filterGoodsName; set => SetProperty(ref _filterGoodsName, value); } + + private string? _filterDriverName; + public string? FilterDriverName { get => _filterDriverName; set => SetProperty(ref _filterDriverName, value); } + + public ObservableCollection> InoutDirectionOptions { get; } = new(); + public ObservableCollection> BillTypeOptions { get; } = new(); + + public DelegateCommand SearchCommand { get; } + public DelegateCommand ResetCommand { get; } + public DelegateCommand AddCommand { get; } + public DelegateCommand EditCommand { get; } + public DelegateCommand DeleteCommand { get; } + public DelegateCommand PrevPageCommand { get; } + public DelegateCommand NextPageCommand { get; } + + public WeightRecordListViewModel( + IWeightRecordService weightRecordService, + IJeecgDictSyncService dictSyncService, + IContainerExtension container, + IDialogService dialogService, + IRegionManager regionManager) : base(container, regionManager) + { + _weightRecordService = weightRecordService; + _dictSyncService = dictSyncService; + _dialogService = dialogService; + + SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); }); + ResetCommand = new DelegateCommand(async () => + { + FilterBillNo = null; FilterPlateNumber = null; FilterInoutDirection = null; + FilterGoodsName = null; FilterDriverName = null; + PageNo = 1; + await LoadAsync(); + }); + AddCommand = new DelegateCommand(async () => await ShowAddDialogAsync()); + EditCommand = new DelegateCommand(async r => await ShowEditDialogAsync(r)); + DeleteCommand = new DelegateCommand(async r => await DeleteAsync(r)); + 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); + _conflictToken = _eventAggregator.GetEvent() + .Subscribe(OnSyncConflict, ThreadOption.UIThread); + + _ = InitializeAsync(); + } + + private void OnSyncConflict(SyncConflictPayload payload) + { + if (!string.Equals(payload.EntityName, "磅单", StringComparison.OrdinalIgnoreCase)) return; + var parts = new List(); + if (payload.PushedCount > 0) parts.Add($"已同步 {payload.PushedCount} 条本地改动到服务器"); + if (payload.NewRecordsPushed > 0) parts.Add($"已上传 {payload.NewRecordsPushed} 条本地新增记录"); + if (payload.ConflictCount > 0) parts.Add($"{payload.ConflictCount} 条记录与服务器版本冲突,已保留服务器版本"); + if (parts.Count == 0) return; + var message = string.Join("\n", parts); + if (payload.ConflictCount > 0) Growl.Warning(message); else Growl.Success(message); + } + + private async Task InitializeAsync() + { + try + { + await LoadDictOptionsAsync(); + await UIHelper.WaitForRenderAsync(); + await LoadAsync(); + } + catch (Exception ex) { Debug.WriteLine($"磅单列表初始化失败: {ex.Message}"); } + } + + private async Task LoadDictOptionsAsync() + { + try + { + var options = await _dictSyncService.GetDictOptionsAsync("xslmes_inout_direction", includeAll: true); + InoutDirectionOptions.Clear(); + foreach (var item in options) InoutDirectionOptions.Add(item); + if (InoutDirectionOptions.Count == 0) InoutDirectionOptions.Add(new KeyValuePair("全部", "")); + + var billTypeOptions = await _dictSyncService.GetDictOptionsAsync("xslmes_weight_bill_type", includeAll: true); + BillTypeOptions.Clear(); + foreach (var item in billTypeOptions) BillTypeOptions.Add(item); + if (BillTypeOptions.Count == 0) AddDefaultBillTypeOptions(); + } + catch + { + InoutDirectionOptions.Clear(); + InoutDirectionOptions.Add(new KeyValuePair("全部", "")); + InoutDirectionOptions.Add(new KeyValuePair("进厂", "1")); + InoutDirectionOptions.Add(new KeyValuePair("出厂", "2")); + AddDefaultBillTypeOptions(); + } + } + + private void AddDefaultBillTypeOptions() + { + BillTypeOptions.Clear(); + BillTypeOptions.Add(new KeyValuePair("全部", "")); + BillTypeOptions.Add(new KeyValuePair("已称毛重", "1")); + BillTypeOptions.Add(new KeyValuePair("称重完成", "2")); + BillTypeOptions.Add(new KeyValuePair("已称皮重", "3")); + } + + public async Task LoadAsync() + { + try + { + IsLoading = true; + var result = await _weightRecordService.PageAsync( + PageNo, PageSize, FilterBillNo, FilterPlateNumber, FilterInoutDirection, FilterGoodsName, FilterDriverName); + + // 填充字典显示文本 + var dictMap = InoutDirectionOptions.ToDictionary(x => x.Value, x => x.Key); + var billTypeMap = BillTypeOptions.ToDictionary(x => x.Value, x => x.Key); + foreach (var r in result.Records) + { + r.InoutDirectionText = dictMap.TryGetValue(r.InoutDirection ?? "", out var txt) ? txt : r.InoutDirection ?? ""; + r.BillTypeText = billTypeMap.TryGetValue(r.BillType ?? "", out var billTypeTxt) ? billTypeTxt : r.BillType ?? ""; + } + + Records = new ObservableCollection(result.Records); + Total = result.Total; + } + catch (Exception ex) { Growl.Error($"加载磅单列表失败:{ex.Message}"); } + finally { IsLoading = false; } + } + + private async Task ShowAddDialogAsync() + { + try + { + var result = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => vm.InitializeForAdd()) + .GetResultAsync(); + if (result) await LoadAsync(); + } + catch (Exception ex) { Growl.Error($"打开新增对话框失败:{ex.Message}"); } + } + + private async Task ShowEditDialogAsync(MesXslWeightRecord record) + { + if (record == null) return; + try + { + var result = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => vm.InitializeForEdit(record)) + .GetResultAsync(); + if (result) await LoadAsync(); + } + catch (Exception ex) { Growl.Error($"打开编辑对话框失败:{ex.Message}"); } + } + + private async Task DeleteAsync(MesXslWeightRecord record) + { + if (record?.Id == null) return; + var confirm = System.Windows.MessageBox.Show( + $"确定删除磅单 {record.BillNo ?? record.PlateNumber}?此操作不可恢复!", + "确认删除", MessageBoxButton.OKCancel, MessageBoxImage.Question); + if (confirm != System.Windows.MessageBoxResult.OK) return; + + var ok = await _weightRecordService.DeleteAsync(record.Id); + if (ok) { Growl.Success("删除成功!"); await LoadAsync(); } + else Growl.Error("删除失败!"); + } + + protected override void CleanUp() + { + base.CleanUp(); + if (_changedToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_changedToken); + _changedToken = null; + } + if (_conflictToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_conflictToken); + _conflictToken = null; + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordOperationViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordOperationViewModel.cs new file mode 100644 index 0000000..9578de4 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/WeightRecord/WeightRecordOperationViewModel.cs @@ -0,0 +1,877 @@ +using HandyControl.Controls; +using HandyControl.Tools.Extension; +using System.Collections.ObjectModel; +using System.Windows.Threading; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Services; +using YY.Admin.Services.Service; +using YY.Admin.Views.WeightRecord; + +namespace YY.Admin.ViewModels.WeightRecord; + +/// +/// 地磅称重操作页面 ViewModel +/// 左侧:串口模拟实时数据(重量 + 车牌);右侧:称重信息录入表单。 +/// +public class WeightRecordOperationViewModel : BaseViewModel +{ + private readonly IWeightRecordService _weightRecordService; + private readonly IJeecgDictSyncService _dictSyncService; + private readonly IVehicleService _vehicleService; + + // ─── 车辆档案匹配 ─── + private MesXslVehicle? _matchedVehicle; + private string? _lastLookedUpPlate; + + private string _vehicleLookupStatus = "None"; // None | Searching | Matched | NotFound + public string VehicleLookupStatus + { + get => _vehicleLookupStatus; + set + { + SetProperty(ref _vehicleLookupStatus, value); + RaisePropertyChanged(nameof(VehicleMatchText)); + RaisePropertyChanged(nameof(ShowVehicleMatchHint)); + } + } + + public bool ShowVehicleMatchHint => _vehicleLookupStatus != "None"; + public string VehicleMatchText => _vehicleLookupStatus switch + { + "Searching" => "正在匹配车辆档案...", + "Matched" => $"✓ 已匹配车辆档案({_matchedVehicle?.VehicleBelongText})", + "NotFound" => "未在档案库中找到,保存后将自动录入", + _ => string.Empty + }; + + // ─── 串口模拟定时器 ─── + private readonly DispatcherTimer _serialTimer; + private readonly Random _rnd = new(); + private double _baseWeight = 35000.0; // 基准重量(模拟车辆在磅上) + private int _stableCountdown = 0; // 稳定倒计时(连续稳定帧数) + private bool _vehiclePresent = false; // 是否有车辆在磅上 + private int _vehicleDetectCooldown = 0; // 车辆检测冷却帧 + private bool _isApplyingSelectedRecord; + + // ─── 实时数据属性 ─── + + private double _currentWeight; + public double CurrentWeight + { + get => _currentWeight; + set + { + SetProperty(ref _currentWeight, value); + RaisePropertyChanged(nameof(CurrentWeightDisplay)); + } + } + + public string CurrentWeightDisplay => $"{CurrentWeight:N2}"; + + private bool _isWeightStable; + public bool IsWeightStable { get => _isWeightStable; set => SetProperty(ref _isWeightStable, value); } + + private string _serialStatusText = "正在连接..."; + public string SerialStatusText { get => _serialStatusText; set => SetProperty(ref _serialStatusText, value); } + + private bool _isSerialConnected; + public bool IsSerialConnected { get => _isSerialConnected; set => SetProperty(ref _isSerialConnected, value); } + + private string _detectedPlate = string.Empty; + public string DetectedPlate { get => _detectedPlate; set => SetProperty(ref _detectedPlate, value); } + + private bool _hasPlate; + public bool HasPlate { get => _hasPlate; set => SetProperty(ref _hasPlate, value); } + + public ObservableCollection OperationLogs { get; } = new(); + + // ─── 采集状态 ─── + + private bool _grossWeightCaptured; + public bool GrossWeightCaptured { get => _grossWeightCaptured; set => SetProperty(ref _grossWeightCaptured, value); } + + private bool _tareWeightCaptured; + public bool TareWeightCaptured { get => _tareWeightCaptured; set => SetProperty(ref _tareWeightCaptured, value); } + + // ─── 表单绑定属性 ─── + + private DateTime _weighDate = DateTime.Today; + public DateTime WeighDate { get => _weighDate; set => SetProperty(ref _weighDate, value); } + + private string? _inoutDirection = "1"; + public string? InoutDirection + { + get => _inoutDirection; + set + { + if (!SetProperty(ref _inoutDirection, value)) return; + RaisePropertyChanged(nameof(CaptureGrossButtonText)); + RaisePropertyChanged(nameof(CaptureTareButtonText)); + CaptureGrossWeightCommand.RaiseCanExecuteChanged(); + CaptureTareWeightCommand.RaiseCanExecuteChanged(); + if (_isApplyingSelectedRecord) return; + if (SelectedRecentWeightRecord != null) + { + ClearFormByDirectionSwitch(); + } + _ = LoadRecentWeightRecordsAsync(PlateNumber); + } + } + public string CaptureGrossButtonText => "采集毛重"; + public string CaptureTareButtonText => "采集皮重"; + + private string? _plateNumber; + public string? PlateNumber + { + get => _plateNumber; + set + { + if (!SetProperty(ref _plateNumber, value)) return; + if (_isApplyingSelectedRecord) return; + _ = LoadRecentWeightRecordsAsync(value); + var trimmed = value?.Trim() ?? string.Empty; + if (trimmed.Length >= 6) + _ = LookupVehicleByPlateAsync(trimmed); + else + ClearVehicleMatch(); + } + } + + // ─── 发货/收货单位(含选择器状态) ─── + + private MesXslSupplier? _selectedSupplier; + private MesXslCustomer? _selectedCustomer; + + public bool HasSelectedSupplier => _selectedSupplier != null; + public bool HasSelectedCustomer => _selectedCustomer != null; + + public string SenderUnitDisplay => _selectedSupplier != null + ? $"[{_selectedSupplier.SupplierCode}] {_selectedSupplier.SupplierName}" + : (!string.IsNullOrWhiteSpace(_senderUnit) ? _senderUnit : "点击右侧「选择」从供应商列表中选取"); + + public string ReceiverUnitDisplay => _selectedCustomer != null + ? $"[{_selectedCustomer.CustomerCode}] {_selectedCustomer.CustomerName}" + : (!string.IsNullOrWhiteSpace(_receiverUnit) ? _receiverUnit : "点击右侧「选择」从客户列表中选取"); + + private string? _senderUnit; + public string? SenderUnit + { + get => _senderUnit; + set + { + SetProperty(ref _senderUnit, value); + RaisePropertyChanged(nameof(SenderUnitDisplay)); + } + } + + private string? _receiverUnit; + public string? ReceiverUnit + { + get => _receiverUnit; + set + { + SetProperty(ref _receiverUnit, value); + RaisePropertyChanged(nameof(ReceiverUnitDisplay)); + } + } + + private string? _goodsName; + public string? GoodsName { get => _goodsName; set => SetProperty(ref _goodsName, value); } + + private double? _grossWeight; + public double? GrossWeight + { + get => _grossWeight; + set { SetProperty(ref _grossWeight, value); RecalcNetWeight(); } + } + + private double? _tareWeight; + public double? TareWeight + { + get => _tareWeight; + set { SetProperty(ref _tareWeight, value); RecalcNetWeight(); } + } + + private double? _netWeight; + public double? NetWeight { get => _netWeight; set => SetProperty(ref _netWeight, value); } + + public string NetWeightDisplay => NetWeight.HasValue ? $"{NetWeight.Value:N2}" : "—"; + + private string? _driverName; + public string? DriverName { get => _driverName; set => SetProperty(ref _driverName, value); } + + private string? _driverPhone; + public string? DriverPhone { get => _driverPhone; set => SetProperty(ref _driverPhone, value); } + + // ─── 字典 ─── + public ObservableCollection> InoutDirectionOptions { get; } = new(); + public ObservableCollection RecentWeightRecords { get; } = new(); + private bool _isPlateNumberLocked; + public bool IsPlateNumberLocked + { + get => _isPlateNumberLocked; + set => SetProperty(ref _isPlateNumberLocked, value); + } + private WeightRecordSimpleItem? _selectedRecentWeightRecord; + public WeightRecordSimpleItem? SelectedRecentWeightRecord + { + get => _selectedRecentWeightRecord; + set + { + if (!SetProperty(ref _selectedRecentWeightRecord, value)) return; + IsPlateNumberLocked = value?.Source?.Id != null; + if (value == null) return; + ApplySelectedRecordToForm(value); + } + } + + // ─── 命令 ─── + public DelegateCommand CaptureGrossWeightCommand { get; } + public DelegateCommand CaptureTareWeightCommand { get; } + public DelegateCommand SaveCommand { get; } + public DelegateCommand ClearCommand { get; } + public DelegateCommand UseDetectedPlateCommand { get; } + public DelegateCommand OpenSupplierPickerCommand { get; } + public DelegateCommand OpenCustomerPickerCommand { get; } + public DelegateCommand ClearSupplierCommand { get; } + public DelegateCommand ClearCustomerCommand { get; } + + public WeightRecordOperationViewModel( + IWeightRecordService weightRecordService, + IJeecgDictSyncService dictSyncService, + IVehicleService vehicleService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _weightRecordService = weightRecordService; + _dictSyncService = dictSyncService; + _vehicleService = vehicleService; + + CaptureGrossWeightCommand = new DelegateCommand(CaptureGrossWeight, CanCaptureGrossWeight) + .ObservesProperty(() => IsWeightStable) + .ObservesProperty(() => GrossWeightCaptured) + .ObservesProperty(() => TareWeightCaptured); + CaptureTareWeightCommand = new DelegateCommand(CaptureTareWeight, CanCaptureTareWeight) + .ObservesProperty(() => IsWeightStable) + .ObservesProperty(() => GrossWeightCaptured) + .ObservesProperty(() => TareWeightCaptured); + SaveCommand = new DelegateCommand(async () => await SaveAsync()); + ClearCommand = new DelegateCommand(ClearForm); + UseDetectedPlateCommand = new DelegateCommand(() => PlateNumber = DetectedPlate, () => HasPlate) + .ObservesProperty(() => HasPlate); + OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync()); + OpenCustomerPickerCommand = new DelegateCommand(async () => await OpenCustomerPickerAsync()); + ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection); + ClearCustomerCommand = new DelegateCommand(ClearCustomerSelection); + + _ = LoadDictOptionsAsync(); + _ = LoadRecentWeightRecordsAsync(PlateNumber); + + // 启动串口模拟定时器(200ms 刷新一次,约 5Hz) + _serialTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) }; + _serialTimer.Tick += OnSerialTimerTick; + _serialTimer.Start(); + + // 模拟1秒后串口连接成功 + var connectTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + connectTimer.Tick += (s, e) => + { + ((DispatcherTimer)s!).Stop(); + IsSerialConnected = true; + SerialStatusText = "COM3 9600bps 已连接"; + AddLog("串口连接成功 COM3"); + // 3秒后模拟第一辆车到来 + var vehicleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) }; + vehicleTimer.Tick += (s2, e2) => { ((DispatcherTimer)s2!).Stop(); SimulateVehicleArrival(); }; + vehicleTimer.Start(); + }; + connectTimer.Start(); + } + + private void OnSerialTimerTick(object? sender, EventArgs e) + { + if (!IsSerialConnected) return; + + if (_vehiclePresent) + { + // 车辆在磅上:在基准重量附近波动 + var noise = (_rnd.NextDouble() - 0.5) * 80; + CurrentWeight = Math.Max(0, _baseWeight + noise); + + // 稳定检测:连续10帧波动小于30kg视为稳定 + if (Math.Abs(noise) < 30) _stableCountdown = Math.Min(_stableCountdown + 1, 10); + else _stableCountdown = Math.Max(_stableCountdown - 2, 0); + IsWeightStable = _stableCountdown >= 8; + } + else + { + // 无车辆:重量归零并稳定 + CurrentWeight = Math.Max(0, CurrentWeight - 500); + if (CurrentWeight < 50) { CurrentWeight = 0; IsWeightStable = true; } + else IsWeightStable = false; + } + + // 车辆检测冷却计数 + if (_vehicleDetectCooldown > 0) _vehicleDetectCooldown--; + } + + private void SimulateVehicleArrival() + { + _vehiclePresent = true; + _stableCountdown = 0; + _baseWeight = _rnd.Next(18000, 65000); // 18~65吨随机 + AddLog("检测到车辆入场"); + + // 模拟摄像头识别车牌(2秒后回传) + var plateTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; + plateTimer.Tick += (s, e) => + { + ((DispatcherTimer)s!).Stop(); + var plates = new[] { "粤A88888", "粤B12345", "粤C98765", "湘A66666", "川B55555" }; + DetectedPlate = plates[_rnd.Next(plates.Length)]; + HasPlate = true; + AddLog($"车牌识别:{DetectedPlate}"); + }; + plateTimer.Start(); + + // 模拟15秒后车辆离开(可手动操作) + var leaveTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; + leaveTimer.Tick += (s, e) => + { + ((DispatcherTimer)s!).Stop(); + if (_vehiclePresent) SimulateVehicleLeave(); + }; + leaveTimer.Start(); + } + + private void SimulateVehicleLeave() + { + _vehiclePresent = false; + _stableCountdown = 0; + HasPlate = false; + AddLog("车辆离场"); + + // 8秒后下一辆车 + if (_vehicleDetectCooldown <= 0) + { + _vehicleDetectCooldown = 40; // 8秒 / 200ms = 40帧 + var nextTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(8) }; + nextTimer.Tick += (s, e) => { ((DispatcherTimer)s!).Stop(); SimulateVehicleArrival(); }; + nextTimer.Start(); + } + } + + private void CaptureGrossWeight() + { + var weight = Math.Round(CurrentWeight, 2); + GrossWeight = weight; + GrossWeightCaptured = true; + AddLog($"采集毛重:{GrossWeight:N2} kg"); + } + + private void CaptureTareWeight() + { + var weight = Math.Round(CurrentWeight, 2); + TareWeight = weight; + TareWeightCaptured = true; + AddLog($"采集皮重:{TareWeight:N2} kg"); + } + + private bool CanCaptureGrossWeight() + { + return IsWeightStable && !GrossWeightCaptured; + } + + private bool CanCaptureTareWeight() + { + return IsWeightStable && !TareWeightCaptured; + } + + private void RecalcNetWeight() + { + if (GrossWeight.HasValue && TareWeight.HasValue) + { + NetWeight = Math.Round(GrossWeight.Value - TareWeight.Value, 2); + RaisePropertyChanged(nameof(NetWeightDisplay)); + } + else + { + NetWeight = null; + RaisePropertyChanged(nameof(NetWeightDisplay)); + } + } + + private async Task SaveAsync() + { + if (string.IsNullOrWhiteSpace(PlateNumber)) + { + HandyControl.Controls.MessageBox.Warning("车牌号不能为空!"); + return; + } + var selectedSource = SelectedRecentWeightRecord?.Source; + var isCompleteSelectedRecord = !string.IsNullOrWhiteSpace(selectedSource?.Id); + + var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal); + if (!isCompleteSelectedRecord) + { + if (isOutbound && !TareWeight.HasValue) + { + HandyControl.Controls.MessageBox.Warning("出厂流程请先采集皮重!"); + return; + } + if (!isOutbound && !GrossWeight.HasValue) + { + HandyControl.Controls.MessageBox.Warning("进厂流程请先采集毛重!"); + return; + } + } + else + { + var needSecondWeightCaptured = isOutbound ? GrossWeight.HasValue : TareWeight.HasValue; + if (!needSecondWeightCaptured) + { + HandyControl.Controls.MessageBox.Warning(isOutbound + ? "当前为已称皮重单据补毛重,必须先采集毛重!" + : "当前为已称毛重单据补皮重,必须先采集皮重!"); + return; + } + } + + var entity = new MesXslWeightRecord + { + Id = selectedSource?.Id, + BillNo = selectedSource?.BillNo, + WeighDate = WeighDate, + InoutDirection = InoutDirection, + PlateNumber = PlateNumber, + SenderUnit = SenderUnit, + ReceiverUnit = ReceiverUnit, + GoodsName = GoodsName, + GrossWeight = GrossWeight, + TareWeight = TareWeight, + NetWeight = NetWeight, + DriverName = DriverName, + DriverPhone = DriverPhone + }; + + try + { + var ok = isCompleteSelectedRecord + ? await _weightRecordService.EditAsync(entity) + : await _weightRecordService.AddAsync(entity); + if (ok) + { + if (isCompleteSelectedRecord) + { + Growl.Success("皮重回填成功,单据已更新为称重完成!"); + AddLog(isOutbound + ? $"更新成功:{entity.BillNo ?? PlateNumber},毛重 {GrossWeight:N2} kg" + : $"更新成功:{entity.BillNo ?? PlateNumber},皮重 {TareWeight:N2} kg"); + } + else + { + Growl.Success("磅单保存成功!"); + AddLog($"保存成功:{PlateNumber},净重 {NetWeight:N2} kg"); + } + _ = UpsertVehicleAsync(); + await LoadRecentWeightRecordsAsync(PlateNumber); + // 保存后重置重量相关字段,保留日期/方向 + GrossWeight = null; TareWeight = null; NetWeight = null; + GrossWeightCaptured = false; TareWeightCaptured = false; + SelectedRecentWeightRecord = null; + IsPlateNumberLocked = false; + PlateNumber = null; DetectedPlate = string.Empty; HasPlate = false; + SenderUnit = null; ReceiverUnit = null; GoodsName = null; + DriverName = null; DriverPhone = null; + ClearSupplierSelection(); + ClearCustomerSelection(); + ClearVehicleMatch(); + RaisePropertyChanged(nameof(NetWeightDisplay)); + } + else + { + Growl.Error("磅单保存失败!"); + } + } + catch (Exception ex) + { + Growl.Error($"保存失败:{ex.Message}"); + } + } + + private void ClearForm() + { + var confirm = System.Windows.MessageBox.Show( + "确认清空所有填写内容?", "确认清空", + System.Windows.MessageBoxButton.OKCancel, + System.Windows.MessageBoxImage.Question); + if (confirm != System.Windows.MessageBoxResult.OK) return; + + WeighDate = DateTime.Today; + InoutDirection = "1"; + PlateNumber = null; SenderUnit = null; ReceiverUnit = null; + GoodsName = null; GrossWeight = null; TareWeight = null; NetWeight = null; + DriverName = null; DriverPhone = null; + GrossWeightCaptured = false; TareWeightCaptured = false; + DetectedPlate = string.Empty; HasPlate = false; + SelectedRecentWeightRecord = null; + IsPlateNumberLocked = false; + ClearSupplierSelection(); + ClearCustomerSelection(); + ClearVehicleMatch(); + RaisePropertyChanged(nameof(NetWeightDisplay)); + AddLog("表单已清空"); + } + + private async Task OpenSupplierPickerAsync() + { + SupplierPickerDialogViewModel? pickerVm = null; + bool confirmed; + try + { + confirmed = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => { pickerVm = vm; }) + .GetResultAsync(); + } + catch { return; } + + if (!confirmed || pickerVm?.SelectedSupplier == null) return; + var supplier = pickerVm.SelectedSupplier; + _selectedSupplier = supplier; + SenderUnit = supplier.SupplierShortName ?? supplier.SupplierName; + RaisePropertyChanged(nameof(SenderUnitDisplay)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + AddLog($"已选发货单位:{supplier.SupplierName}"); + } + + private async Task OpenCustomerPickerAsync() + { + CustomerPickerDialogViewModel? pickerVm = null; + bool confirmed; + try + { + confirmed = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => { pickerVm = vm; }) + .GetResultAsync(); + } + catch { return; } + + if (!confirmed || pickerVm?.SelectedCustomer == null) return; + var customer = pickerVm.SelectedCustomer; + _selectedCustomer = customer; + ReceiverUnit = customer.CustomerShortName ?? customer.CustomerName; + RaisePropertyChanged(nameof(ReceiverUnitDisplay)); + RaisePropertyChanged(nameof(HasSelectedCustomer)); + AddLog($"已选收货单位:{customer.CustomerName}"); + } + + private void ClearSupplierSelection() + { + _selectedSupplier = null; + SenderUnit = null; + RaisePropertyChanged(nameof(SenderUnitDisplay)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + } + + private void ClearCustomerSelection() + { + _selectedCustomer = null; + ReceiverUnit = null; + RaisePropertyChanged(nameof(ReceiverUnitDisplay)); + RaisePropertyChanged(nameof(HasSelectedCustomer)); + } + + private void AddLog(string message) + { + var entry = $"{DateTime.Now:HH:mm:ss} {message}"; + OperationLogs.Insert(0, entry); + while (OperationLogs.Count > 8) OperationLogs.RemoveAt(OperationLogs.Count - 1); + } + + private async Task LoadDictOptionsAsync() + { + try + { + var options = await _dictSyncService.GetDictOptionsAsync("xslmes_inout_direction"); + InoutDirectionOptions.Clear(); + foreach (var item in options) InoutDirectionOptions.Add(item); + if (InoutDirectionOptions.Count == 0) AddDefaultDirectionOptions(); + } + catch { AddDefaultDirectionOptions(); } + } + + private void AddDefaultDirectionOptions() + { + InoutDirectionOptions.Clear(); + InoutDirectionOptions.Add(new KeyValuePair("进厂", "1")); + InoutDirectionOptions.Add(new KeyValuePair("出厂", "2")); + } + + private async Task LoadRecentWeightRecordsAsync(string? plateNumber) + { + try + { + var plate = (plateNumber ?? string.Empty).Trim(); + RecentWeightRecords.Clear(); + + if (string.IsNullOrWhiteSpace(plate)) + { + SelectedRecentWeightRecord = null; + return; + } + + // 按进出方向查询当天待补称单据:进厂=已称毛重,出厂=已称皮重。 + var page = await _weightRecordService.PageAsync(1, 100, filterPlateNumber: plate); + var today = DateTime.Today; + var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal); + var candidates = page.Records + .Where(r => + string.Equals((r.PlateNumber ?? string.Empty).Trim(), plate, StringComparison.OrdinalIgnoreCase) && + r.WeighDate.HasValue && + r.WeighDate.Value.Date == today && + (isOutbound + ? (r.TareWeight.HasValue && !r.GrossWeight.HasValue) + : (r.GrossWeight.HasValue && !r.TareWeight.HasValue))) + .OrderByDescending(r => r.CreateTime ?? DateTime.MinValue) + .ToList(); + + foreach (var record in candidates) + { + RecentWeightRecords.Add(new WeightRecordSimpleItem + { + Source = record, + BillNo = record.BillNo ?? "-", + PlateNumber = record.PlateNumber ?? "-", + FirstWeightDisplay = isOutbound + ? (record.TareWeight.HasValue ? $"{record.TareWeight.Value:N2}" : "-") + : (record.GrossWeight.HasValue ? $"{record.GrossWeight.Value:N2}" : "-") + }); + } + + SelectedRecentWeightRecord = null; + } + catch + { + // 查询失败时保持现有列表,不中断主流程 + } + } + + private void ApplySelectedRecordToForm(WeightRecordSimpleItem selected) + { + if (selected.Source == null) return; + + // 带入历史磅单时,不触发车辆重新查询 + _matchedVehicle = null; + _lastLookedUpPlate = selected.Source.PlateNumber?.Trim(); + VehicleLookupStatus = "None"; + + _isApplyingSelectedRecord = true; + try + { + // 自动带入基础信息,并按进出方向保留"待补称"的第二次称重位。 + WeighDate = selected.Source.WeighDate ?? DateTime.Today; + InoutDirection = selected.Source.InoutDirection; + PlateNumber = selected.Source.PlateNumber; + _selectedSupplier = null; + _selectedCustomer = null; + SenderUnit = selected.Source.SenderUnit; + ReceiverUnit = selected.Source.ReceiverUnit; + RaisePropertyChanged(nameof(SenderUnitDisplay)); + RaisePropertyChanged(nameof(ReceiverUnitDisplay)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + RaisePropertyChanged(nameof(HasSelectedCustomer)); + GoodsName = selected.Source.GoodsName; + DriverName = selected.Source.DriverName; + DriverPhone = selected.Source.DriverPhone; + + var isOutbound = string.Equals(InoutDirection, "2", StringComparison.Ordinal); + if (isOutbound) + { + TareWeight = selected.Source.TareWeight; + TareWeightCaptured = TareWeight.HasValue; + GrossWeight = null; + GrossWeightCaptured = false; + } + else + { + GrossWeight = selected.Source.GrossWeight; + GrossWeightCaptured = GrossWeight.HasValue; + TareWeight = null; + TareWeightCaptured = false; + } + AddLog($"已带入榜单:{selected.BillNo}"); + } + finally + { + _isApplyingSelectedRecord = false; + } + } + + private void ClearFormByDirectionSwitch() + { + // 已选榜单后切换进出方向,清空数据防止串单。 + WeighDate = DateTime.Today; + PlateNumber = null; + SenderUnit = null; + ReceiverUnit = null; + GoodsName = null; + DriverName = null; + DriverPhone = null; + GrossWeight = null; + TareWeight = null; + NetWeight = null; + GrossWeightCaptured = false; + TareWeightCaptured = false; + SelectedRecentWeightRecord = null; + IsPlateNumberLocked = false; + ClearSupplierSelection(); + ClearCustomerSelection(); + ClearVehicleMatch(); + RaisePropertyChanged(nameof(NetWeightDisplay)); + AddLog("已切换进出方向,当前表单数据已清空"); + } + + // ─── 车辆档案查询与回写 ─── + + private async Task LookupVehicleByPlateAsync(string plate) + { + if (string.Equals(plate, _lastLookedUpPlate, StringComparison.OrdinalIgnoreCase)) return; + _lastLookedUpPlate = plate; + + VehicleLookupStatus = "Searching"; + try + { + var result = await _vehicleService.PageAsync(1, 1, plateNumber: plate); + var vehicle = result.Records.FirstOrDefault(v => + string.Equals(v.PlateNumber?.Trim(), plate, StringComparison.OrdinalIgnoreCase) + && v.Status != "1"); + + if (vehicle != null) + { + _matchedVehicle = vehicle; + VehicleLookupStatus = "Matched"; + ApplyVehicleToForm(vehicle); + } + else + { + _matchedVehicle = null; + VehicleLookupStatus = "NotFound"; + } + } + catch + { + _matchedVehicle = null; + VehicleLookupStatus = "None"; + } + } + + private void ApplyVehicleToForm(MesXslVehicle vehicle) + { + // 司机信息优先带出 + if (!string.IsNullOrWhiteSpace(vehicle.DriverName)) DriverName = vehicle.DriverName; + if (!string.IsNullOrWhiteSpace(vehicle.DriverPhone)) DriverPhone = vehicle.DriverPhone; + + // 根据车辆归属自动判断进出方向与发收货单位 + if (vehicle.VehicleBelong == "2") // 供应商 → 进厂 + { + if (!_isApplyingSelectedRecord) InoutDirection = "1"; + _selectedSupplier = null; + SenderUnit = vehicle.SupplierShortName ?? vehicle.SupplierName; + RaisePropertyChanged(nameof(SenderUnitDisplay)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + } + else if (vehicle.VehicleBelong == "1") // 客户 → 出厂 + { + if (!_isApplyingSelectedRecord) InoutDirection = "2"; + _selectedCustomer = null; + ReceiverUnit = vehicle.CustomerShortName; + RaisePropertyChanged(nameof(ReceiverUnitDisplay)); + RaisePropertyChanged(nameof(HasSelectedCustomer)); + } + // VehicleBelong == "3"(本公司):方向不定,不自动切换 + + AddLog($"车辆档案匹配:{vehicle.PlateNumber}({vehicle.VehicleBelongText})"); + } + + private void ClearVehicleMatch() + { + _matchedVehicle = null; + _lastLookedUpPlate = null; + VehicleLookupStatus = "None"; + } + + private async Task UpsertVehicleAsync() + { + if (string.IsNullOrWhiteSpace(PlateNumber)) return; + try + { + if (_matchedVehicle != null) + { + // 已有档案:仅在司机信息有变动时回写 + bool driverChanged = + !string.Equals(DriverName, _matchedVehicle.DriverName) || + !string.Equals(DriverPhone, _matchedVehicle.DriverPhone); + if (!driverChanged) return; + + await _vehicleService.EditAsync(new MesXslVehicle + { + Id = _matchedVehicle.Id, + PlateNumber = _matchedVehicle.PlateNumber, + VehicleBelong = _matchedVehicle.VehicleBelong, + TareWeightKg = _matchedVehicle.TareWeightKg, + LoadCapacity = _matchedVehicle.LoadCapacity, + SupplierId = _matchedVehicle.SupplierId, + SupplierName = _matchedVehicle.SupplierName, + SupplierShortName = _matchedVehicle.SupplierShortName, + CustomerIds = _matchedVehicle.CustomerIds, + CustomerShortName = _matchedVehicle.CustomerShortName, + DriverName = DriverName, + DriverPhone = DriverPhone, + Status = _matchedVehicle.Status, + TenantId = _matchedVehicle.TenantId, + Version = _matchedVehicle.Version, + }); + AddLog($"车辆档案已更新:{PlateNumber}"); + } + else + { + // 无档案:根据表单信息新建车辆档案 + var vehicleBelong = InoutDirection == "2" ? "1" : "2"; // 出厂→客户 进厂→供应商 + var newVehicle = new MesXslVehicle + { + PlateNumber = PlateNumber?.Trim(), + VehicleBelong = vehicleBelong, + SupplierId = _selectedSupplier?.Id, + SupplierName = _selectedSupplier?.SupplierName, + SupplierShortName = _selectedSupplier?.SupplierShortName, + CustomerShortName = _selectedCustomer?.CustomerShortName + ?? (InoutDirection == "2" ? ReceiverUnit : null), + DriverName = DriverName, + DriverPhone = DriverPhone, + Status = "0", + }; + await _vehicleService.AddAsync(newVehicle); + AddLog($"已新建车辆档案:{PlateNumber}"); + } + } + catch + { + // 车辆回写失败不影响磅单保存,静默处理 + } + } + + protected override void CleanUp() + { + base.CleanUp(); + _serialTimer.Stop(); + } +} + +public class WeightRecordSimpleItem +{ + public MesXslWeightRecord? Source { get; set; } + public string BillNo { get; set; } = "-"; + public string PlateNumber { get; set; } = "-"; + public string FirstWeightDisplay { get; set; } = "-"; +} diff --git a/yy-admin-master/YY.Admin/Views/WeightRecord/CustomerPickerDialogView.xaml b/yy-admin-master/YY.Admin/Views/WeightRecord/CustomerPickerDialogView.xaml new file mode 100644 index 0000000..cf7134d --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/WeightRecord/CustomerPickerDialogView.xaml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/WeightRecord/WeightRecordOperationView.xaml.cs b/yy-admin-master/YY.Admin/Views/WeightRecord/WeightRecordOperationView.xaml.cs new file mode 100644 index 0000000..118fe57 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/WeightRecord/WeightRecordOperationView.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows.Controls; + +namespace YY.Admin.Views.WeightRecord; + +public partial class WeightRecordOperationView : UserControl +{ + public WeightRecordOperationView() => InitializeComponent(); +} diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/Configuration/appsettings.json b/yy-admin-master/_publish/YY.Admin-win-x64/Configuration/appsettings.json new file mode 100644 index 0000000..9abbb4e --- /dev/null +++ b/yy-admin-master/_publish/YY.Admin-win-x64/Configuration/appsettings.json @@ -0,0 +1,96 @@ +{ + // 缓存配置 + "Cache": { + "Prefix": "yyadmin_", // 全局缓存前缀 + "CacheType": "Memory", // Memory、Redis + "Redis": { + "Configuration": "server=localhost;db=2;password=123456;", // Redis连接字符串 + "Prefix": "yyadmin_", // Redis前缀(目前没用) + "MaxMessageSize": "1048576" // 最大消息大小 默认1024 * 1024 + } + }, + // 数据库连接字符串参考地址:https://www.connectionstrings.com/ + "DbConnection": { + "EnableConsoleSql": true, // 启用控制台打印SQL + "ConnectionConfigs": [ + { + "ConfigId": "1300000000001", // 默认库标识-禁止修改 + "DbType": "Sqlite", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access、OpenGauss、QuestDB、HG、ClickHouse、GBase、Odbc、Custom + "DbNickName": "系统库", + //"ConnectionString": "DataSource=./Admin.NET.db", // Sqlite + "ConnectionString": "DataSource=./Admin.NET.db", // Sqlite + //"ConnectionString": "PORT=5432;DATABASE=xxx;HOST=localhost;PASSWORD=xxx;USER ID=xxx", // PostgreSQL + //"ConnectionString": "server= ;port=;database=;user=;password=;CharSet=utf8;sslmode=none;max pool size=1000;", // MySql, + "DbSettings": { + "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + "EnableInitView": false, // 启用视图初始化(若实体和视图没有变化建议关闭) + "EnableDiffLog": false, // 启用库表差异日志 + "EnableUnderLine": true, // 启用驼峰转下划线 + "EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密) + }, + "TableSettings": { + "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + }, + "SeedSettings": { + "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭) + "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + } + }, + { + "ConfigId": "Slave", // 从数据库 + "DbType": "Sqlite", // 数据库类型 + "DbNickName": "业务库", + "ConnectionString": "Data Source=./Slave.db", // Sqlite + "DbSettings": { + "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + "EnableInitView": false, // 启用视图初始化(若实体和视图没有变化建议关闭) + "EnableDiffLog": false, // 启用库表差异日志 + "EnableUnderLine": true, // 启用驼峰转下划线 + "EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密) + }, + "TableSettings": { + "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + }, + "SeedSettings": { + "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭) + "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + } + } + ] + }, + "AutoUpdate": { + "RemoteConfigUrl": "http://14.103.155.227:8083/updates/version.xml" //更新文件地址 + }, + "JeecgIntegration": { + "Enabled": true, // 是否启用Jeecg(可与本地并存;见 PreferLocalLogin 决定先后顺序) + "PreferLocalLogin": true, // true:先校验本地库再尝试 Jeecg(工控脱网时无需等待 MES,直接用本地账号登录) + "FallbackToLocal": true, // false:仅 MES 登录,失败即结束(不推荐工控场景) + "BaseUrl": "http://127.0.0.1:8080/jeecg-boot", // Jeecg后端地址(按实际环境修改) + "LoginPath": "/sys/login", // Jeecg登录接口 + "UserInfoPath": "/sys/user/getUserInfo", // Jeecg用户信息接口 + "UserListPath": "/sys/user/scada/queryUser", // Jeecg 用户列表(SCADA:分页 + updatedAfter 增量,见文档) + "ScadaUserPageSize": 500, // SCADA queryUser 每页条数,最大 1000 + "ScadaUserIncludeDetail": false, // true 时含部门/公司/租户明细,耗时更高 + "ScadaUseUpdatedAfter": true, // 后台/定时同步:有水位时用 updatedAfter 增量。登录触发的 SCADA 同步固定全量分页,避免只拉到少数变更用户 + "TenantListPath": "/sys/tenant/list", // Jeecg租户分页接口 + "UserPermissionPath": "/sys/permission/getUserPermissionByToken", // Jeecg当前用户菜单与按钮权限接口 + "ResetLocalIdentityDataOnJeecgLogin": false, // true 时 Jeecg 登录成功会清空本地用户/角色等(易丢失种子账号导致脱网无法登录)。工控独立运行建议保持 false + "AutoProvisionLocalUser": true, // Jeecg认证成功后本地不存在账号时自动创建 + "SyncUserProfileToLocal": true, // 每次登录时同步Jeecg用户基础信息到本地 + "SyncAllUsersOnJeecgLogin": true, // true:Jeecg 登录成功后拉取用户列表并写入本地(关闭则不会同步用户表) + "UseJeecgUserIdAsLocalPrimaryKey": true, // true 时本地 sys_user.id 与 Jeecg getUserInfo 的 id 一致(雪花 long) + "UserSyncSkipUnchanged": true, // 与Jeecg updateTime 一致时跳过写库,减少重复保存 + "UserListUseUpdateTimeQuery": false, // 仅非 SCADA 的 /sys/user/list:true 时附带 updateTime_begin。SCADA 增量由 ScadaUseUpdatedAfter 控制 + "IncrementalSyncOverlapMinutes": 2, // 增量时间窗口重叠,避免时钟误差漏数据 + "BackgroundSyncIntervalMinutes": 30, // 主窗口在线时定时增量同步间隔(分钟) + "AnonymousMode": true, // 工控机免密模式:优先走免登录接口与匿名WebSocket通道 + "WebSocketUrl": "", // 可选:Jeecg 或自建推送地址;免密模式下若误配到 /ws/device/websocket 会自动切回 /websocket/scada-sync + "WebSocketPath": "/websocket/scada-sync", // 匿名实时推送通道 + "WebSocketInactivityReconnectSeconds": 0, // 0=关闭空闲强制重连;仅保留WS心跳保活,避免重连窗口丢推送 + "DefaultTenantId": 1002, // 自动创建本地用户时使用的默认租户ID + "Captcha": "", // 如启用登录验证码,在此传入验证码 + "CheckKey": "" // 如启用登录验证码,在此传入验证码key + } +} diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/D3DCompiler_47_cor3.dll b/yy-admin-master/_publish/YY.Admin-win-x64/D3DCompiler_47_cor3.dll new file mode 100644 index 0000000..948cc90 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/D3DCompiler_47_cor3.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/Microsoft.Data.SqlClient.SNI.dll b/yy-admin-master/_publish/YY.Admin-win-x64/Microsoft.Data.SqlClient.SNI.dll new file mode 100644 index 0000000..12b8900 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/Microsoft.Data.SqlClient.SNI.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/PenImc_cor3.dll b/yy-admin-master/_publish/YY.Admin-win-x64/PenImc_cor3.dll new file mode 100644 index 0000000..3791694 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/PenImc_cor3.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/PresentationNative_cor3.dll b/yy-admin-master/_publish/YY.Admin-win-x64/PresentationNative_cor3.dll new file mode 100644 index 0000000..a2337af Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/PresentationNative_cor3.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/Updates/version.xml b/yy-admin-master/_publish/YY.Admin-win-x64/Updates/version.xml new file mode 100644 index 0000000..2f5a08e --- /dev/null +++ b/yy-admin-master/_publish/YY.Admin-win-x64/Updates/version.xml @@ -0,0 +1,13 @@ + + + 1.2.0.0 + http://your-update-server.com/YourAppSetup.exe + + • 新增用户管理功能 + • 优化系统性能 + • 修复已知问题 + • 改进用户体验 + + 2025-11-26 + true + \ No newline at end of file diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/WebView2Loader.dll b/yy-admin-master/_publish/YY.Admin-win-x64/WebView2Loader.dll new file mode 100644 index 0000000..f6d1d7b Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/WebView2Loader.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/YY.Admin.dll.config b/yy-admin-master/_publish/YY.Admin-win-x64/YY.Admin.dll.config new file mode 100644 index 0000000..bda80c2 --- /dev/null +++ b/yy-admin-master/_publish/YY.Admin-win-x64/YY.Admin.dll.config @@ -0,0 +1,18 @@ + + + + +
+ + +
+ + + + + + 0 + + + + \ No newline at end of file diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/YY.Admin.exe b/yy-admin-master/_publish/YY.Admin-win-x64/YY.Admin.exe new file mode 100644 index 0000000..fe67fb8 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/YY.Admin.exe differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/e_sqlite3.dll b/yy-admin-master/_publish/YY.Admin-win-x64/e_sqlite3.dll new file mode 100644 index 0000000..8c1c1d9 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/e_sqlite3.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/glfw3.dll b/yy-admin-master/_publish/YY.Admin-win-x64/glfw3.dll new file mode 100644 index 0000000..63ef361 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/glfw3.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/libHarfBuzzSharp.dll b/yy-admin-master/_publish/YY.Admin-win-x64/libHarfBuzzSharp.dll new file mode 100644 index 0000000..2bb6849 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/libHarfBuzzSharp.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/libSkiaSharp.dll b/yy-admin-master/_publish/YY.Admin-win-x64/libSkiaSharp.dll new file mode 100644 index 0000000..8fa4499 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/libSkiaSharp.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/vcruntime140_cor3.dll b/yy-admin-master/_publish/YY.Admin-win-x64/vcruntime140_cor3.dll new file mode 100644 index 0000000..5786e93 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/vcruntime140_cor3.dll differ diff --git a/yy-admin-master/_publish/YY.Admin-win-x64/wpfgfx_cor3.dll b/yy-admin-master/_publish/YY.Admin-win-x64/wpfgfx_cor3.dll new file mode 100644 index 0000000..36f7b75 Binary files /dev/null and b/yy-admin-master/_publish/YY.Admin-win-x64/wpfgfx_cor3.dll differ