Files
zszq-trs/YLErpDAL/Modules/TradeModule/TradeOAService.cs
T

692 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BaseOUDAL;
using NPOI.SS.Formula.Functions;
using Qdp.Foundation.Utilities;
using YLErp.DBModels;
using YLErp.DBModels.Consts;
using YLErp.DBModels.Enums;
using YLErp.Model;
using YLErp.Modules.ApiModule;
using YLErp.Modules.TradeDalModule;
using YLErp.Modules.UnderlyingModule;
using static YLErp.ConsGlobal;
namespace YLErp.Modules.TradeModule
{
/// <summary>
/// 交易OA服务
/// </summary>
public class TradeOAService : YLBaseService
{
private static readonly IYcLogger logger = LogFactory.GetLogger<TradeOAService>();
private static readonly System.Net.Http.HttpClient _httpClient = new System.Net.Http.HttpClient
{
Timeout = TimeSpan.FromMinutes(2)
};
public TradeOAService(OptUserInfo userInfo) : base(userInfo)
{
}
public TradeOAService(YLBaseService baseService) : base(baseService)
{
}
/// <summary>
/// 处理单个交易的OA提交
/// </summary>
/// <param name="tradeId">交易ID</param>
/// <returns>处理结果</returns>
public async Task<TradeOAResult> ProcessSingleOAAsync(int tradeId)
{
try
{
// 获取交易信息
var trade = DbContext.trade.FirstOrDefault(x => x.id == tradeId);
if (trade == null)
{
return new TradeOAResult { TradeId = tradeId, Success = false, Message = "未找到交易信息" };
}
// 获取交易确认书信息
var tcr = DbContext.trade_contract_r.Where(t => t.Type == ContractTypeEnum.Trade && t.IsValid && t.TradeId == tradeId)
.FirstOrDefault();
trade_contract_document tdoc = null;
if (tcr != null)
{
tdoc = DbContext.trade_contract_document.Where(t => t.Code == tcr.ContractCode && t.Type == tcr.Type).FirstOrDefault();
}
// 将之前的OA记录设置为无效
var existingOAResults = DbContext.tradeContractOaResult.Where(x => x.trade_id == tradeId && x.is_valid == true).ToList();
foreach (var existingResult in existingOAResults)
{
existingResult.is_valid = false;
existingResult.SetOpt(UserInfo);
}
// 创建OA结果记录
var oaResult = new trade_contract_oa_result
{
trade_id = tradeId,
contract_code = tcr?.ContractCode,
status = "提交中",
is_valid = true,
};
oaResult.SetOpt(UserInfo);
DbContext.tradeContractOaResult.Add(oaResult);
await DbContext.SaveChangesAsync();
// 调用OA接口
var oaResp = await CallOAInterfaceAsync(trade, tcr?.ContractCode, tdoc, oaResult.id);
if (oaResp != null && oaResp.code == 0)
{
oaResult.status = "提交成功";
oaResult.oa_fileid = oaResp.data?.requestid;
oaResult.oa_msg = oaResp.data?.msg ?? "提交成功";
}
else
{
oaResult.status = "提交失败";
oaResult.oa_msg = oaResp?.data?.msg ?? "OA接口调用失败";
}
await DbContext.SaveChangesAsync();
return new TradeOAResult { TradeId = tradeId, Success = oaResult.status == "提交成功", Message = oaResult.oa_msg };
}
catch (Exception ex)
{
logger.Error($"处理交易ID {tradeId} 的OA时发生错误: {ex.Message}", ex);
return new TradeOAResult { TradeId = tradeId, Success = false, Message = $"提交OA失败: {ex.Message}" };
}
}
/// <summary>
/// 上传附件到OA系统
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>附件数据列表(包含 annexId 和 annexName</returns>
private async Task<List<UploadAttachmentData>> UploadAttachmentAsync(string filePath)
{
try
{
// 从配置文件读取上传配置
var oaConfigSection = AppManager.GetConfiguration().GetSection("oa_confg");
var uploadUrl = oaConfigSection["UploadUrl"];
var systemToken = oaConfigSection["SystemToken"];
if (string.IsNullOrEmpty(uploadUrl))
{
logger.Info("OA接口配置中UploadUrl为空");
throw new Exception("OA接口配置中上传地址为空");
}
if (string.IsNullOrEmpty(systemToken))
{
logger.Info("OA接口配置中SystemToken为空");
throw new Exception("OA接口配置中上传令牌为空");
}
if (!File.Exists(filePath))
{
logger.Info($"附件文件不存在:{filePath}");
throw new Exception("附件文件不存在");
}
// 创建multipart/form-data请求
using (var httpClient = new HttpClient())
{
// 设置超时时间为2分钟
httpClient.Timeout = TimeSpan.FromMinutes(2);
using (var form = new MultipartFormDataContent())
{
// 添加文件
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var fileName = Path.GetFileName(filePath);
form.Add(new StreamContent(fileStream), "file", fileName);
// 添加systemToken
form.Add(new StringContent(systemToken), "systemToken");
// 发送请求
try
{
var response = await httpClient.PostAsync(uploadUrl, form);
response.EnsureSuccessStatusCode(); // 确保响应成功
var responseContent = await response.Content.ReadAsStringAsync();
logger.Info($"附件上传接口返回: {responseContent}");
// 解析响应
try
{
var uploadResponse = JsonHelper.Deserialize<UploadAttachmentResponse>(responseContent);
if (uploadResponse != null)
{
if (uploadResponse.code == "0" && uploadResponse.data != null)
{
// 返回完整的附件数据列表(包含 annexId 和 annexName
logger.Info($"成功上传附件,获取到{uploadResponse.data.Count}个附件");
return uploadResponse.data;
}
else
{
logger.Error($"附件上传失败:{uploadResponse.status}code{uploadResponse.code}");
throw new Exception($"附件上传失败:{uploadResponse.status}");
}
}
else
{
logger.Error("附件上传响应解析为空");
throw new Exception("附件上传响应解析失败");
}
}
catch (Exception ex)
{
logger.Error($"解析附件上传响应失败:{ex.Message},响应内容:{responseContent}", ex);
throw new Exception($"解析附件上传响应失败:{ex.Message}");
}
}
catch (TaskCanceledException ex)
{
logger.Error($"上传附件时任务被取消:{ex.Message}", ex);
throw new Exception("上传附件超时,请检查网络连接");
}
catch (Exception ex)
{
logger.Error($"上传附件时发生异常:{ex.Message}", ex);
throw;
}
}
}
}
catch (Exception ex)
{
logger.Error($"上传附件时发生异常:{ex.Message}", ex);
throw;
}
}
/// <summary>
/// 调用OA接口
/// </summary>
/// <param name="trade">交易信息</param>
/// <param name="contractCode">合同编号</param>
/// <param name="tdoc">交易确认书文档</param>
/// <param name="oaResultId">OA结果记录ID</param>
/// <returns>是否成功</returns>
private async Task<GuoLianOAResponse> CallOAInterfaceAsync(trade trade, string contractCode, trade_contract_document tdoc, int oaResultId)
{
try
{
// 获取OA配置
var oaConfigSection = AppManager.GetConfiguration().GetSection("oa_confg");
var baseUrl = oaConfigSection["BaseUrl"];
var subUrl = oaConfigSection["SubUrl"];
var systemToken = oaConfigSection["SystemToken"];
var systemName = oaConfigSection["SystemName"];
var isNextFlow = oaConfigSection["IsNextFlow"] ?? "0";
var flowId = oaConfigSection["FlowId"];
if (string.IsNullOrEmpty(baseUrl))
{
logger.Info("OA接口配置中BaseUrl为空");
return new GuoLianOAResponse() { code = -1, data = new GuoLianOAData { msg = "OA接口配置中地址为空" } };
}
if (string.IsNullOrEmpty(systemToken))
{
logger.Info("OA接口配置中SystemToken为空");
return new GuoLianOAResponse() { code = -1, data = new GuoLianOAData { msg = "OA接口配置中SystemToken为空" } };
}
// 处理附件
List<UploadAttachmentData> attachmentDataList = null;
if (tdoc != null && !string.IsNullOrEmpty(tdoc.AbsolutePath))
{
try
{
attachmentDataList = await UploadAttachmentAsync(tdoc.AbsolutePath);
logger.Info($"成功上传附件并获取到annexId{string.Join(", ", attachmentDataList.Select(a => a.annexId))}");
}
catch (Exception ex)
{
logger.Error($"上传附件失败:{tdoc.AbsolutePath},错误:{ex.Message}", ex);
return new GuoLianOAResponse() { code = -1, data = new GuoLianOAData { msg = "上传附件失败" } };
}
}
if (tdoc == null)
{
logger.Info("未找到确认书文档");
return new GuoLianOAResponse() { code = -1, data = new GuoLianOAData { msg = "未找到确认书文档" } };
}
//通过UserName找到LoginName
var applicantLoginName = DbContextFactory.GetErpBaseContext().SystemUsers.Where(u => u.Name == UserInfo.UserName).Select(u => u.LoginName).FirstOrDefault();
// 构建detailList,yycl(应用场景/附件)放在明细列表中
var detailList = new List<object>();
if (attachmentDataList != null && attachmentDataList.Any())
{
foreach (var att in attachmentDataList)
{
detailList.Add(new
{
detailIndex = 1,
yylxzx = 3, //用印类型 0公司章 1法人代表签章 2部门章 3营业部章 4无需用章
sl = 1,//用印数量
yzbgyx = "yangx",
qysdzz = "2809700652352016752",
yyslmx = 1,
yycl = att
});
}
}
// 构建dataMap
var dataMap = new
{
title = $"固定收益部申请【{contractCode}】TRS交易确认书",
urgentLevel = "正常",//未找到对应
applyDate = DateTime.Now.ToString("yyyy-MM-dd"),
applicant = applicantLoginName,
applicantComp = "国联民生证券总部",
htlx = 0,//合同类型 0业务 1非业务
ywlx = 1,
//业务类型 0资产管理业务金融产品筹设及调整事项合同(含ABS业务合同) 1金融交易类合同(除股票质押式回购业务部分购回、购回协议) 4采购类 3工会类 7党建活动类 2其他 6股票质押式回购业务部分购回、购回协议 8代销协议
htmj = 0,
htmb = 0,
//合同密级 0一般 1秘密 2绝密
qysdzz = "2809700652352016752",
yyr = "yangx," + applicantLoginName,
hehj = 0,
//合同缓急 0一般 1紧急 2特急
//去除后缀
htmc = Path.GetFileNameWithoutExtension(tdoc.FileName),//合同名称
sfgljy = 1,//是否关联交易 0是 1否
jflx = 0,//甲方类型 0企业 1个人
jf = trade.ClientName,//甲方
yyfs = 0,//用印方式 0电子章 1物理章
yflx = 0,//乙方类型 0企业 1个人
yf = "国联民生证券股份有限公司",//乙方
sfdxtzdfqsjf = 1,//是否短信通知对方签署 0是 1否
sfdxtzyf = 1,//是否短信通知乙方 0是 1否
htje = trade.OriginalStockEqvNotional,//合同金额
htyxksrq = trade.StartDate?.ToString("yyyy-MM-dd"),//合同有效期开始日期
htyxjsrq = trade.ExerciseDate?.ToString("yyyy-MM-dd"),//合同有效期结束日期
bz = "人民币",//币种
htqdrq = DateTime.Now.ToString("yyyy-MM-dd"),//合同签订日期
sfqdljxy = 2,
//是否签订廉洁协议 0已签 1未签 2无需签订
htbjjs = $"我司与{trade.ClientName}已完成NAFMII协议签署,并开展TRS交易:{contractCode},参与标的{trade.UnderlyingCode} {trade.UnderlyingName},参与标的数量面额{trade.OriginalStockEqvNotional}元人民币",
//合同背景介绍
detailList = detailList.Any() ? detailList : null,
creater = applicantLoginName
};
// 构建查询参数(不包含dataMap,dataMap放在请求体中)
// 注意:Java后端使用驼峰命名,参数名需与后端@RequestParam注解的名称一致
var query =
$"flowId={Uri.EscapeDataString(flowId)}" +
$"&isnextflow={Uri.EscapeDataString(isNextFlow ?? string.Empty)}" +
$"&systemToken={Uri.EscapeDataString(systemToken)}" +
$"&systemName={Uri.EscapeDataString(systemName ?? string.Empty)}";
var requestUrl = subUrl.Contains("?") ? $"{subUrl}&{query}" : $"{subUrl}?{query}";
logger.Info($"国联OA接口URL: {requestUrl}");
// 调用OA接口 (POST请求,dataMap放在请求体中)
string response = null;
try
{
// 设置较长的超时时间,默认15秒可能不够
var httpClient = new HttpClientWrap(baseUrl, 60); // 60秒超时
response = httpClient.PostJson(requestUrl, dataMap, null);
logger.Info($"国联OA接口返回: {response}");
// 解析响应
var responseObj = JsonHelper.Deserialize<GuoLianOAResponse>(response);
return responseObj;
}
catch (TaskCanceledException ex)
{
logger.Error($"调用OA接口时任务被取消:{ex.Message}", ex);
return new GuoLianOAResponse() { code = -2, data = new GuoLianOAData { msg = "调用OA接口超时,请检查网络连接" } };
}
catch (Exception ex)
{
logger.Error($"调用OA接口时发生异常:{ex.Message},响应内容:{response}", ex);
return new GuoLianOAResponse() { code = -2, data = new GuoLianOAData { msg = $"调用OA接口时发生异常: {ex.Message}" } };
}
}
catch (Exception ex)
{
logger.Error($"调用OA接口时发生异常,交易ID{trade.id},异常信息:{ex.Message}", ex);
return new GuoLianOAResponse() { code = -2, data = new GuoLianOAData { msg = $"调用OA接口时发生异常: {ex.Message}" } };
}
}
/// <summary>
/// 批量查询并同步 OA 用印状态
/// </summary>
/// <returns>处理结果列表</returns>
public List<TradeOAResult> SyncOAStampStatus()
{
var results = new List<TradeOAResult>();
// 1. 查询提交成功且有效的 OA 记录
var pendingOAs = DbContext.tradeContractOaResult
.Where(x => x.is_valid && x.status == "提交成功" && !string.IsNullOrEmpty(x.oa_fileid))
.ToList();
if (!pendingOAs.Any())
{
logger.Info("没有待同步的OA用印记录");
return results;
}
logger.Info($"找到 {pendingOAs.Count} 条待同步OA用印记录");
foreach (var oa in pendingOAs)
{
try
{
// 2. 调用 OA 查询接口
var queryResult = QueryOAStatus(oa.oa_fileid);
if (queryResult == null)
{
logger.Info($"OA查询返回空,requestid:{oa.oa_fileid}");
continue;
}
if (queryResult.code != 0)
{
logger.Info($"OA查询失败,requestid:{oa.oa_fileid}, msg:{queryResult.msg}");
continue;
}
// 从 data 数组中找到匹配的 requestId
var flowData = queryResult.data?.FirstOrDefault(d => d.requestId == oa.oa_fileid);
if (flowData == null)
{
logger.Info($"OA查询结果中未找到匹配记录,requestid:{oa.oa_fileid}");
continue;
}
// 3. 判断用印是否完成:检查 detailMap 中是否有用印文件
if (!HasStampFiles(flowData.flowInfos))
{
logger.Info($"OA用印未完成,detailMap 中无文件信息,requestid:{oa.oa_fileid}");
continue;
}
// 4. 获取用印文件并同步
var syncResult = SyncStampFileToDoc(oa, flowData);
results.Add(syncResult);
}
catch (Exception ex)
{
logger.Error($"同步OA状态失败,OA ID:{oa.id}, requestid:{oa.oa_fileid}, 错误:{ex.Message}", ex);
results.Add(new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = $"同步失败: {ex.Message}" });
}
}
logger.Info($"OA用印同步完成,共处理 {results.Count} 条,成功 {results.Count(r => r.Success)} 条");
return results;
}
/// <summary>
/// 查询 OA 审批状态
/// </summary>
/// <param name="requestId">OA请求ID</param>
/// <returns>查询结果</returns>
private OAStatusQueryResponse QueryOAStatus(string requestId)
{
try
{
var oaConfigSection = AppManager.GetConfiguration().GetSection("oa_confg");
var baseUrl = oaConfigSection["BaseUrl"];
var queryUrl = oaConfigSection["QueryUrl"];
var systemToken = oaConfigSection["SystemToken"];
var systemName = oaConfigSection["SystemName"];
if (string.IsNullOrEmpty(baseUrl) || string.IsNullOrEmpty(queryUrl))
{
logger.Info("OA查询接口配置不完整");
return null;
}
var requestParam = $"{queryUrl}?requestId={Uri.EscapeDataString(requestId)}&systemToken={Uri.EscapeDataString(systemToken)}&systemName={Uri.EscapeDataString(systemName ?? string.Empty)}";
var httpClient = new HttpClientWrap(baseUrl, 30);
var response = httpClient.Get(requestParam, null);
logger.Info($"查询OA用印状态返回: {response}");
if (string.IsNullOrEmpty(response))
{
return null;
}
return JsonHelper.Deserialize<OAStatusQueryResponse>(response);
}
catch (Exception ex)
{
logger.Error($"查询OA状态异常,requestid:{requestId}, 错误:{ex.Message}", ex);
return null;
}
}
/// <summary>
/// 通过 OA 接口下载用印文件
/// </summary>
/// <param name="fileId">文件 ID</param>
/// <returns>文件字节数组</returns>
private byte[] DownloadOAFile(string fileId)
{
try
{
if (string.IsNullOrEmpty(fileId))
{
return null;
}
var oaConfigSection = AppManager.GetConfiguration().GetSection("oa_confg");
var baseUrl = oaConfigSection["BaseUrl"];
var downloadFileUrl = oaConfigSection["DownloadFileUrl"];
var systemToken = oaConfigSection["SystemToken"];
var systemName = oaConfigSection["SystemName"];
if (string.IsNullOrEmpty(baseUrl) || string.IsNullOrEmpty(downloadFileUrl))
{
logger.Info("OA下载接口配置不完整");
return null;
}
var requestUrl = $"{baseUrl.TrimEnd('/')}/{downloadFileUrl.TrimStart('/')}?fileid={Uri.EscapeDataString(fileId)}&systemToken={Uri.EscapeDataString(systemToken)}&systemName={Uri.EscapeDataString(systemName ?? string.Empty)}";
logger.Info($"通过OA接口下载用印文件: fileid={fileId}");
using (var httpClient = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromMinutes(2) })
{
var response = httpClient.GetAsync(requestUrl).Result;
var fileBytes = response.Content.ReadAsByteArrayAsync().Result;
// 检查返回的是否为错误 JSON
if (response.Content.Headers.ContentType?.MediaType == "application/json" ||
(fileBytes.Length > 0 && fileBytes[0] == (byte)'{'))
{
var responseStr = System.Text.Encoding.UTF8.GetString(fileBytes);
logger.Error($"下载用印文件接口返回错误: {responseStr}");
return null;
}
logger.Info($"通过OA接口下载用印文件成功,大小: {fileBytes.Length} 字节");
return fileBytes;
}
}
catch (Exception ex)
{
logger.Error($"通过OA接口下载用印文件失败: fileid={fileId}, 错误:{ex.Message}", ex);
return null;
}
}
/// <summary>
/// 将 OA 用印文件同步到确认书记录
/// </summary>
/// <param name="oa">OA结果记录</param>
/// <param name="queryData">OA查询返回的流程数据</param>
/// <returns>处理结果</returns>
private TradeOAResult SyncStampFileToDoc(trade_contract_oa_result oa, OAFlowData queryData)
{
try
{
// 根据 contract_code 查找确认书文档(直接使用基类 DbContext)
var contractDoc = DbContext.trade_contract_document.FirstOrDefault(d => d.Code == oa.contract_code);
if (contractDoc == null)
{
logger.Info($"未找到确认书文档,contract_code:{oa.contract_code}");
return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = "未找到确认书文档" };
}
// 从 fjFileInfo 中提取用印文件信息
if (!TryExtractFileFromFjFileInfo(queryData.flowInfos, out var fileId, out var fileName))
{
logger.Info($"无法从 fjFileInfo 中提取用印文件信息,contract_code:{oa.contract_code}");
return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = "无法提取用印文件信息" };
}
// 通过 OA 接口下载用印文件
var fileBytes = DownloadOAFile(fileId);
if (fileBytes == null || fileBytes.Length == 0)
{
logger.Info($"未获取到用印文件,contract_code:{oa.contract_code}");
return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = "未获取到用印文件" };
}
// 文件名安全过滤:防止路径遍历
fileName = SanitizeFileName(fileName);
if (string.IsNullOrEmpty(fileName))
{
fileName = $"{oa.contract_code}_用印.pdf";
}
// 保存用印文件到本地
var tradeNumber = "未知";
var contractR = DbContext.trade_contract_r.FirstOrDefault(r => r.ContractCode == oa.contract_code && r.IsValid);
if (contractR != null)
{
tradeNumber = contractR.TradeNumber;
}
var filePath = Path.Combine(OtcAppContext.AppDocsPath, "contractdoc", PS.Config.Company.ToString(), tradeNumber);
if (!Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}
var stampFileFullName = Path.Combine(filePath, fileName);
if (File.Exists(stampFileFullName))
{
File.Delete(stampFileFullName);
}
File.WriteAllBytes(stampFileFullName, fileBytes);
logger.Info($"用印文件保存成功: {stampFileFullName}");
// 更新确认书文档状态
contractDoc.StampDocumentFileName = stampFileFullName.Replace(OtcAppContext.AppDocsPath, "/App_Docs").Replace("\\", "/");
// 根据 SourceType 判断用印状态:null或1=我方用印,2=双方用印
var sourceType = contractDoc.SourceType ?? 1;
if (sourceType == 1)
{
contractDoc.Status = ContractStatusEnum.OurPartyStamped; // 我方用印
}
else if (sourceType == 2)
{
contractDoc.Status = ContractStatusEnum.BothPartyStamped; // 双方用印
}
contractDoc.OptDate = DateTime.Now;
// 更新 OA 结果状态(oa 本身由基类 DbContext 追踪,直接修改即可)
oa.status = "用印完成";
oa.OptDate = DateTime.Now;
DbContext.SaveChanges();
logger.Info($"确认书 {oa.contract_code} 用印状态已更新为: {contractDoc.Status}");
return new TradeOAResult { TradeId = oa.trade_id, Success = true, Message = $"用印同步成功,状态: {contractDoc.Status}" };
}
catch (Exception ex)
{
logger.Error($"同步用印文件失败,contract_code:{oa.contract_code}, 错误:{ex.Message}", ex);
return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = $"同步失败: {ex.Message}" };
}
}
/// <summary>
/// 判断 flowInfos 中是否存在用印文件
/// </summary>
private bool HasStampFiles(Newtonsoft.Json.Linq.JObject flowInfos)
{
var fjFileInfo = flowInfos?["fjFileInfo"];
return fjFileInfo != null && fjFileInfo.Any();
}
/// <summary>
/// 从 fjFileInfo 中提取用印文件 ID 和文件名
/// </summary>
private bool TryExtractFileFromFjFileInfo(Newtonsoft.Json.Linq.JObject flowInfos, out string fileId, out string fileName)
{
fileId = null;
fileName = null;
var fjFileInfo = flowInfos?["fjFileInfo"];
if (fjFileInfo == null || !fjFileInfo.Any())
{
return false;
}
var firstFile = fjFileInfo.FirstOrDefault();
if (firstFile != null)
{
fileId = firstFile["id"]?.ToString();
fileName = firstFile["fileName"]?.ToString();
if (!string.IsNullOrEmpty(fileId))
{
return true;
}
}
return false;
}
/// <summary>
/// 过滤文件名中的非法字符,防止路径遍历
/// </summary>
private string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
{
return null;
}
// 仅保留纯文件名,丢弃路径
fileName = Path.GetFileName(fileName);
// 替换 Windows 非法字符
var invalidChars = Path.GetInvalidFileNameChars();
foreach (var c in invalidChars)
{
fileName = fileName.Replace(c, '_');
}
// 防御 .. 路径遍历
fileName = fileName.Replace("..", "_");
return fileName;
}
}
}