OA用印自动同步,完善确认书来源标识

This commit is contained in:
锦麟 王
2026-04-16 15:12:39 +08:00
parent 8fc2f43d4d
commit e3d2feedb2
14 changed files with 458 additions and 5 deletions
@@ -497,6 +497,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
contractRecord.SellerName = isGJBuyer ? trade.ClientName : PS.Config.CompanyFullName;
contractRecord.FileType = FileTypeEnum.Word;
contractRecord.Status = ContractStatusEnum.Draft;
contractRecord.SourceType = 1; // 系统生成
contractRecord.Paths = $"App_Docs\\GTJA\\{outPutFileName}";
contractRecord.FileName = outPutFileName;
contractRecord.Type = ContractTypeEnum.Trade;
@@ -58,6 +58,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
contractRecord.ContractVersion = contractVersion;
contractRecord.FileType = FileTypeEnum.Pdf;
contractRecord.Status = ContractStatusEnum.Draft;
contractRecord.SourceType = 1; // 系统生成
contractRecord.Paths = outPath;
contractRecord.FileName = outPath.Replace(".xlsx", ".pdf");
contractRecord.Type = ContractTypeEnum.Trade;
@@ -545,6 +545,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
contractRecord.SellerName = isGJBuyer ? firstTrade.ClientName : (PS.Config.CompanyName ?? "");
contractRecord.FileType = fileType;
contractRecord.Status = ContractStatusEnum.Draft;
contractRecord.SourceType = 1; // 系统生成
contractRecord.Paths = "/App_Docs/contractdoc/output/" + fileName;
contractRecord.FileName = fileName;
contractRecord.StampDocumentFileName = null;
@@ -697,6 +698,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
contractRecord.SellerName = isGJBuyer ? firstTrade.ClientName : (PS.Config.CompanyName ?? "");
contractRecord.FileType = fileType;
contractRecord.Status = ContractStatusEnum.Draft;
contractRecord.SourceType = 1; // 系统生成
contractRecord.Paths = "/App_Docs/contractdoc/output/" + fileName;
contractRecord.FileName = fileName;
contractRecord.StampDocumentFileName = null;
@@ -611,6 +611,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
contractRecord.SellerName = isGJBuyer ? trade.ClientName : PS.Config.CompanyFullName;
contractRecord.FileType = FileTypeEnum.Word;
contractRecord.Status = ContractStatusEnum.Draft;
contractRecord.SourceType = 1; // 系统生成
contractRecord.Paths = $"App_Docs\\GTJA\\{outPutFileName}";
contractRecord.FileName = outPutFileName;
contractRecord.Type = ContractTypeEnum.Trade;
@@ -800,6 +801,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
#region
contractDoc.Comments = fileDescription;
contractDoc.SourceType = 2; // 人工上传
if (OurpartySeal && CounterpartySeal)
{ contractDoc.Status = ContractStatusEnum.BothPartyStamped; }
else if (OurpartySeal) { contractDoc.Status = ContractStatusEnum.OurPartyStamped; }
@@ -922,6 +924,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
}
#region
contractDoc.SourceType = 2; // 人工上传
var confrimService = new TradeConfirmService(OptUser, db);
foreach (var contractDocR in contractDocRArr)
{
+21
View File
@@ -1,3 +1,4 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
@@ -61,4 +62,24 @@ namespace YLErp.Modules.TradeModule
public string annexId { get; set; }
public string annexName { get; set; }
}
/// <summary>
/// OA 审批状态查询响应模型(适配新查询接口 /gateway/oaflow/getRequestData
/// </summary>
public class OAStatusQueryResponse
{
public int code { get; set; }
public string status { get; set; }
public string msg { get; set; }
public List<OAFlowData> data { get; set; }
}
/// <summary>
/// OA 流程查询数据模型
/// </summary>
public class OAFlowData
{
public string requestId { get; set; }
public JObject flowInfos { get; set; }
}
}
@@ -18,6 +18,10 @@ namespace YLErp.Modules.TradeModule
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)
{
@@ -332,6 +336,314 @@ namespace YLErp.Modules.TradeModule
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. 判断用印是否完成:fj 字段为空表示未盖完章,有值表示已用印
var fj = flowData.flowInfos?["fj"];
if (fj == null || fj.Type == Newtonsoft.Json.Linq.JTokenType.Null)
{
logger.Info($"OA用印未完成,fj为空,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="fileUrl">文件下载地址</param>
/// <returns>文件字节数组</returns>
private byte[] DownloadOAFile(string fileUrl)
{
try
{
if (string.IsNullOrEmpty(fileUrl))
{
return null;
}
logger.Info($"下载OA用印文件: {fileUrl}");
var response = _httpClient.GetAsync(fileUrl).Result;
response.EnsureSuccessStatusCode();
var fileBytes = response.Content.ReadAsByteArrayAsync().Result;
logger.Info($"下载OA用印文件成功,大小: {fileBytes.Length} 字节");
return fileBytes;
}
catch (Exception ex)
{
logger.Error($"下载OA用印文件失败: {fileUrl}, 错误:{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 = "未找到确认书文档" };
}
// 从 fj 字段提取用印文件信息
var fj = queryData.flowInfos?["fj"];
if (!TryExtractFileFromFj(fj, out var fileUrl, out var fileName))
{
logger.Info($"无法从 fj 中提取用印文件信息,contract_code:{oa.contract_code}");
return new TradeOAResult { TradeId = oa.trade_id, Success = false, Message = "无法提取用印文件信息" };
}
// 下载用印文件
var fileBytes = DownloadOAFile(fileUrl);
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>
/// 从 fj 字段尝试提取文件下载地址和文件名
/// fj 格式:{"annexId":"url1,url2","annexName":"name1,name2"}
/// </summary>
private bool TryExtractFileFromFj(Newtonsoft.Json.Linq.JToken fj, out string fileUrl, out string fileName)
{
fileUrl = null;
fileName = null;
if (fj == null || fj.Type == Newtonsoft.Json.Linq.JTokenType.Null)
{
return false;
}
// 如果是字符串,直接当作 URL
if (fj.Type == Newtonsoft.Json.Linq.JTokenType.String)
{
fileUrl = fj.ToString();
fileName = Path.GetFileName(fileUrl);
return !string.IsNullOrEmpty(fileUrl);
}
// 解析 annexId(逗号分隔的 URL 数组)和 annexName(逗号分隔的文件名数组)
var annexIdStr = fj["annexId"]?.ToString();
var annexNameStr = fj["annexName"]?.ToString();
if (!string.IsNullOrEmpty(annexIdStr))
{
fileUrl = annexIdStr.Split(',').Select(s => s.Trim()).FirstOrDefault(s => !string.IsNullOrEmpty(s));
}
if (!string.IsNullOrEmpty(annexNameStr))
{
fileName = annexNameStr.Split(',').Select(s => s.Trim()).FirstOrDefault(s => !string.IsNullOrEmpty(s));
}
if (string.IsNullOrEmpty(fileName) && !string.IsNullOrEmpty(fileUrl))
{
fileName = Path.GetFileName(fileUrl);
}
return !string.IsNullOrEmpty(fileUrl);
}
/// <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;
}
}
}