wanshenmean
2026-03-26 e25dc0d8fad5a2362bf75cf5ca9f26a0fe6c674c
feat(WMS): 增强日志配置与添加事务处理支持

后端变更:
- ServiceBase.cs: 新增 ExecuteWithinTransactionAsync 事务处理辅助方法
- 自动管理事务开始/提交/回滚
- 支持嵌套事务(仅在外层事务时真正开启事务)
- StockSerivce.cs/Sys_MenuService.cs/TaskService.cs: 应用新的事务方法

日志配置增强:
- Program.cs:
- Serilog 配置优化控制台输出模板
- 添加 Seq 日志服务器支持用于结构化日志
- 日志文件增加大小限制(10MB)
- WIDESEA_WMSServer.csproj:
- 添加 Serilog.Sinks.Seq 包(v9.0.0)
- 更新 Serilog.Sinks.File 到 v6.0.0
- appsettings.json:
- 新增 Serilog 详细配置节
- 新增 LocalLogConfig 本地日志配置(日志级别/文件大小/文件数量等)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
已修改7个文件
237 ■■■■■ 文件已修改
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/BaseServices/ServiceBase.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_SystemService/Sys_MenuService.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/WIDESEA_WMSServer.csproj 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/BaseServices/ServiceBase.cs
@@ -36,6 +36,51 @@
        public ISqlSugarClient Db => BaseDal.Db;
        protected async Task<WebResponseContent> ExecuteWithinTransactionAsync(Func<Task<WebResponseContent>> operation)
        {
            var db = Db as SqlSugarClient;
            if (db == null)
            {
                return WebResponseContent.Instance.Error("Database context does not support transactions");
            }
            var ownsTransaction = db.Ado.Transaction == null;
            try
            {
                if (ownsTransaction)
                {
                    db.BeginTran();
                }
                var result = await operation();
                if (result?.Status == true)
                {
                    if (ownsTransaction)
                    {
                        db.CommitTran();
                    }
                    return result;
                }
                if (ownsTransaction)
                {
                    db.RollbackTran();
                }
                return result ?? WebResponseContent.Instance.Error("Transaction failed");
            }
            catch
            {
                if (ownsTransaction)
                {
                    db.RollbackTran();
                }
                throw;
            }
        }
        private PropertyInfo[] _propertyInfo { get; set; } = null;
        public PropertyInfo[] TProperties
        {
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs
@@ -1,4 +1,5 @@
using WIDESEA_Common.StockEnum;
using SqlSugar;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Stock;
using WIDESEA_IStockService;
@@ -51,6 +52,51 @@
        }
        /// <summary>
        /// 在事务中执行操作
        /// </summary>
        private async Task<WebResponseContent> ExecuteWithinTransactionAsync(Func<Task<WebResponseContent>> operation)
        {
            var db = StockInfoService.Repository.Db as SqlSugarClient;
            if (db == null)
            {
                return WebResponseContent.Instance.Error("Database context does not support transactions");
            }
            var ownsTransaction = db.Ado.Transaction == null;
            try
            {
                if (ownsTransaction)
                {
                    db.BeginTran();
                }
                var result = await operation();
                if (result?.Status == true)
                {
                    if (ownsTransaction)
                    {
                        db.CommitTran();
                    }
                    return result;
                }
                if (ownsTransaction)
                {
                    db.RollbackTran();
                }
                return result ?? WebResponseContent.Instance.Error("Transaction failed");
            }
            catch
            {
                if (ownsTransaction)
                {
                    db.RollbackTran();
                }
                throw;
            }
        }
        /// <summary>
        /// 组盘
        /// </summary>
        public async Task<WebResponseContent> GroupPalletAsync(StockDTO stock)
@@ -72,14 +118,15 @@
                Status = StockStatusEmun.组盘暂存.GetHashCode(),
            }).ToList();
            return await ExecuteWithinTransactionAsync(async () =>
            {
            var existingStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
            var result = false;
            if (existingStock != null)
            {
                details.ForEach(d => d.StockId = existingStock.Id);
                result = await StockInfoDetailService.Repository.AddDataAsync(details) > 0;
                if (result) return content.OK("组盘成功");
                return content.Error("组盘失败");
                    return result ? content.OK("组盘成功") : content.Error("组盘失败");
            }
            var entity = new Dt_StockInfo
@@ -92,8 +139,8 @@
            };
            result = StockInfoService.Repository.AddData(entity, x => x.Details);
            if (result) return content.OK("组盘成功");
            return content.Error("组盘失败");
                return result ? content.OK("组盘成功") : content.Error("组盘失败");
            });
        }
        /// <summary>
@@ -101,7 +148,6 @@
        /// </summary>
        public async Task<WebResponseContent> ChangePalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            if (stock == null ||
                string.IsNullOrWhiteSpace(stock.TargetPalletNo) ||
@@ -111,6 +157,8 @@
                return content.Error("源托盘号与目标托盘号相同");
            }
            return await ExecuteWithinTransactionAsync(async () =>
            {
            var sourceStock = await StockInfoService.Repository.QueryDataNavFirstAsync(s => s.PalletCode == stock.SourcePalletNo);
            if (sourceStock == null) return content.Error("源托盘不存在");
@@ -149,6 +197,7 @@
            var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);
            if (!result) return content.Error("换盘失败");
            return content.OK("换盘成功");
            });
        }
        /// <summary>
@@ -160,6 +209,8 @@
            if (stock == null || string.IsNullOrWhiteSpace(stock.SourcePalletNo))
                return content.Error("源托盘号不能为空");
            return await ExecuteWithinTransactionAsync(async () =>
            {
            var sourceStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
            if (sourceStock == null) return content.Error("源托盘不存在");
@@ -185,6 +236,7 @@
            var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);
            if (!result) return content.Error("拆盘失败");
            return content.OK("拆盘成功");
            });
        }
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_SystemService/Sys_MenuService.cs
@@ -309,6 +309,30 @@
            }
        }
        public object x_GetTreeItem(int menuId)
        {
            var sysMenu = BaseDal.QueryData(x => x.MenuId == menuId)
                .Select(
                p => new
                {
                    p.MenuId,
                    p.ParentId,
                    p.MenuName,
                    p.Url,
                    p.Auth,
                    p.OrderNo,
                    p.Icon,
                    p.Enable,
                    // 2022.03.26增移动端加菜单类型
                    MenuType = p.MenuType,
                    p.CreateDate,
                    p.Creater,
                    p.TableName,
                    p.ModifyDate
                }).FirstOrDefault();
            return sysMenu;
        }
        /// <summary>
        /// 编辑菜单时,获取菜单信息
        /// </summary>
@@ -316,7 +340,7 @@
        /// <returns></returns>
        public object GetTreeItem(int menuId)
        {
            return GetTreeItem(menuId);
            return x_GetTreeItem(menuId);
        }
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -172,18 +172,23 @@
                var locationInfo = await _locationInfoService.GetLocationInfo(task.Roadway);
                if (locationInfo == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                return await ExecuteWithinTransactionAsync(async () =>
                {
                locationInfo.LocationStatus = LocationStatusEnum.FreeLock.GetHashCode();
                task.CurrentAddress = task.SourceAddress;
                task.NextAddress = locationInfo.LocationCode;
                task.TargetAddress = locationInfo.LocationCode;
                task.TaskStatus = TaskInStatusEnum.Line_InFinish.GetHashCode();
                var updateResult = await BaseDal.UpdateDataAsync(task);
                var locationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
                    var updateTaskResult = await BaseDal.UpdateDataAsync(task);
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
                    if (!updateTaskResult || !updateLocationResult)
                    {
                        return WebResponseContent.Instance.Error("任务更新失败");
                    }
                return WebResponseContent.Instance.OK(
                    updateResult && locationResult ? "任务更新成功" : "任务更新失败",
                    locationInfo.LocationCode);
                    return WebResponseContent.Instance.OK("任务更新成功", locationInfo.LocationCode);
                });
            }
            catch (Exception ex)
            {
@@ -205,6 +210,10 @@
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await ExecuteWithinTransactionAsync(async () =>
                {
                stockInfo.LocationCode = location.LocationCode;
                stockInfo.LocationId = location.Id;
                stockInfo.OutboundDate = task.Roadway switch
@@ -222,6 +231,7 @@
                if (!updateLocationResult || !updateStockResult)
                    return WebResponseContent.Instance.Error("任务完成失败");
                return await CompleteTaskAsync(task);
                });
            }
            catch (Exception ex)
            {
@@ -243,6 +253,10 @@
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode); 
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await ExecuteWithinTransactionAsync(async () =>
                {
                stockInfo.LocationId = 0;
                stockInfo.LocationCode = null;
                stockInfo.OutboundDate = DateTime.Now;
@@ -254,6 +268,7 @@
                if (!updateLocationResult || !updateStockResult)
                    return WebResponseContent.Instance.Error("任务完成失败");
                return await CompleteTaskAsync(task);
                });
            }
            catch (Exception ex)
            {
@@ -282,6 +297,8 @@
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await ExecuteWithinTransactionAsync(async () =>
                {
                stockInfo.LocationCode = targetLocation.LocationCode;
                stockInfo.LocationId = targetLocation.Id;
                stockInfo.StockStatus = StockStatusEmun.入库完成.GetHashCode();
@@ -297,6 +314,7 @@
                    return WebResponseContent.Instance.Error("移库任务完成失败");
                return await CompleteTaskAsync(task);
                });
            }
            catch (Exception ex)
            {
@@ -549,6 +567,8 @@
                    taskList.Add(task);
                }
                var transactionResult = await ExecuteWithinTransactionAsync(async () =>
                {
                var addResult = await BaseDal.AddDataAsync(taskList) > 0;
                if (!addResult)
                {
@@ -590,6 +610,13 @@
                    }
                }
                    return WebResponseContent.Instance.OK();
                });
                if (!transactionResult.Status)
                {
                    return transactionResult;
                }
                // 6. 通知 WCS(异步,不影响主流程)
                _ = Task.Run(() =>
                {
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
@@ -1,5 +1,3 @@
using System.Reflection;
using System.Text;
using Autofac;
using Autofac.Core;
using Autofac.Extensions.DependencyInjection;
@@ -12,6 +10,9 @@
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Serilog;
using Serilog.Formatting.Json;
using System.Reflection;
using System.Text;
using WIDESEA_Core;
using WIDESEA_Core.Authorization;
using WIDESEA_Core.BaseServices;
@@ -42,13 +43,27 @@
    loggerConfiguration
        .ReadFrom.Configuration(context.Configuration)
        .ReadFrom.Services(services)
        .Enrich.FromLogContext()
        .WriteTo.Console()
        //.Enrich.FromLogContext()
        .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
        .WriteTo.File(
            "logs/serilog-.log.txt",
            //new JsonFormatter(renderMessage: true),
            "logs/serilog-.log",
            outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
            rollingInterval: RollingInterval.Day,
            retainedFileCountLimit: 30,
            shared: true);
            // 每个日志文件最大大小(字节),此处设置为10MB
            fileSizeLimitBytes: 10 * 1024 * 1024,
            shared: true
            )
         // 6. 可选:输出到Seq日志服务器(结构化日志服务器)
         // 需要安装 Serilog.Sinks.Seq NuGet包,并确保Seq服务在 http://localhost:5341 运行
         // 如不需要Seq日志,注释掉下方代码即可
         .WriteTo.Seq(
             serverUrl: "http://localhost:5341",
             apiKey: "CWVa8UWQ9CdUp9GWXCPL", // 如Seq需要ApiKey则配置真实密钥
             batchPostingLimit: 1000, // 批量发送数量
             period: TimeSpan.FromSeconds(2) // 发送间隔
         );
});
builder.ConfigureApplication();
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/WIDESEA_WMSServer.csproj
@@ -50,7 +50,8 @@
      <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
      <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
      <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
      <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
      <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
      <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
      <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.7.3" />
    </ItemGroup>
    
@@ -88,4 +89,8 @@
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      </Content>
    </ItemGroup>
    <ItemGroup>
      <Folder Include="logs\" />
    </ItemGroup>
</Project>
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json
@@ -1,10 +1,31 @@
{
  "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Information",
        "Microsoft.AspNetCore": "Warning",
        "Microsoft.AspNetCore.Routing": "Warning",
        "Microsoft.AspNetCore.Mvc": "Warning",
        "Microsoft.AspNetCore.Mvc.Infrastructure": "Warning",
        "Microsoft.AspNetCore.Mvc.Filters": "Warning",
        "Microsoft.AspNetCore.Mvc.ModelBinding": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning"
      }
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.DataProtection": "Warning"
      "Microsoft.AspNetCore.DataProtection": "Warning",
      "Microsoft.AspNetCore.Routing": "Warning",
      "Microsoft.AspNetCore.Mvc": "Warning",
      "Microsoft.AspNetCore.Mvc.Infrastructure": "Warning",
      "Microsoft.AspNetCore.Mvc.Filters": "Warning",
      "Microsoft.AspNetCore.Mvc.ModelBinding": "Warning"
    }
  },
  "dics": "inOrderType,outOrderType,inboundState,createType,enableEnum,enableStatusEnum,locationStatusEnum,locationTypeEnum,taskTypeEnum,taskStatusEnum,outboundStatusEnum,orderDetailStatusEnum,stockStatusEmun,stockChangeType,outStockStatus,receiveOrderTypeEnum,authorityScope,authorityScopes,locationChangeType,warehouses,suppliers,taskType,receiveStatus,purchaseType,purchaseOrderStatus,printStatus",
@@ -25,6 +46,14 @@
    // 注意,http://127.0.0.1:1818 和 http://localhost:1818 是不一样的
    "IPs": "http://127.0.0.1:8080,http://localhost:8080,http://127.0.0.1:8081,http://localhost:8081"
  },
  "LocalLogConfig": {
    "LogLevel": "DEBUG", //日志级别 DEBUG,INFO,WARN,ERROR,FATAL
    "LogFileSize": 10, //单个日志文件大小,单位MB
    "LogFileCount": 300, //日志文件数量
    "EnableConsoleOutput": false, //是否输出到控制台
    "EnableFloderByLevel": true //是否按日志级别生成不同的文件夹
  },
  "LogAopEnable": false,
  "PrintSql": false, //打印SQL语句
  "ApiName": "WIDESEA",