|
|
@@ -3,7 +3,6 @@ using Aspose.Words;
|
|
|
using EyeSoft.Extensions;
|
|
|
using EyeSoft.IO;
|
|
|
using FluentValidation;
|
|
|
-using Humanizer;
|
|
|
using NPOI.SS.UserModel;
|
|
|
using NPOI.XSSF.UserModel;
|
|
|
using OASystem.API.OAMethodLib;
|
|
|
@@ -911,7 +910,8 @@ namespace OASystem.API.Controllers
|
|
|
return Ok(JsonView(true, "操作成功!", new List<dynamic> {
|
|
|
new { Id = 1, Name = "生成收款单(四川)" },
|
|
|
new { Id = 2, Name = "生成收款单(北京)" },
|
|
|
- new { Id = 3, Name = "汇款账单" }
|
|
|
+ new { Id = 3, Name = "汇款账单" },
|
|
|
+ new { Id = 4, Name = "实际报价明细" }
|
|
|
}
|
|
|
));
|
|
|
}
|
|
|
@@ -1491,6 +1491,117 @@ namespace OASystem.API.Controllers
|
|
|
|
|
|
return Ok(JsonView(true, "成功", new { Url = url }));
|
|
|
}
|
|
|
+ else if (dto.FileType == 4) //实际报价明细
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 1. 文件验证
|
|
|
+ if (_DelegationInfo.FrFilePaths == null || _DelegationInfo.FrFilePaths.Count < 1)
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, "该团组未上传实际报价相关文件!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 构建源文件夹路径
|
|
|
+ var groupDir = @$"{_DelegationInfo.TeamName}({_DelegationInfo.Id})";
|
|
|
+ var sourceFolderPath = AppSettingsHelper.Get("ReceivablesUploadFileBasePath") + @"/" + groupDir;
|
|
|
+
|
|
|
+ if (!Directory.Exists(sourceFolderPath))
|
|
|
+ {
|
|
|
+ return NotFound(JsonView(false, $"文件夹不存在: {sourceFolderPath}"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 检查文件夹是否为空
|
|
|
+ var files = Directory.GetFiles(sourceFolderPath, "*.*", SearchOption.AllDirectories);
|
|
|
+ if (files.Length == 0)
|
|
|
+ {
|
|
|
+ return BadRequest(JsonView(false, $"文件夹为空: {Path.GetFileName(sourceFolderPath)}"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 生成目标Zip文件路径
|
|
|
+ string targetZipPath = sourceFolderPath.Trim();
|
|
|
+
|
|
|
+ // 5. 确保目标目录存在
|
|
|
+ var targetDirectory = Path.GetDirectoryName(targetZipPath);
|
|
|
+ if (!Directory.Exists(targetDirectory))
|
|
|
+ {
|
|
|
+ Directory.CreateDirectory(targetDirectory);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 检查目标文件是否已存在(可选:覆盖或返回错误)
|
|
|
+ if (System.IO.File.Exists(targetZipPath))
|
|
|
+ {
|
|
|
+ System.IO.File.Delete(targetZipPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ //// 7. 创建Zip包
|
|
|
+ //using (var zipFileStream = new FileStream(targetZipPath, FileMode.Create))
|
|
|
+ //{
|
|
|
+ // using (var zipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Create))
|
|
|
+ // {
|
|
|
+ // foreach (string filePath in filePaths)
|
|
|
+ // {
|
|
|
+ // // 增加空值/空字符串校验,提升代码健壮性
|
|
|
+ // if (!string.IsNullOrEmpty(filePath) && System.IO.File.Exists(filePath))
|
|
|
+ // {
|
|
|
+ // // 获取文件名
|
|
|
+ // string fileName = Path.GetFileName(filePath);
|
|
|
+ // // 将文件添加到ZIP归档中
|
|
|
+ // zipArchive.CreateEntryFromFile(filePath, fileName);
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ //}
|
|
|
+
|
|
|
+
|
|
|
+ //// 8. 验证Zip文件是否创建成功
|
|
|
+ //if (!File.Exists(targetZipPath))
|
|
|
+ //{
|
|
|
+ // return StatusCode(StatusCodes.Status500InternalServerError,
|
|
|
+ // JsonView(false, "Zip文件创建失败"));
|
|
|
+ //}
|
|
|
+
|
|
|
+ //var fileInfo = new FileInfo(targetZipPath);
|
|
|
+ //if (fileInfo.Length == 0)
|
|
|
+ //{
|
|
|
+ // SafeDeleteFile(targetZipPath);
|
|
|
+ // return StatusCode(StatusCodes.Status500InternalServerError,
|
|
|
+ // JsonView(false, "创建的Zip文件为空"));
|
|
|
+ //}
|
|
|
+
|
|
|
+ //// 9. 计算文件哈希(可选)
|
|
|
+ //var fileHash = await CalculateFileHashAsync(targetZipPath);
|
|
|
+
|
|
|
+ //// 10. 创建返回结果
|
|
|
+ //var result = new ZipCreationResult
|
|
|
+ //{
|
|
|
+ // Success = true,
|
|
|
+ // Message = "文件夹打包成功",
|
|
|
+ // SourceFolderPath = sourceFolderPath,
|
|
|
+ // ZipFilePath = targetZipPath,
|
|
|
+ // ZipFileSize = fileInfo.Length,
|
|
|
+ // ZipFileSizeFormatted = FormatFileSize(fileInfo.Length),
|
|
|
+ // FileCount = creationResult.FileCount,
|
|
|
+ // ZipCreationTime = fileInfo.CreationTime,
|
|
|
+ // FileHash = fileHash,
|
|
|
+ // AccessUrl = GenerateAccessUrl(targetZipPath) // 生成访问URL
|
|
|
+ //};
|
|
|
+
|
|
|
+ //// 11. 记录到数据库(可选)
|
|
|
+ //await SaveZipCreationRecordAsync(request, result);
|
|
|
+
|
|
|
+ //_logger.LogInformation("文件夹打包成功: {SourceFolderPath} -> {TargetZipPath}, 大小: {Size}, 文件数: {FileCount}",
|
|
|
+ // sourceFolderPath, targetZipPath, result.ZipFileSizeFormatted, result.FileCount);
|
|
|
+
|
|
|
+ return Ok(JsonView(false, "操作失败!"));
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ //_logger.LogError(ex, "文件夹打包失败: {SourceFolderPath}", request?.SourceFolderPath);
|
|
|
+ //return StatusCode(StatusCodes.Status500InternalServerError,
|
|
|
+ // JsonView(false, $"文件夹打包失败: {ex.Message}"));
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
return Ok(JsonView(false, "操作失败!"));
|
|
|
}
|
|
|
@@ -1562,7 +1673,6 @@ namespace OASystem.API.Controllers
|
|
|
lshCell.AppendChild(p);
|
|
|
}
|
|
|
|
|
|
-
|
|
|
/// <summary>
|
|
|
/// 已收账单
|
|
|
/// 提示导入出入境报价费用
|
|
|
@@ -1579,6 +1689,516 @@ namespace OASystem.API.Controllers
|
|
|
var view = _mapper.Map<List<ProceedsReceivedNewView>>(data);
|
|
|
return Ok(JsonView(true, "操作成功", view));
|
|
|
}
|
|
|
+
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 已收账单上传文件
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="groupId">团组ID</param>
|
|
|
+ /// <param name="files">上传的文件列表</param>
|
|
|
+ /// <returns>操作结果</returns>
|
|
|
+ [HttpPost]
|
|
|
+ [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
|
|
|
+ [ProducesResponseType(typeof(JsonView), StatusCodes.Status400BadRequest)]
|
|
|
+ public async Task<IActionResult> ReceivablesUploadFile(int groupId, List<IFormFile> files)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 1. 参数验证
|
|
|
+ if (groupId < 1) return Ok(JsonView(false, "请传入有效的GroupId参数!"));
|
|
|
+
|
|
|
+ if (files == null || !files.Any()) return Ok(JsonView(false, "请选择要上传的文件!"));
|
|
|
+
|
|
|
+ // 2. 文件验证
|
|
|
+ var fileValidationResult = await ValidateFilesAsync(files);
|
|
|
+ if (!fileValidationResult.IsValid)
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, fileValidationResult.ErrorMessage));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 检查团组信息
|
|
|
+ var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(x => x.IsDel == 0 && x.Id == groupId);
|
|
|
+ if (groupInfo == null)
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, $"团组信息不存在!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 创建文件存储目录
|
|
|
+ var groupDir = @$"{groupInfo.TeamName}({groupInfo.Id})";
|
|
|
+ var fileServerPath = AppSettingsHelper.Get("ReceivablesUploadFileBasePath") + @"/" + groupDir;
|
|
|
+ if (!Directory.Exists(fileServerPath)) Directory.CreateDirectory(fileServerPath);
|
|
|
+
|
|
|
+ // 5. 为文件生成安全文件名
|
|
|
+ var uploadResults = new List<string>();
|
|
|
+ foreach (var file in fileValidationResult.ValidFiles)
|
|
|
+ {
|
|
|
+ var safeFileName = GenerateSafeFileName(file.FileName, groupInfo);
|
|
|
+ var savePath = Path.Combine(fileServerPath, safeFileName);
|
|
|
+
|
|
|
+ // 防止文件名冲突
|
|
|
+ var uniqueFileName = GetUniqueFileName(savePath);
|
|
|
+ var finalSavePath = Path.Combine(fileServerPath, uniqueFileName);
|
|
|
+
|
|
|
+ // 确保资源释放
|
|
|
+ using (var fileStream = new FileStream(finalSavePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
|
|
|
+ {
|
|
|
+
|
|
|
+ await file.CopyToAsync(fileStream);
|
|
|
+
|
|
|
+ // 刷新缓冲区
|
|
|
+ await fileStream.FlushAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ uploadResults.Add($"{groupDir}/{uniqueFileName}");
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!uploadResults.Any())
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, "文件存储失败!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 数据库存储路径
|
|
|
+ var upd = await _sqlSugar.Updateable<Grp_DelegationInfo>()
|
|
|
+ .SetColumns(x => x.FrFilePaths == uploadResults)
|
|
|
+ .Where(x => x.Id == groupId)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+ if (upd < 1)
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, "文件路径存储失败!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 返回上传结果
|
|
|
+ var url = AppSettingsHelper.Get("OfficeBaseUrl");
|
|
|
+ var pathPrefix = AppSettingsHelper.Get("ReceivablesUploadFileFtpPath");
|
|
|
+ for (int i = 0; i < uploadResults.Count; i++)
|
|
|
+ {
|
|
|
+ uploadResults[i] = @$"{url}/{pathPrefix}/{uploadResults[i]}";
|
|
|
+ }
|
|
|
+
|
|
|
+ return Ok(JsonView(true, "操作成功!", uploadResults));
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, "已收账单文件上传失败,GroupId: {GroupId}", groupId);
|
|
|
+ return StatusCode(StatusCodes.Status500InternalServerError,
|
|
|
+ JsonView(false, "系统繁忙,请稍后重试"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #region 文件名安全处理
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 生成安全的文件名
|
|
|
+ /// </summary>
|
|
|
+ private string GenerateSafeFileName(string originalFileName, Grp_DelegationInfo groupInfo)
|
|
|
+ {
|
|
|
+ // 1. 移除路径信息,只保留文件名
|
|
|
+ var fileName = Path.GetFileName(originalFileName);
|
|
|
+
|
|
|
+ // 2. 移除特殊字符和危险字符
|
|
|
+ fileName = RemoveDangerousCharacters(fileName);
|
|
|
+
|
|
|
+ // 3. 移除或替换空白字符
|
|
|
+ fileName = RemoveWhitespaceCharacters(fileName);
|
|
|
+
|
|
|
+ // 4. 限制文件名长度
|
|
|
+ fileName = LimitFileNameLength(fileName);
|
|
|
+
|
|
|
+ // 5. 可选:添加时间戳和团组标识
|
|
|
+ //fileName = AddFileIdentifier(fileName, groupInfo);
|
|
|
+
|
|
|
+ return fileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 移除危险字符
|
|
|
+ /// </summary>
|
|
|
+ private string RemoveDangerousCharacters(string fileName)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(fileName))
|
|
|
+ return Guid.NewGuid().ToString(); // 如果文件名无效,生成GUID
|
|
|
+
|
|
|
+ // 定义危险字符(包含路径遍历、特殊系统字符等)
|
|
|
+ var dangerousChars = new[]
|
|
|
+ {
|
|
|
+ "..", "/", "\\", ":", "*", "?", "\"", "<", ">", "|",
|
|
|
+ "\0", "\r", "\n", "\t", "\b", "\f" // 控制字符
|
|
|
+ };
|
|
|
+
|
|
|
+ // 定义需要替换的字符
|
|
|
+ var charMap = new Dictionary<char, char>
|
|
|
+ {
|
|
|
+ ['#'] = '-',
|
|
|
+ ['%'] = '-',
|
|
|
+ ['&'] = '-',
|
|
|
+ ['@'] = '-',
|
|
|
+ ['+'] = '-',
|
|
|
+ ['='] = '-',
|
|
|
+ [';'] = '-',
|
|
|
+ [','] = '-',
|
|
|
+ ['`'] = '-',
|
|
|
+ ['~'] = '-',
|
|
|
+ ['!'] = '-',
|
|
|
+ ['^'] = '-',
|
|
|
+ ['('] = '-',
|
|
|
+ [')'] = '-',
|
|
|
+ ['['] = '-',
|
|
|
+ [']'] = '-',
|
|
|
+ ['{'] = '-',
|
|
|
+ ['}'] = '-',
|
|
|
+ ['$'] = '-'
|
|
|
+ };
|
|
|
+
|
|
|
+ // 移除危险字符串
|
|
|
+ foreach (var dangerous in dangerousChars)
|
|
|
+ {
|
|
|
+ fileName = fileName.Replace(dangerous, string.Empty);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 替换特殊字符
|
|
|
+ var chars = fileName.ToCharArray();
|
|
|
+ for (int i = 0; i < chars.Length; i++)
|
|
|
+ {
|
|
|
+ if (charMap.TryGetValue(chars[i], out var replacement))
|
|
|
+ {
|
|
|
+ chars[i] = replacement;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return new string(chars);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 移除空白字符
|
|
|
+ /// </summary>
|
|
|
+ private string RemoveWhitespaceCharacters(string fileName)
|
|
|
+ {
|
|
|
+ // 移除开头和结尾的空白
|
|
|
+ fileName = fileName.Trim();
|
|
|
+
|
|
|
+ // 替换内部连续空白为单个下划线
|
|
|
+ var regex = new Regex(@"\s+");
|
|
|
+ fileName = regex.Replace(fileName, "_");
|
|
|
+
|
|
|
+ return fileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 限制文件名长度
|
|
|
+ /// </summary>
|
|
|
+ private string LimitFileNameLength(string fileName)
|
|
|
+ {
|
|
|
+ const int maxFileNameLength = 100; // 限制文件名最大长度
|
|
|
+
|
|
|
+ if (fileName.Length <= maxFileNameLength)
|
|
|
+ return fileName;
|
|
|
+
|
|
|
+ // 获取文件扩展名
|
|
|
+ var extension = Path.GetExtension(fileName);
|
|
|
+ var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(extension))
|
|
|
+ {
|
|
|
+ // 没有扩展名,直接截断
|
|
|
+ return fileName.Substring(0, maxFileNameLength);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保留扩展名,截断主文件名
|
|
|
+ var maxNameLength = maxFileNameLength - extension.Length;
|
|
|
+ if (maxNameLength <= 0)
|
|
|
+ {
|
|
|
+ // 如果扩展名太长,使用短名称
|
|
|
+ return Guid.NewGuid().ToString().Substring(0, 8) + extension;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nameWithoutExtension.Length > maxNameLength)
|
|
|
+ {
|
|
|
+ nameWithoutExtension = nameWithoutExtension.Substring(0, maxNameLength);
|
|
|
+ }
|
|
|
+
|
|
|
+ return nameWithoutExtension + extension;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 添加文件标识符
|
|
|
+ /// </summary>
|
|
|
+ private string AddFileIdentifier(string fileName, Grp_DelegationInfo groupInfo)
|
|
|
+ {
|
|
|
+ // 可选:添加时间戳
|
|
|
+ var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
|
|
|
+
|
|
|
+ // 可选:添加团组标识
|
|
|
+ var groupIdentifier = $"{groupInfo.TeamName}_{groupInfo.Id}";
|
|
|
+ groupIdentifier = RemoveDangerousCharacters(groupIdentifier);
|
|
|
+ groupIdentifier = RemoveWhitespaceCharacters(groupIdentifier);
|
|
|
+
|
|
|
+ // 获取文件扩展名
|
|
|
+ var extension = Path.GetExtension(fileName);
|
|
|
+ var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
|
|
|
+
|
|
|
+ // 如果文件名太短,添加标识符
|
|
|
+ if (string.IsNullOrWhiteSpace(nameWithoutExtension) || nameWithoutExtension.Length < 3)
|
|
|
+ {
|
|
|
+ nameWithoutExtension = $"{groupIdentifier}_{timestamp}";
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 添加标识符前缀
|
|
|
+ nameWithoutExtension = $"{groupIdentifier}_{timestamp}_{nameWithoutExtension}";
|
|
|
+ }
|
|
|
+
|
|
|
+ return nameWithoutExtension + extension;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 获取唯一文件名(防止冲突)
|
|
|
+ /// </summary>
|
|
|
+ private string GetUniqueFileName(string filePath)
|
|
|
+ {
|
|
|
+ var directory = Path.GetDirectoryName(filePath);
|
|
|
+ var fileName = Path.GetFileName(filePath);
|
|
|
+ var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
|
|
|
+ var extension = Path.GetExtension(fileName);
|
|
|
+
|
|
|
+ var counter = 1;
|
|
|
+ var newFileName = fileName;
|
|
|
+
|
|
|
+ while (System.IO.File.Exists(Path.Combine(directory, newFileName)))
|
|
|
+ {
|
|
|
+ newFileName = $"{fileNameWithoutExtension}_{counter}{extension}";
|
|
|
+ counter++;
|
|
|
+
|
|
|
+ // 防止无限循环
|
|
|
+ if (counter > 1000)
|
|
|
+ {
|
|
|
+ newFileName = $"{fileNameWithoutExtension}_{Guid.NewGuid().ToString().Substring(0, 8)}{extension}";
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return newFileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ #region 私有方法
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 验证文件
|
|
|
+ /// </summary>
|
|
|
+ private async Task<(bool IsValid, string ErrorMessage, List<IFormFile> ValidFiles)> ValidateFilesAsync(List<IFormFile> files)
|
|
|
+ {
|
|
|
+ var allowedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
|
+ {
|
|
|
+ ".pdf", ".xls", ".xlsx", ".jpg", ".jpeg", ".png", ".doc", ".docx", ".txt"
|
|
|
+ };
|
|
|
+
|
|
|
+ const long maxFileSize = 20 * 1024 * 1024; // 20MB
|
|
|
+
|
|
|
+ var invalidFiles = new List<string>();
|
|
|
+ var validFiles = new List<IFormFile>();
|
|
|
+
|
|
|
+ foreach (var file in files)
|
|
|
+ {
|
|
|
+ // 检查文件大小
|
|
|
+ if (file.Length > maxFileSize)
|
|
|
+ {
|
|
|
+ string[] sizes = { "B", "KB", "MB", "GB", "TB" };
|
|
|
+ double len = file.Length;
|
|
|
+ int order = 0;
|
|
|
+
|
|
|
+ while (len >= 1024 && order < sizes.Length - 1)
|
|
|
+ {
|
|
|
+ order++;
|
|
|
+ len = len / 1024;
|
|
|
+ }
|
|
|
+
|
|
|
+ string fileSize = string.Format("{0:0.##} {1}", len, sizes[order]);
|
|
|
+
|
|
|
+ invalidFiles.Add($"{file.FileName} 文件大小({fileSize})超过限制(20MB)");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查扩展名
|
|
|
+ var fileExtension = Path.GetExtension(file.FileName);
|
|
|
+ if (string.IsNullOrEmpty(fileExtension) || !allowedExtensions.Contains(fileExtension))
|
|
|
+ {
|
|
|
+ invalidFiles.Add($"{file.FileName} 文件类型({fileExtension})不支持");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 基础文件名安全检查
|
|
|
+ var fileName = Path.GetFileName(file.FileName);
|
|
|
+ if (!IsFileNameSafe(fileName))
|
|
|
+ {
|
|
|
+ invalidFiles.Add($"{file.FileName} 文件名包含非法字符");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ validFiles.Add(file);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (invalidFiles.Any())
|
|
|
+ {
|
|
|
+ return (false, $"文件验证失败: {string.Join("; ", invalidFiles)}", new List<IFormFile>());
|
|
|
+ }
|
|
|
+
|
|
|
+ return (true, string.Empty, validFiles);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 基础文件名安全检查
|
|
|
+ /// </summary>
|
|
|
+ private bool IsFileNameSafe(string fileName)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(fileName))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ // 检查是否包含路径遍历
|
|
|
+ if (fileName.Contains(".."))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ // 检查是否包含路径分隔符
|
|
|
+ if (fileName.Contains("/") || fileName.Contains("\\"))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ // 检查系统保留字符
|
|
|
+ var systemChars = new[] { ":", "*", "?", "\"", "<", ">", "|", "\0" };
|
|
|
+ if (systemChars.Any(ch => fileName.Contains(ch)))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ... 其他方法保持不变
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 已收账单 删除已上传的文件
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="groupId">团组ID</param>
|
|
|
+ /// <param name="fileName">要删除的文件名</param>
|
|
|
+ /// <returns>操作结果</returns>
|
|
|
+ [HttpDelete]
|
|
|
+ [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
|
|
|
+ [ProducesResponseType(typeof(JsonView), StatusCodes.Status400BadRequest)]
|
|
|
+ [ProducesResponseType(typeof(JsonView), StatusCodes.Status404NotFound)]
|
|
|
+ public async Task<IActionResult> DeleteReceivablesFile(int groupId, [FromBody] string fileName)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 1. 参数验证
|
|
|
+ if (groupId < 1)
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, "请传入有效的GroupId参数!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.IsNullOrWhiteSpace(fileName))
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, "请指定要删除的文件名!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 安全检查:防止路径遍历攻击
|
|
|
+ if (fileName.Contains("..") || fileName.Contains("/") || fileName.Contains("\\"))
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, "文件名包含非法字符!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 获取团组信息
|
|
|
+ var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(x => x.IsDel == 0 && x.Id == groupId);
|
|
|
+ if (groupInfo == null)
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, $"团组信息不存在!"));
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 4. 获取当前文件列表
|
|
|
+ var currentFilePaths = groupInfo.FrFilePaths ?? new List<string>();
|
|
|
+ var groupDir = @$"{groupInfo.TeamName}({groupInfo.Id})";
|
|
|
+
|
|
|
+ // 5. 检查要删除的文件是否存在
|
|
|
+ if (!currentFilePaths.Contains(groupDir + @"/" + fileName))
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, $"文件 '{fileName}' 不存在于该团组的文件列表中!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 构建文件完整路径
|
|
|
+ var fileServerPath = AppSettingsHelper.Get("ReceivablesUploadFileBasePath") + @"/" + groupDir;
|
|
|
+ var fullFilePath = Path.Combine(fileServerPath, fileName);
|
|
|
+
|
|
|
+ // 7. 验证文件是否存在
|
|
|
+ if (!System.IO.File.Exists(fullFilePath))
|
|
|
+ {
|
|
|
+ // 文件在磁盘上不存在,但从数据库中移除记录
|
|
|
+ var upd = await RemoveFileFromDatabaseAsync(groupId, groupDir + @"/" + fileName, currentFilePaths);
|
|
|
+ return Ok(JsonView(false, $"文件 '{fileName}' 在服务器上不存在,已从数据库中移除记录!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 8. 删除物理文件
|
|
|
+ bool fileDeleted = false;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ System.IO.File.Delete(fullFilePath);
|
|
|
+ fileDeleted = true;
|
|
|
+ }
|
|
|
+ catch (IOException ioEx)
|
|
|
+ {
|
|
|
+ _logger.LogError(ioEx, "删除文件失败,FilePath: {FilePath}", fullFilePath);
|
|
|
+ return Ok(JsonView(false, $"删除文件失败:文件可能正在被使用或不存在。"));
|
|
|
+ }
|
|
|
+ catch (UnauthorizedAccessException authEx)
|
|
|
+ {
|
|
|
+ _logger.LogError(authEx, "删除文件权限不足,FilePath: {FilePath}", fullFilePath);
|
|
|
+ return Ok(JsonView(false, $"删除文件失败:权限不足。"));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!fileDeleted)
|
|
|
+ {
|
|
|
+ return Ok(JsonView(false, $"删除文件失败!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 9. 更新数据库(从文件列表中移除该文件)
|
|
|
+ await RemoveFileFromDatabaseAsync(groupId, groupDir + @"/" + fileName, currentFilePaths);
|
|
|
+
|
|
|
+ return Ok(JsonView(true, $"文件 '{fileName}' 删除成功!"));
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, "删除文件失败,GroupId: {GroupId}, FileName: {FileName}", groupId, fileName);
|
|
|
+ return StatusCode(StatusCodes.Status500InternalServerError,
|
|
|
+ JsonView(false, "系统繁忙,请稍后重试"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 从数据库中移除文件记录
|
|
|
+ /// </summary>
|
|
|
+ private async Task<bool> RemoveFileFromDatabaseAsync(int groupId, string fileName, List<string> currentFilePaths)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 从当前文件列表中移除指定文件
|
|
|
+ var updatedFileList = currentFilePaths.Where(f => f != fileName).ToList();
|
|
|
+
|
|
|
+ // 更新数据库
|
|
|
+ var upd = await _sqlSugar.Updateable<Grp_DelegationInfo>()
|
|
|
+ .SetColumns(x => x.FrFilePaths == updatedFileList)
|
|
|
+ .Where(x => x.Id == groupId)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+ return upd > 0;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, "更新数据库文件列表失败,GroupId: {GroupId}, FileName: {FileName}",
|
|
|
+ groupId, fileName);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
#endregion
|
|
|
|
|
|
#region 已收款项
|
|
|
@@ -7234,7 +7854,7 @@ Group by PriceType ", dto.diId);
|
|
|
foreach (var keyword in keywords.Where(k => !string.IsNullOrWhiteSpace(k)))
|
|
|
{
|
|
|
query = query.Where(x => x.TeamName.Contains(keyword.Trim()));
|
|
|
-
|
|
|
+
|
|
|
}
|
|
|
}
|
|
|
|