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>
| | |
| | | |
| | | 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 |
| | | { |
| | |
| | | using WIDESEA_Common.StockEnum; |
| | | using SqlSugar; |
| | | using WIDESEA_Common.StockEnum; |
| | | using WIDESEA_Core; |
| | | using WIDESEA_DTO.Stock; |
| | | using WIDESEA_IStockService; |
| | |
| | | } |
| | | |
| | | /// <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) |
| | |
| | | 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 |
| | |
| | | }; |
| | | |
| | | result = StockInfoService.Repository.AddData(entity, x => x.Details); |
| | | if (result) return content.OK("组盘成功"); |
| | | return content.Error("组盘失败"); |
| | | return result ? content.OK("组盘成功") : content.Error("组盘失败"); |
| | | }); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | /// </summary> |
| | | public async Task<WebResponseContent> ChangePalletAsync(StockDTO stock) |
| | | { |
| | | |
| | | WebResponseContent content = new WebResponseContent(); |
| | | if (stock == null || |
| | | string.IsNullOrWhiteSpace(stock.TargetPalletNo) || |
| | |
| | | return content.Error("源托盘号与目标托盘号相同"); |
| | | } |
| | | |
| | | return await ExecuteWithinTransactionAsync(async () => |
| | | { |
| | | var sourceStock = await StockInfoService.Repository.QueryDataNavFirstAsync(s => s.PalletCode == stock.SourcePalletNo); |
| | | if (sourceStock == null) return content.Error("源托盘不存在"); |
| | | |
| | |
| | | var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities); |
| | | if (!result) return content.Error("换盘失败"); |
| | | return content.OK("换盘成功"); |
| | | }); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | 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("源托盘不存在"); |
| | | |
| | |
| | | var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities); |
| | | if (!result) return content.Error("拆盘失败"); |
| | | return content.OK("拆盘成功"); |
| | | }); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | } |
| | | } |
| | | |
| | | 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> |
| | |
| | | /// <returns></returns> |
| | | public object GetTreeItem(int menuId) |
| | | { |
| | | return GetTreeItem(menuId); |
| | | return x_GetTreeItem(menuId); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | 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) |
| | | { |
| | |
| | | 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 |
| | |
| | | if (!updateLocationResult || !updateStockResult) |
| | | return WebResponseContent.Instance.Error("任务完成失败"); |
| | | return await CompleteTaskAsync(task); |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | |
| | | 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; |
| | |
| | | if (!updateLocationResult || !updateStockResult) |
| | | return WebResponseContent.Instance.Error("任务完成失败"); |
| | | return await CompleteTaskAsync(task); |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | |
| | | 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(); |
| | |
| | | return WebResponseContent.Instance.Error("移库任务完成失败"); |
| | | |
| | | return await CompleteTaskAsync(task); |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | |
| | | taskList.Add(task); |
| | | } |
| | | |
| | | var transactionResult = await ExecuteWithinTransactionAsync(async () => |
| | | { |
| | | var addResult = await BaseDal.AddDataAsync(taskList) > 0; |
| | | if (!addResult) |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | return WebResponseContent.Instance.OK(); |
| | | }); |
| | | if (!transactionResult.Status) |
| | | { |
| | | return transactionResult; |
| | | } |
| | | |
| | | // 6. 通知 WCS(异步,不影响主流程) |
| | | _ = Task.Run(() => |
| | | { |
| | |
| | | using System.Reflection; |
| | | using System.Text; |
| | | using Autofac; |
| | | using Autofac.Core; |
| | | using Autofac.Extensions.DependencyInjection; |
| | |
| | | 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; |
| | |
| | | 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(); |
| | | |
| | |
| | | <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> |
| | | |
| | |
| | | <CopyToOutputDirectory>Always</CopyToOutputDirectory> |
| | | </Content> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <Folder Include="logs\" /> |
| | | </ItemGroup> |
| | | </Project> |
| | |
| | | { |
| | | "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", |
| | |
| | | // 注意,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", |