Browse Source

Merge branch 'develop' of http://132.232.92.186:3000/XinXiBu/OA2023 into develop

yuanrf 1 day ago
parent
commit
9600b42547

+ 0 - 2
OASystem/OASystem.Api/Controllers/AuthController.cs

@@ -137,7 +137,6 @@ namespace OASystem.API.Controllers
                 AnnouncementUnReadCount = announcementUnReadCount
             };
 
-
             DateTime createZebraTime = DateTime.Now;
             string authorId = dto.Number + "Token";
             string authorToken = await RedisRepository.RedisFactory.CreateRedisRepository().StringGetAsync<string>(authorId);//string 取
@@ -237,7 +236,6 @@ namespace OASystem.API.Controllers
                 AnnouncementUnReadCount = announcementUnReadCount
             };
 
-
             DateTime createZebraTime = DateTime.Now;
             string authorId = dto.Number + "Token";
             string authorToken = await RedisRepository.RedisFactory.CreateRedisRepository().StringGetAsync<string>(authorId);//string 取

+ 138 - 11
OASystem/OASystem.Api/Controllers/FinancialController.cs

@@ -2148,7 +2148,7 @@ namespace OASystem.API.Controllers
 
                 // 4. 获取当前文件列表
                 var currentFilePaths = groupInfo.FrFilePaths ?? new List<string>();
-                var groupDir = @$"{groupInfo.TeamName}({groupInfo.Id})";
+                var groupDir = @$"{groupInfo.TeamName}_{groupInfo.Id}";
 
                 // 5. 检查要删除的文件是否存在
                 if (!currentFilePaths.Contains(groupDir + @"/" + fileName))
@@ -4145,9 +4145,9 @@ namespace OASystem.API.Controllers
                 int rowNumber = 1;
                 foreach (var subItem in item.childList)
                 {
-                    string remaksDescription = $"{rowNumber}、【{item.priceTypeStr}】{item.Instructions}({subItem.PriceName}) CNY:{subItem.ItemTotal.ToString("#0.0000")}(单价:{subItem.Price.ToString("#0.0000")} * {subItem.Quantity.ToString("#0.0000")})";
+                    string remaksDescription = $"{rowNumber}、【{item.priceTypeStr}】{item.Instructions}({subItem.PriceName}) CNY:{subItem.ItemTotal.ToString("#0.0000")}(单价:{subItem.Price.ToString("#0.0000")} * {subItem.Quantity.ToString("#0.0000")})【备注:{(string.IsNullOrEmpty(subItem.Remark) ? " - " : subItem.Remark)}】】";
                     subItem.RemaksDescription = remaksDescription;
-                    string excelRemaksDescription = $"【{item.priceTypeStr}】{item.Instructions}({subItem.PriceName}) CNY:{subItem.ItemTotal.ToString("#0.0000")}(单价:{subItem.Price.ToString("#0.0000")} * {subItem.Quantity.ToString("#0.0000")})【申请人:{item.CreateUser}  申请时间:{item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss")}】";
+                    string excelRemaksDescription = $"【{item.priceTypeStr}】{item.Instructions}({subItem.PriceName}) CNY:{subItem.ItemTotal.ToString("#0.0000")}(单价:{subItem.Price.ToString("#0.0000")} * {subItem.Quantity.ToString("#0.0000")})【备注:{(string.IsNullOrEmpty(subItem.Remark) ? " - " : subItem.Remark)}】【申请人:{item.CreateUser}  申请时间:{item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss")}】";
                     subItem.ExcelRemaksDescription = excelRemaksDescription;
                     rowNumber++;
                 }
@@ -4661,6 +4661,14 @@ namespace OASystem.API.Controllers
                 //    return Ok(JsonView(false, "参数ConpanyId不可使用!"));
                 //}
 
+
+                var designer = new WorkbookDesigner
+                {
+                    Workbook = new Workbook(AppSettingsHelper.Get("ExcelBasePath") + "Template/付款申请书.xls")
+                };
+
+                #region 数据处理
+
                 string _requestPaymentDt = DateTime.Now.ToString("yyyy-MM-dd"),//申请付款日期
                        _appliedAmount = "", //申请付款金额
                        _GZStr = "",    //公转价格描述
@@ -4670,8 +4678,6 @@ namespace OASystem.API.Controllers
                 decimal dailyGZAmout = 0.00M, dailySZAmout = 0.00M;
                 string dailyGZStr = "", dailySZStr = "", groupGZStr = "", groupSZStr = "";
 
-                #region 数据处理
-
                 //团组费用相关
                 foreach (var item in _GroupData)
                 {
@@ -4683,12 +4689,17 @@ namespace OASystem.API.Controllers
                         if (subItem.TransferMark.Equals("公转"))
                         {
                             groupGZAmout += subItem.CNYSubTotalAmount;
-                            groupGZSubStr += $"{subItem.RemaksDescription}\r\n";
+
+                            groupGZSubStr += subItem.IsBgColorShown
+                                ? $"[ORANGE]{subItem.RemaksDescription}[/ORANGE]\r\n"
+                                : $"{subItem.RemaksDescription}\r\n";
                         }
                         else if (subItem.TransferMark.Equals("私转"))
                         {
                             groupSZAmout += subItem.CNYSubTotalAmount;
-                            groupSZSubStr += $"{subItem.RemaksDescription}\r\n";
+                            groupSZSubStr += subItem.IsBgColorShown
+                                ? $"[ORANGE]{subItem.RemaksDescription}[/ORANGE]\r\n"
+                                : $"{subItem.RemaksDescription}\r\n";
                         }
 
                         //groupGZSubStr += $"\t";
@@ -4725,10 +4736,8 @@ namespace OASystem.API.Controllers
                 _appliedAmount = $"公转:CNY {(groupGZAmout + dailyGZAmout).ToString("#0.00")}\r\n私转:CNY {(groupSZAmout + dailySZAmout).ToString("#0.00")}";
                 #endregion
 
-                var designer = new WorkbookDesigner
-                {
-                    Workbook = new Workbook(AppSettingsHelper.Get("ExcelBasePath") + "Template/付款申请书.xls")
-                };
+                //FillExcelWithMarkedText(designer.Workbook.Worksheets[0], _GZStr, 3);
+
                 designer.SetDataSource("Date", _requestPaymentDt);
                 designer.SetDataSource("Price", _appliedAmount);
                 designer.SetDataSource("Content", _GZStr);
@@ -4737,6 +4746,9 @@ namespace OASystem.API.Controllers
                 //根据数据源处理生成报表内容
                 designer.Process();
 
+                // 直接处理工作表中的合并单元格
+                SimplifyProcessOrangeTags(designer.Workbook);
+
                 string fileName = ("PayRequest/付款申请(" + dto.beginDt + "~" + dto.endDt + ").xlsx");
                 designer.Workbook.Save(AppSettingsHelper.Get("ExcelBasePath") + fileName);
                 string rst = AppSettingsHelper.Get("ExcelBaseUrl") + AppSettingsHelper.Get("ExcelFtpPath") + fileName;
@@ -4751,6 +4763,121 @@ namespace OASystem.API.Controllers
             }
         }
 
+        /// <summary>
+        /// 简化版本的处理方法
+        /// </summary>
+        private void SimplifyProcessOrangeTags(Workbook workbook)
+        {
+            Color orangeColor = Color.FromArgb(255, 140, 0);
+
+            Worksheet worksheet = workbook.Worksheets[0];
+
+            // 处理所有合并区域
+            foreach (CellArea mergedArea in worksheet.Cells.MergedCells)
+            {
+                Cell firstCell = worksheet.Cells[mergedArea.StartRow, mergedArea.StartColumn];
+                string cellValue = firstCell.StringValue;
+
+                if (!string.IsNullOrEmpty(cellValue) && cellValue.Contains("[ORANGE]"))
+                {
+                    // 简单替换并应用颜色
+                    ProcessCellSimple(firstCell, cellValue, orangeColor);
+                }
+            }
+
+            // 处理普通单元格
+            for (int row = 0; row <= worksheet.Cells.MaxDataRow; row++)
+            {
+                for (int col = 0; col <= worksheet.Cells.MaxDataColumn; col++)
+                {
+                    if (IsCellInMergedRange(worksheet, row, col))
+                        continue;
+
+                    Cell cell = worksheet.Cells[row, col];
+                    if (cell.StringValue != null && cell.StringValue.Contains("[ORANGE]"))
+                    {
+                        ProcessCellSimple(cell, cell.StringValue, orangeColor);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// 检查单元格是否在合并区域内
+        /// </summary>
+        /// <param name="worksheet">工作表</param>
+        /// <param name="row">行索引(0-based)</param>
+        /// <param name="col">列索引(0-based)</param>
+        /// <returns>如果在合并区域内返回true,否则返回false</returns>
+        private bool IsCellInMergedRange(Worksheet worksheet, int row, int col)
+        {
+            try
+            {
+                // 获取工作表中的所有合并区域
+                ArrayList mergedAreas = worksheet.Cells.MergedCells;
+
+                if (mergedAreas == null || mergedAreas.Count == 0)
+                    return false;
+
+                // 遍历所有合并区域
+                foreach (object areaObj in mergedAreas)
+                {
+                    if (areaObj is CellArea area)
+                    {
+                        // 检查单元格是否在该合并区域内
+                        if (row >= area.StartRow && row <= area.EndRow &&
+                            col >= area.StartColumn && col <= area.EndColumn)
+                        {
+                            // 如果是要找的单元格,但不是第一个单元格(左上角),也返回true
+                            return true;
+                        }
+                    }
+                }
+
+                return false;
+            }
+            catch (Exception ex)
+            {
+                // 出错时记录日志,默认返回false
+                Console.WriteLine($"检查合并区域失败: {ex.Message}");
+                return false;
+            }
+        }
+
+        private void ProcessCellSimple(Cell cell, string text, Color orangeColor)
+        {
+            // 简单的标记处理
+            string plainText = text.Replace("[ORANGE]", "").Replace("[/ORANGE]", "");
+            cell.PutValue(plainText);
+
+            // 找到所有橙色部分并应用颜色
+            int startIndex = 0;
+            while (true)
+            {
+                int orangeStart = text.IndexOf("[ORANGE]", startIndex);
+                if (orangeStart < 0) break;
+
+                int orangeEnd = text.IndexOf("[/ORANGE]", orangeStart);
+                if (orangeEnd < 0) break;
+
+                string orangeContent = text.Substring(orangeStart + "[ORANGE]".Length,
+                                                     orangeEnd - orangeStart - "[ORANGE]".Length);
+
+                // 计算在纯文本中的位置
+                string beforeOrange = text.Substring(0, orangeStart);
+                int plainPosition = beforeOrange.Replace("[ORANGE]", "").Replace("[/ORANGE]", "").Length;
+
+                if (plainPosition >= 0 && plainPosition < plainText.Length)
+                {
+                    FontSetting fontSetting = cell.Characters(plainPosition, orangeContent.Length);
+                    fontSetting.Font.Color = orangeColor;
+                    fontSetting.Font.IsBold = true;
+                }
+
+                startIndex = orangeEnd + "[/ORANGE]".Length;
+            }
+        }
+
         #endregion
 
         #region 超支费用

+ 2 - 4
OASystem/OASystem.Api/Controllers/GroupsController.cs

@@ -16525,7 +16525,6 @@ FROM
         /// <param name="diId"></param>
         /// <param name="currUserId"></param>
         /// <returns></returns>
-        /// <exception cref="CustomException"></exception>
         private async Task<int> EnterExitCostVerifyIsNull(int diId, int currUserId)
         {
             var parentId = 0;
@@ -16540,7 +16539,7 @@ FROM
             {
                 var currencys = await EnterExitCostMobileGetCurrencyInit();
                 int addId = await _sqlSugar.Insertable(new Grp_EnterExitCost() { DiId = diId, CreateUserId = currUserId, CurrencyRemark = JsonConvert.SerializeObject(currencys) }).ExecuteReturnIdentityAsync();
-                if (addId < 1) throw new CustomException("出入境费用添加失败!");
+                
                 parentId = addId;
             }
 
@@ -18246,7 +18245,6 @@ FROM
         /// <param name="draftId"></param>
         /// <param name="currUserId"></param>
         /// <returns></returns>
-        /// <exception cref="CustomException"></exception>
         private async Task<int> EnterExitCostDraftVerifyIsNull(int draftId, int currUserId)
         {
             var parentId = 0;
@@ -18257,7 +18255,7 @@ FROM
             {
                 var currencys = await EnterExitCostDraftMobileGetCurrencyInit();
                 int addId = await _sqlSugar.Insertable(new Grp_EnterExitCostDraft() { CreateUserId = currUserId, CurrencyRemark = JsonConvert.SerializeObject(currencys) }).ExecuteReturnIdentityAsync();
-                if (addId < 1) throw new CustomException("出入境费用添加失败!");
+                
                 parentId = addId;
             }
 

+ 0 - 12
OASystem/OASystem.Api/Middlewares/CustomException.cs

@@ -1,12 +0,0 @@
-namespace OASystem.API.Middlewares
-{
-    /// <summary>
-    /// 自定义异常
-    /// </summary>
-    public class CustomException : Exception
-    {
-        public CustomException(string message) : base(message)
-        {
-        }
-    }
-}

+ 504 - 202
OASystem/OASystem.Api/Middlewares/RecordAPIOperationMiddleware.cs

@@ -3,6 +3,11 @@ using OASystem.Domain.Attributes;
 using OASystem.Domain.Entities.Customer;
 using UAParser;
 using static OASystem.API.OAMethodLib.JWTHelper;
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Microsoft.AspNetCore.Http;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
 
 namespace OASystem.API.Middlewares
 {
@@ -14,25 +19,43 @@ namespace OASystem.API.Middlewares
         private readonly RequestDelegate _next;
         private readonly HttpClient _httpClient;
         private readonly IConfiguration _config;
-        private readonly SqlSugarClient _sqlSugar;
+        private readonly ILogger<RecordAPIOperationMiddleware> _logger;
+        private readonly IServiceProvider _serviceProvider;
 
         /// <summary>
-        /// 初始化DeleteUserId
+        /// 初始化
         /// </summary>
-        /// <param name="next"></param>
-        /// <param name="config"></param>
-        /// <param name="httpClient"></param>
-        /// <param name="sqlSugar"></param>
-        public RecordAPIOperationMiddleware(RequestDelegate next, IConfiguration config, HttpClient httpClient, SqlSugarClient sqlSugar)
+        public RecordAPIOperationMiddleware(
+            RequestDelegate next,
+            IConfiguration config,
+            IHttpClientFactory httpClientFactory,
+            ILogger<RecordAPIOperationMiddleware> logger,
+            IServiceProvider serviceProvider)
         {
             _next = next;
-            _httpClient = httpClient;
             _config = config;
-            _sqlSugar = sqlSugar;
+            _logger = logger;
+            _serviceProvider = serviceProvider;
+            _httpClient = httpClientFactory.CreateClient();
+            _httpClient.Timeout = TimeSpan.FromSeconds(5);
         }
 
         public async Task InvokeAsync(HttpContext context)
         {
+            // 跳过 OPTIONS 请求(CORS 预检请求)
+            if (context.Request.Method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
+            {
+                await _next(context);
+                return;
+            }
+
+            // 跳过静态文件请求
+            if (IsStaticFileRequest(context.Request.Path))
+            {
+                await _next(context);
+                return;
+            }
+
             // 启用请求体流的缓冲,允许多次读取
             context.Request.EnableBuffering();
 
@@ -47,80 +70,95 @@ namespace OASystem.API.Middlewares
                 var startTime = DateTime.UtcNow;
                 int portType = 1, userId = 0, id = 0, status = 0;
                 string updatePreData = string.Empty, updateBefData = string.Empty;
-                bool param5Bool = false, param6Bool = false;
-                
-                try
+
+                // 获取用户ID和其他参数
+                using (var scope = _serviceProvider.CreateScope())
                 {
-                    userId = await ReadToken(context);
+                    var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
 
-                    if (!string.IsNullOrEmpty(requestBodyText))
+                    try
                     {
-                        object param1 = string.Empty, param2 = string.Empty, param3 = string.Empty, param4 = string.Empty,
-                               param5 = string.Empty, param6 = string.Empty, param7 = string.Empty, param8 = string.Empty;
-                        var requestBodyJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(requestBodyText);
-                        bool exists1 = requestBodyJson.TryGetValue("portType", out param1);
-                        bool exists2 = requestBodyJson.TryGetValue("userId", out param2);
-                        bool exists3 = requestBodyJson.TryGetValue("currUserId", out param3);
-                        bool exists4 = requestBodyJson.TryGetValue("createUserId", out param4);
-                        bool exists5 = requestBodyJson.TryGetValue("id", out param5);
-                        bool exists6 = requestBodyJson.TryGetValue("status", out param6);
-                        bool exists7 = requestBodyJson.TryGetValue("operationUserId", out param7);
-                        bool exists8 = requestBodyJson.TryGetValue("deleteUserId", out param8);
-
-                        if (exists1) int.TryParse(param1.ToString(), out portType);
-
-                        //用户Id处理
-                        if (userId < 1)
+                        userId = await ReadToken(context);
+
+                        if (!string.IsNullOrEmpty(requestBodyText))
                         {
-                            if (apiLogAttribute.OperationEnum == OperationEnum.Login)
+                            var requestBodyJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(requestBodyText);
+
+                            if (requestBodyJson != null)
                             {
-                                var number = requestBodyJson?["number"].ToString();
-                                if (!string.IsNullOrEmpty(number))
+                                // 提取参数
+                                if (requestBodyJson.TryGetValue("portType", out var param1Obj) && param1Obj != null)
                                 {
-                                    var info = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Number.Equals(number)).FirstAsync();
+                                    int.TryParse(param1Obj.ToString(), out portType);
+                                }
 
-                                    userId = info?.Id ?? 0;
+                                if (requestBodyJson.TryGetValue("id", out var param5Obj) && param5Obj != null)
+                                {
+                                    int.TryParse(param5Obj.ToString(), out id);
                                 }
-                            }
-                            else
-                            {
-                                if (exists2) int.TryParse(param2.ToString(), out userId);
-                                else if (exists3) int.TryParse(param3.ToString(), out userId);
-                                else if (exists4) int.TryParse(param4.ToString(), out userId);
-                                else if (exists7) int.TryParse(param7.ToString(), out userId);
-                                else if (exists8) int.TryParse(param8.ToString(), out userId);
-                            }
-                        }
-                        //编辑前数据查询;
-                        if (exists5) param5Bool = int.TryParse(param5.ToString(), out id);
-                        if (exists6) param6Bool = int.TryParse(param6.ToString(), out status);
 
-                        if (param6Bool)
-                        {
-                            //  2 修改 
-                            if (status == 1) apiLogAttribute.OperationEnum = OperationEnum.Add;
-                            else if (status == 2)
-                            {
-                                apiLogAttribute.OperationEnum = OperationEnum.Edit;
-                                updatePreData = await TableInfoToJson(apiLogAttribute.TableName, id);
-                            }
-                        }
-                        else if (param5Bool)
-                        {
-                            if (apiLogAttribute.OperationEnum != OperationEnum.Del)
-                            {
-                                //  id > 0 修改 
-                                if (id < 1) apiLogAttribute.OperationEnum = OperationEnum.Add;
-                                else if (id > 0)
+                                if (requestBodyJson.TryGetValue("status", out var param6Obj) && param6Obj != null)
+                                {
+                                    int.TryParse(param6Obj.ToString(), out status);
+                                }
+
+                                // 用户Id处理
+                                if (userId < 1)
+                                {
+                                    if (apiLogAttribute.OperationEnum == OperationEnum.Login)
+                                    {
+                                        var number = requestBodyJson.TryGetValue("number", out var numberObj) ? numberObj?.ToString() : null;
+                                        if (!string.IsNullOrEmpty(number))
+                                        {
+                                            var info = await sqlSugar.Queryable<Sys_Users>()
+                                                .Where(x => x.IsDel == 0 && x.Number.Equals(number))
+                                                .FirstAsync();
+
+                                            userId = info?.Id ?? 0;
+                                        }
+                                    }
+                                    else
+                                    {
+                                        userId = ParseUserIdFromParams(requestBodyJson);
+                                    }
+                                }
+
+                                // 根据status判断操作类型
+                                if (status > 0)
+                                {
+                                    if (status == 1)
+                                        apiLogAttribute.OperationEnum = OperationEnum.Add;
+                                    else if (status == 2)
+                                    {
+                                        apiLogAttribute.OperationEnum = OperationEnum.Edit;
+                                        if (id > 0)
+                                        {
+                                            updatePreData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
+                                        }
+                                    }
+                                }
+                                // 根据id判断操作类型
+                                else if (id > 0 && apiLogAttribute.OperationEnum != OperationEnum.Del)
                                 {
                                     apiLogAttribute.OperationEnum = OperationEnum.Edit;
-                                    updatePreData = await TableInfoToJson(apiLogAttribute.TableName, id);
+                                    updatePreData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
+                                }
+                                else if (apiLogAttribute.OperationEnum != OperationEnum.Del && id < 1)
+                                {
+                                    apiLogAttribute.OperationEnum = OperationEnum.Add;
                                 }
                             }
                         }
                     }
+                    catch (JsonException)
+                    {
+                        _logger.LogDebug("JSON解析失败,可能请求体不是JSON格式");
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.LogError(ex, "解析请求参数时发生错误");
+                    }
                 }
-                catch (JsonException) { }
 
                 // 保存原始响应体流
                 var originalResponseBody = context.Response.Body;
@@ -141,95 +179,92 @@ namespace OASystem.API.Middlewares
                 // 将响应体内容写回原始响应体流
                 await responseMemoryStream.CopyToAsync(originalResponseBody);
 
-                //修改后数据查询
-                if (param6Bool)
-                {
-                    //  2 修改 
-                    if (status == 2) updateBefData = await TableInfoToJson(apiLogAttribute.TableName, id);
-                }
-                else if (param5Bool)
+                // 修改后数据查询
+                if (status == 2 && id > 0)
                 {
-
-                    if (apiLogAttribute.OperationEnum != OperationEnum.Del)
+                    using (var scope = _serviceProvider.CreateScope())
                     {
-                        //  id > 0 修改 
-                        if (id > 0) updateBefData = await TableInfoToJson(apiLogAttribute.TableName, id);
+                        var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
+                        updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
                     }
                 }
-                string remoteIp = string.Empty,
-                       location = string.Empty;
-
-                // 检查请求头中的X-Forwarded-For,以获取真实的客户端IP地址
-                if (context.Request.Headers.ContainsKey("X-Forwarded-For"))
-                {
-                    remoteIp = context.Request.Headers["X-Forwarded-For"].ToString().Split(',', StringSplitOptions.RemoveEmptyEntries)[0];
-                    //_logger.LogInformation($"IP:{remoteIp}");
-                }
-                else
+                else if (apiLogAttribute.OperationEnum == OperationEnum.Edit && id > 0)
                 {
-                    remoteIp = context.Connection.RemoteIpAddress?.ToString();
+                    using (var scope = _serviceProvider.CreateScope())
+                    {
+                        var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
+                        updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
+                    }
                 }
 
-                (remoteIp, location) = await GetIpInfo(remoteIp);
-                //remoteIp = await GetExternalIp();
-                //location = await GetIpLocation(remoteIp);
-
-                string deviceType = string.Empty, browser = string.Empty, os = string.Empty;
-                var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault();
-                if (!string.IsNullOrEmpty(userAgent))
-                {
-                    // 解析User-Agent头
-                    var parser = Parser.GetDefault();
-                    var client = parser.Parse(userAgent);
-
-                    // 提取浏览器信息
-                    browser = client.UA.Family; // 浏览器名称
-                    var browserVersion = client.UA.Major + "." + client.UA.Minor + "." + client.UA.Patch; // 浏览器版本
-                    browser += $"({browserVersion})";
-
-                    // 提取操作系统信息
-                    os = client.OS.Family; // 操作系统名称
-
-                    var osVersion = string.Empty; // 操作系统版本
-                    if (!string.IsNullOrEmpty(client.OS.Major)) osVersion += client.OS.Major;
-                    if (!string.IsNullOrEmpty(client.OS.Minor)) osVersion += "." + client.OS.Minor;
-                    if (!string.IsNullOrEmpty(client.OS.Patch)) osVersion += "." + client.OS.Patch;
-
-                    if (!string.IsNullOrEmpty(osVersion)) os += $"({osVersion})";
-
-                    // 提取设备信息
-                    deviceType = client.Device.Family; // 设备类型,如 'mobile', 'tablet', 'desktop' 等
-
-                }
+                // 获取IP信息和设备信息
+                string remoteIp = GetClientIp(context);
+                string location = await GetIpLocationSafe(remoteIp);
+                var (deviceType, browser, os) = GetDeviceInfo(context);
 
                 // 记录请求结束时间
                 var endTime = DateTime.UtcNow;
                 // 计算耗时
                 var duration = (long)(endTime - startTime).TotalMilliseconds;
 
+                // 截断过长的文本
+                var truncatedRequestBody = TruncateString(requestBodyText, 4000);
+                var truncatedResponseBody = TruncateString(responseBodyText, 4000);
+                var truncatedPreData = TruncateString(updatePreData, 4000);
+                var truncatedBefData = TruncateString(updateBefData, 4000);
+
                 var logInfo = new Crm_TableOperationRecord()
                 {
                     TableName = apiLogAttribute.TableName,
                     PortType = portType,
                     OperationItem = apiLogAttribute.OperationEnum,
                     DataId = id,
-                    RequestUrl = context.Request.Path,
+                    RequestUrl = context.Request.Path + context.Request.QueryString,
                     RemoteIp = remoteIp,
                     Location = location,
-                    RequestParam = !string.IsNullOrEmpty(requestBodyText) ? JsonConvert.SerializeObject(requestBodyText) : null,
-                    ReturnResult = !string.IsNullOrEmpty(responseBodyText) ? JsonConvert.SerializeObject(responseBodyText) : null,
+                    RequestParam = !string.IsNullOrEmpty(truncatedRequestBody) ? JsonConvert.SerializeObject(truncatedRequestBody) : null,
+                    ReturnResult = !string.IsNullOrEmpty(truncatedResponseBody) ? JsonConvert.SerializeObject(truncatedResponseBody) : null,
                     Elapsed = duration,
                     Status = context.Response.StatusCode.ToString(),
                     CreateUserId = userId,
-                    UpdatePreData = updatePreData,
-                    UpdateBefData = updateBefData,
+                    UpdatePreData = truncatedPreData,
+                    UpdateBefData = truncatedBefData,
                     Browser = browser,
                     Os = os,
                     DeviceType = deviceType,
+                    CreateTime = DateTime.Now
                 };
 
-                // 存储到数据库
-                await _sqlSugar.Insertable(logInfo).ExecuteCommandAsync();
+                // 异步记录日志,不阻塞响应
+                _ = Task.Run(async () =>
+                {
+                    try
+                    {
+                        // 在异步任务中创建新的作用域
+                        using var logScope = _serviceProvider.CreateScope();
+                        var logSqlSugar = logScope.ServiceProvider.GetRequiredService<SqlSugarClient>();
+
+                        // 设置较短的超时时间
+                        logSqlSugar.Ado.CommandTimeOut = 3;
+
+                        // 存储到数据库
+                        await logSqlSugar.Insertable(logInfo).ExecuteCommandAsync();
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.LogError(ex, "异步记录操作日志失败");
+
+                        // 降级:写入文件日志
+                        try
+                        {
+                            await WriteLogToFile(apiLogAttribute, requestBodyText, responseBodyText, userId, ex.Message);
+                        }
+                        catch (Exception fileEx)
+                        {
+                            _logger.LogError(fileEx, "写入文件日志失败");
+                        }
+                    }
+                });
             }
             else
             {
@@ -237,119 +272,386 @@ namespace OASystem.API.Middlewares
             }
         }
 
-        private async Task<string> TableInfoToJson(string tableName, int id)
+        /// <summary>
+        /// 从请求参数中解析用户ID
+        /// </summary>
+        private int ParseUserIdFromParams(Dictionary<string, object> requestBodyJson)
         {
-            if (_sqlSugar.DbMaintenance.IsAnyTable(tableName))
+            var userIdKeys = new[] { "userId", "currUserId", "createUserId", "operationUserId", "deleteUserId" };
+
+            foreach (var key in userIdKeys)
             {
-                string jsonLabel = string.Empty;
-                if (tableName.Equals("Crm_NewClientData"))
+                if (requestBodyJson.TryGetValue(key, out var value) && value != null)
                 {
-                    var info = await _sqlSugar.Queryable<Crm_NewClientData>().FirstAsync(x => x.Id == id);
-                    if (info != null)
+                    if (int.TryParse(value.ToString(), out var parsedUserId) && parsedUserId > 0)
                     {
-                        EncryptionProcessor.DecryptProperties(info);
-                        if (info != null) jsonLabel = JsonConvert.SerializeObject(info);
+                        return parsedUserId;
                     }
                 }
-                else
+            }
+
+            return 0;
+        }
+
+        /// <summary>
+        /// 获取表数据JSON
+        /// </summary>
+        private async Task<string> TableInfoToJson(SqlSugarClient sqlSugar, string tableName, int id)
+        {
+            if (sqlSugar.DbMaintenance.IsAnyTable(tableName))
+            {
+                try
                 {
-                    var sql = string.Format(" Select * From {0} Where Id={1}", tableName, id);
+                    string jsonLabel = string.Empty;
+                    if (tableName.Equals("Crm_NewClientData"))
+                    {
+                        var info = await sqlSugar.Queryable<Crm_NewClientData>()
+                            .Where(x => x.Id == id)
+                            .FirstAsync();
 
-                    var info = await _sqlSugar.SqlQueryable<dynamic>(sql).FirstAsync();
-                    if (info != null) jsonLabel = JsonConvert.SerializeObject(info);
+                        if (info != null)
+                        {
+                            EncryptionProcessor.DecryptProperties(info);
+                            if (info != null)
+                                jsonLabel = JsonConvert.SerializeObject(info);
+                        }
+                    }
+                    else
+                    {
+                        var sql = $"SELECT * FROM {tableName} WHERE Id = {id}";
+                        var info = await sqlSugar.SqlQueryable<dynamic>(sql).FirstAsync();
+                        if (info != null)
+                            jsonLabel = JsonConvert.SerializeObject(info);
+                    }
+                    return jsonLabel;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, $"获取表数据失败: {tableName}, Id: {id}");
+                    return string.Empty;
                 }
-                return jsonLabel;
             }
-
             return string.Empty;
         }
 
+        /// <summary>
+        /// 是否是静态文件请求
+        /// </summary>
+        private bool IsStaticFileRequest(PathString path)
+        {
+            try
+            {
+                var staticExtensions = new[] {
+                    ".css", ".js", ".png", ".jpg", ".jpeg", ".gif",
+                    ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot",
+                    ".mp4", ".mp3", ".avi", ".mov", ".wmv", ".flv",
+                    ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
+                    ".zip", ".rar", ".7z", ".tar", ".gz"
+                };
+
+                var pathString = path.Value?.ToLower() ?? string.Empty;
+                return staticExtensions.Any(ext => pathString.EndsWith(ext.ToLower(), StringComparison.OrdinalIgnoreCase));
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// 读取请求体
+        /// </summary>
         private async Task<string> ReadRequestBody(HttpRequest request)
         {
-            request.EnableBuffering();
-
-            using var reader = new StreamReader(
-                request.Body,
-                Encoding.UTF8,
-                detectEncodingFromByteOrderMarks: false,
-                bufferSize: 8192,
-                leaveOpen: true
-            );
-
-            var body = await reader.ReadToEndAsync();
-            request.Body.Position = 0;
-            return body;
+            try
+            {
+                request.Body.Position = 0;
+
+                using var reader = new StreamReader(
+                    request.Body,
+                    Encoding.UTF8,
+                    detectEncodingFromByteOrderMarks: false,
+                    bufferSize: 8192,
+                    leaveOpen: true
+                );
+
+                var body = await reader.ReadToEndAsync();
+                request.Body.Position = 0;
+                return body;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "读取请求体失败");
+                return string.Empty;
+            }
         }
 
+        /// <summary>
+        /// 读取响应体
+        /// </summary>
         private async Task<string> ReadResponseBody(Stream stream)
         {
-            using var reader = new StreamReader(
-                stream,
-                Encoding.UTF8,
-                detectEncodingFromByteOrderMarks: false,
-                bufferSize: 8192,
-                leaveOpen: true
-            );
-
-            var body = await reader.ReadToEndAsync();
-            stream.Position = 0;
-            return body;
+            try
+            {
+                using var reader = new StreamReader(
+                    stream,
+                    Encoding.UTF8,
+                    detectEncodingFromByteOrderMarks: false,
+                    bufferSize: 8192,
+                    leaveOpen: true
+                );
+
+                var body = await reader.ReadToEndAsync();
+                stream.Position = 0;
+                return body;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "读取响应体失败");
+                return string.Empty;
+            }
         }
 
+        /// <summary>
+        /// 读取Token
+        /// </summary>
         private async Task<int> ReadToken(HttpContext context)
         {
-            var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
+            try
+            {
+                var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
 
-            // 检查Authorization头是否存在且以"Bearer "开头
-            if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+                if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+                {
+                    var authInfo = JwtHelper.SerializeJwt(authHeader);
+                    if (authInfo != null)
+                        return authInfo.UserId;
+                }
+            }
+            catch (Exception ex)
             {
-                var authInfo = JwtHelper.SerializeJwt(authHeader);
-                if (authInfo != null) return authInfo.UserId;
+                _logger.LogError(ex, "读取Token失败");
             }
 
             return 0;
         }
 
         /// <summary>
-        /// 获取IP信息
+        /// 获取客户端IP
         /// </summary>
-        /// <param name="ip">ipv4 or ipv6</param>
-        /// <returns></returns>
-        private async Task<(string ip, string local)> GetIpInfo(string ip)
+        private string GetClientIp(HttpContext context)
         {
-            string local = string.Empty;
-
-            //if (IPAddress.TryParse(ip,out _))
-            //{
-            //    var response = await _httpClient.GetAsync($"https://api.vore.top/api/IPdata?ip={ip}");
-
-            //    response.EnsureSuccessStatusCode();
-            //    var json = await response.Content.ReadAsStringAsync();
-            //    var ipInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
-            //    if (ipInfo.code == 200)
-            //    {
-            //        ip = ipInfo.ipinfo.text;
-            //        local = $"{ipInfo.adcode.o}";
-            //    }
-            //}
-
-            return (ip, local);
+            try
+            {
+                // 优先从X-Forwarded-For获取
+                if (context.Request.Headers.ContainsKey("X-Forwarded-For"))
+                {
+                    var xForwardedFor = context.Request.Headers["X-Forwarded-For"].ToString();
+                    if (!string.IsNullOrEmpty(xForwardedFor))
+                    {
+                        var ips = xForwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries);
+                        if (ips.Length > 0)
+                            return ips[0].Trim();
+                    }
+                }
+
+                // 其次从X-Real-IP获取
+                if (context.Request.Headers.ContainsKey("X-Real-IP"))
+                {
+                    var xRealIp = context.Request.Headers["X-Real-IP"].ToString();
+                    if (!string.IsNullOrEmpty(xRealIp))
+                        return xRealIp.Trim();
+                }
+
+                // 最后从RemoteIpAddress获取
+                var remoteIp = context.Connection.RemoteIpAddress?.ToString();
+                if (!string.IsNullOrEmpty(remoteIp))
+                {
+                    // 处理IPv6映射的IPv4地址
+                    if (remoteIp.StartsWith("::ffff:"))
+                        return remoteIp.Substring(7);
+                    return remoteIp;
+                }
+
+                return "Unknown";
+            }
+            catch
+            {
+                return "Unknown";
+            }
         }
 
-        private async Task<string> GetExternalIp()
+        /// <summary>
+        /// 安全获取IP位置
+        /// </summary>
+        private async Task<string> GetIpLocationSafe(string ip)
         {
-            var response = await _httpClient.GetAsync("https://ifconfig.me");
-            response.EnsureSuccessStatusCode();
-            return await response.Content.ReadAsStringAsync();
+            if (string.IsNullOrWhiteSpace(ip) || ip == "Unknown" || ip == "::1" || ip == "127.0.0.1")
+                return "本地";
+
+            try
+            {
+                // IPv6地址
+                if (ip.Contains(":"))
+                    return "IPv6";
+
+                // 检查是否是内网地址
+                if (IsPrivateIp(ip))
+                    return "内网";
+
+                // 可以在这里调用IP查询API
+                // 但为了性能,暂时返回未知
+                return "未知";
+            }
+            catch (Exception ex)
+            {
+                _logger.LogWarning(ex, $"获取IP位置失败: {ip}");
+                return "未知";
+            }
+        }
+
+        /// <summary>
+        /// 检查是否是内网IP
+        /// </summary>
+        private bool IsPrivateIp(string ip)
+        {
+            try
+            {
+                if (string.IsNullOrWhiteSpace(ip))
+                    return false;
+
+                // 常见的私有IP地址段
+                if (ip.StartsWith("10.") ||
+                    ip.StartsWith("192.168.") ||
+                    ip.StartsWith("172.16.") ||
+                    ip.StartsWith("172.17.") ||
+                    ip.StartsWith("172.18.") ||
+                    ip.StartsWith("172.19.") ||
+                    ip.StartsWith("172.20.") ||
+                    ip.StartsWith("172.21.") ||
+                    ip.StartsWith("172.22.") ||
+                    ip.StartsWith("172.23.") ||
+                    ip.StartsWith("172.24.") ||
+                    ip.StartsWith("172.25.") ||
+                    ip.StartsWith("172.26.") ||
+                    ip.StartsWith("172.27.") ||
+                    ip.StartsWith("172.28.") ||
+                    ip.StartsWith("172.29.") ||
+                    ip.StartsWith("172.30.") ||
+                    ip.StartsWith("172.31."))
+                {
+                    return true;
+                }
+
+                return false;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// 获取设备信息
+        /// </summary>
+        private (string deviceType, string browser, string os) GetDeviceInfo(HttpContext context)
+        {
+            try
+            {
+                var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault();
+                if (string.IsNullOrEmpty(userAgent))
+                    return ("Unknown", "Unknown", "Unknown");
+
+                var parser = Parser.GetDefault();
+                var client = parser.Parse(userAgent);
+
+                // 提取浏览器信息
+                var browser = client.UA.Family;
+                var browserVersion = new List<string>();
+                if (!string.IsNullOrEmpty(client.UA.Major))
+                    browserVersion.Add(client.UA.Major);
+                if (!string.IsNullOrEmpty(client.UA.Minor))
+                    browserVersion.Add(client.UA.Minor);
+                if (!string.IsNullOrEmpty(client.UA.Patch))
+                    browserVersion.Add(client.UA.Patch);
+
+                if (browserVersion.Any())
+                    browser += " " + string.Join(".", browserVersion);
+
+                // 提取操作系统信息
+                var os = client.OS.Family;
+                var osVersion = new List<string>();
+                if (!string.IsNullOrEmpty(client.OS.Major))
+                    osVersion.Add(client.OS.Major);
+                if (!string.IsNullOrEmpty(client.OS.Minor))
+                    osVersion.Add(client.OS.Minor);
+                if (!string.IsNullOrEmpty(client.OS.Patch))
+                    osVersion.Add(client.OS.Patch);
+                if (!string.IsNullOrEmpty(client.OS.PatchMinor))
+                    osVersion.Add(client.OS.PatchMinor);
+
+                if (osVersion.Any())
+                    os += " " + string.Join(".", osVersion);
+
+                // 提取设备信息
+                var deviceType = client.Device.Family;
+                if (string.Equals(deviceType, "Other", StringComparison.OrdinalIgnoreCase))
+                {
+                    // 根据userAgent判断设备类型
+                    var ua = userAgent.ToLower();
+                    if (ua.Contains("mobile") || ua.Contains("android") || ua.Contains("iphone") || ua.Contains("ipad"))
+                        deviceType = "Mobile";
+                    else
+                        deviceType = "Desktop";
+                }
+
+                return (deviceType, browser, os);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogDebug(ex, "解析设备信息失败");
+                return ("Unknown", "Unknown", "Unknown");
+            }
         }
 
-        private async Task<string> GetIpLocation(string ip)
+        /// <summary>
+        /// 截断字符串
+        /// </summary>
+        private string TruncateString(string input, int maxLength)
         {
-            var response = await _httpClient.GetAsync($"https://ipinfo.io/{ip}/json?&language=zh-CN");
-            response.EnsureSuccessStatusCode();
-            var json = await response.Content.ReadAsStringAsync();
-            var ipInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
-            return $"{ipInfo.country}, {ipInfo.city}";
+            if (string.IsNullOrEmpty(input) || input.Length <= maxLength)
+                return input;
+
+            return input.Substring(0, maxLength) + "...[已截断]";
+        }
+
+        /// <summary>
+        /// 将日志写入文件
+        /// </summary>
+        private async Task WriteLogToFile(ApiLogAttribute apiLogAttribute, string requestBody, string responseBody, int userId, string error)
+        {
+            try
+            {
+                var logDir = Path.Combine(Directory.GetCurrentDirectory(), "Logs", "OperationLogs");
+                Directory.CreateDirectory(logDir);
+
+                var logFile = Path.Combine(logDir, $"operation_fallback_{DateTime.Now:yyyyMMdd}.log");
+
+                var logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | " +
+                              $"Table: {apiLogAttribute.TableName} | " +
+                              $"Operation: {apiLogAttribute.OperationEnum} | " +
+                              $"User: {userId} | " +
+                              $"Request: {TruncateString(requestBody, 500)} | " +
+                              $"Response: {TruncateString(responseBody, 500)} | " +
+                              $"Error: {error}" +
+                              Environment.NewLine;
+
+                await File.AppendAllTextAsync(logFile, logEntry);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "写入文件日志失败");
+            }
         }
     }
-}
+}

+ 4 - 4
OASystem/OASystem.Domain/Dtos/Groups/GroupListDto.cs

@@ -317,12 +317,12 @@ namespace OASystem.Domain.Dtos.Groups
         /// <summary>
         /// 额外超支额度
         /// </summary>
-        public decimal ExtOverLimit { get; set; }
+        public string ExtOverLimit { get; set; }
 
         /// <summary>
         /// 额外超支额度币种
         /// </summary>
-        public int ExtOverCurrency { get; set; }
+        public string ExtOverCurrency { get; set; }
     }
 
     /// <summary>
@@ -681,12 +681,12 @@ namespace OASystem.Domain.Dtos.Groups
         /// <summary>
         /// 额外超支额度
         /// </summary>
-        public decimal ExtOverLimit { get; set; }
+        public decimal ExtOverLimit { get; set; } = 0.00M;
 
         /// <summary>
         /// 额外超支额度币种
         /// </summary>
-        public int ExtOverCurrency { get; set; }
+        public int ExtOverCurrency { get; set; } = 836;
     }
 
 }

+ 8 - 4
OASystem/OASystem.Infrastructure/Repositories/Groups/DelegationInfoRepository.cs

@@ -794,6 +794,10 @@ namespace OASystem.Infrastructure.Repositories.Groups
 
             if (dto.PortType == 1 || dto.PortType == 2 || dto.PortType == 3) //web
             {
+                //额外超支参数处理
+                decimal extOverLimit = decimal.TryParse(dto.ExtOverLimit, out decimal tempLimit) ? tempLimit : 0.00M;
+                int extOverCurrency = int.TryParse(dto.ExtOverLimit, out int tempCurrency) ? tempCurrency : 836; //默认人名币
+
                 #region 添加出访起止时间
                 var startTime = new DateTime();
                 var endTime = new DateTime();
@@ -907,8 +911,8 @@ namespace OASystem.Infrastructure.Repositories.Groups
                         Officialneeds = dto.Officialneeds,
                         VisitStartDate = startTime,
                         VisitEndDate = endTime,
-                        ExtOverLimit = dto.ExtOverLimit,
-                        ExtOverCurrency = dto.ExtOverCurrency
+                        ExtOverLimit = extOverLimit,
+                        ExtOverCurrency = extOverCurrency
                     };
 
                     var addId = _sqlSugar.Insertable(delegationInfo).ExecuteReturnIdentity();
@@ -960,8 +964,8 @@ namespace OASystem.Infrastructure.Repositories.Groups
                         Officialneeds = dto.Officialneeds,
                         VisitStartDate = startTime,
                         VisitEndDate = endTime,
-                        ExtOverLimit = dto.ExtOverLimit,
-                        ExtOverCurrency = dto.ExtOverCurrency
+                        ExtOverLimit = extOverLimit,
+                        ExtOverCurrency = extOverCurrency
                     });
 
                     if (updateStatus)

+ 1 - 1
OASystem/OASystem.Infrastructure/Repositories/Groups/EnterExitCostRepository.cs

@@ -304,7 +304,7 @@ namespace OASystem.Infrastructure.Repositories.Groups
         /// <returns></returns>
         public async Task<JsonView> GetEnterExitCostInfoByDiId(EnterExitCostInfobyDiIdDto dto)
         {
-            var result = new JsonView() { Code = 400, Msg = "暂无数据" };
+            var result = new JsonView() { Code = 400, Msg = "暂无数据", Data = new { provinceId = 0 } };
 
             var enterExitCostData = await _sqlSugar.Queryable<Grp_EnterExitCost>().OrderByDescending(x => x.CreateTime).FirstAsync(it => it.DiId == dto.DiId && it.IsDel == 0);