1285 lines
51 KiB
C#
1285 lines
51 KiB
C#
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
|
||
namespace SimpleAdmin.Application;
|
||
|
||
/// <summary>
|
||
/// 实现入职合同“员工先签、企业自动盖章”的契约锁电子签流程。
|
||
/// </summary>
|
||
public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||
{
|
||
private readonly SqlSugarScope _db;
|
||
private readonly IQiyuesuoClient _qiyuesuoClient;
|
||
private readonly IOnboardingContractDocumentService _contractDocumentService;
|
||
private readonly IFileService _fileService;
|
||
private readonly IConfigService _configService;
|
||
private readonly QiyuesuoSettingOptions _setting;
|
||
|
||
public OnboardingElectronicSignService(
|
||
IQiyuesuoClient qiyuesuoClient,
|
||
IOnboardingContractDocumentService contractDocumentService,
|
||
IFileService fileService,
|
||
IConfigService configService)
|
||
{
|
||
_db = DbContext.DB;
|
||
_qiyuesuoClient = qiyuesuoClient;
|
||
_contractDocumentService = contractDocumentService;
|
||
_fileService = fileService;
|
||
_configService = configService;
|
||
_setting = App.GetConfig<QiyuesuoSettingOptions>(
|
||
"QiyuesuoSetting",
|
||
true)
|
||
?? new QiyuesuoSettingOptions();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<OnboardingElectronicSignDetailOutput> Detail(
|
||
BaseIdInput input)
|
||
{
|
||
var task = await GetAuthorizedTask(input.Id);
|
||
return await BuildDetail(task);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<OnboardingElectronicSignDetailOutput> Initiate(
|
||
OnboardingElectronicSignTaskInput input)
|
||
{
|
||
var task = await GetAuthorizedTask(input.TaskId);
|
||
ValidateTaskCanSign(task);
|
||
ValidateSettings();
|
||
|
||
var process = await GetLatestProcess(task.Id);
|
||
var isNewProcess = process == null;
|
||
var creatingAt = process?.UpdateTime ?? process?.StartedAt;
|
||
if (process?.SignStatus == ElectronicSignStatus.Creating
|
||
&& creatingAt > DateTime.Now.AddMinutes(-5))
|
||
{
|
||
return await BuildDetail(task, process);
|
||
}
|
||
if (process != null
|
||
&& process.SignStatus != ElectronicSignStatus.Failed
|
||
&& process.SignStatus != ElectronicSignStatus.Creating
|
||
&& process.SignStatus != ElectronicSignStatus.Draft)
|
||
{
|
||
return await BuildDetail(task, process);
|
||
}
|
||
|
||
var context = await GetInitiationContext(task);
|
||
if (process == null)
|
||
{
|
||
var created = await CreateProcess(task, context.Employee);
|
||
process = created.Process;
|
||
isNewProcess = created.Created;
|
||
}
|
||
|
||
return await StartProcess(
|
||
task,
|
||
context.CompanyName,
|
||
context.TaskContracts,
|
||
process,
|
||
isNewProcess);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<OnboardingElectronicSignDetailOutput> Reinitiate(
|
||
OnboardingElectronicSignReinitiateInput input)
|
||
{
|
||
var task = await GetAuthorizedTask(input.TaskId);
|
||
ValidateTaskCanSign(task);
|
||
ValidateSettings();
|
||
|
||
var previousProcess = await GetLatestProcess(task.Id)
|
||
?? throw Oops.Bah("该入职任务尚未发起电子签,无需重新发起");
|
||
ValidateProcessCanReinitiate(previousProcess);
|
||
|
||
var context = await GetInitiationContext(task);
|
||
var process = await CreateReplacementProcess(
|
||
task,
|
||
context.Employee,
|
||
previousProcess,
|
||
input.Reason.Trim());
|
||
|
||
return await StartProcess(
|
||
task,
|
||
context.CompanyName,
|
||
context.TaskContracts,
|
||
process,
|
||
true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载并校验电子签发起所需的员工、公司和合同数据。
|
||
/// </summary>
|
||
private async Task<(
|
||
ObEmployee Employee,
|
||
string CompanyName,
|
||
List<ObTaskContract> TaskContracts)> GetInitiationContext(
|
||
ObOnboardingTask task)
|
||
{
|
||
var employee = await _db.Queryable<ObEmployee>()
|
||
.InSingleAsync(task.EmployeeId.Value)
|
||
?? throw Oops.Bah("正式员工档案不存在,不能发起电子签");
|
||
if (string.IsNullOrWhiteSpace(employee.EmployeeName))
|
||
throw Oops.Bah("正式员工姓名为空,不能发起电子签");
|
||
if (string.IsNullOrWhiteSpace(employee.Phone))
|
||
throw Oops.Bah("正式员工手机号为空,不能发起电子签");
|
||
|
||
var employeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
||
.FirstAsync(item => item.TaskId == task.Id)
|
||
?? throw Oops.Bah("审核通过的入职登记资料不存在,不能发起电子签");
|
||
ValidateSignerIdentity(employee, employeeInfo);
|
||
|
||
var company = await _db.Queryable<ObCompany>()
|
||
.InSingleAsync(task.CompanyId)
|
||
?? throw Oops.Bah("任务所属公司不存在,不能发起电子签");
|
||
var companyName = string.IsNullOrWhiteSpace(company.LegalName)
|
||
? company.CompanyName
|
||
: company.LegalName;
|
||
companyName = companyName?.Trim();
|
||
if (string.IsNullOrWhiteSpace(companyName))
|
||
throw Oops.Bah("任务所属公司法定名称为空,不能发起电子签");
|
||
|
||
var taskContracts = await _db.Queryable<ObTaskContract>()
|
||
.Where(item => item.TaskId == task.Id)
|
||
.OrderBy(item => item.Id)
|
||
.ToListAsync();
|
||
if (taskContracts.Count == 0)
|
||
throw Oops.Bah("入职任务没有可发起签约的合同");
|
||
|
||
var unconfirmed = taskContracts.FirstOrDefault(item =>
|
||
item.Required && !item.ConfirmedAt.HasValue);
|
||
if (unconfirmed != null)
|
||
throw Oops.Bah($"合同“{unconfirmed.ContractName}”尚未由员工确认");
|
||
|
||
return (employee, companyName, taskContracts);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从本地断点创建或继续契约锁流程,并统一保存失败原因。
|
||
/// </summary>
|
||
private async Task<OnboardingElectronicSignDetailOutput> StartProcess(
|
||
ObOnboardingTask task,
|
||
string companyName,
|
||
List<ObTaskContract> taskContracts,
|
||
ObElectronicSignProcess process,
|
||
bool isNewProcess)
|
||
{
|
||
var creatingAt = process.UpdateTime ?? process.StartedAt;
|
||
if (!isNewProcess
|
||
&& process.SignStatus == ElectronicSignStatus.Creating
|
||
&& creatingAt > DateTime.Now.AddMinutes(-5))
|
||
{
|
||
return await BuildDetail(task, process);
|
||
}
|
||
if (!isNewProcess)
|
||
{
|
||
var originalStatus = process.SignStatus;
|
||
var originalUpdateTime = process.UpdateTime;
|
||
var claimedAt = DateTime.Now;
|
||
var claimedRows = await _db.Updateable<ObElectronicSignProcess>()
|
||
.SetColumns(item => new ObElectronicSignProcess
|
||
{
|
||
SignStatus = ElectronicSignStatus.Creating,
|
||
LastError = null,
|
||
UpdateTime = claimedAt,
|
||
UpdateUserId = UserManager.UserId,
|
||
UpdateUser = UserManager.Name
|
||
})
|
||
.Where(item => item.Id == process.Id
|
||
&& item.IsCurrent == true
|
||
&& item.SignStatus == originalStatus
|
||
&& item.UpdateTime == originalUpdateTime)
|
||
.ExecuteCommandAsync();
|
||
if (claimedRows != 1)
|
||
throw Oops.Bah("电子签流程正在由其他请求处理,请稍后刷新");
|
||
|
||
process.SignStatus = ElectronicSignStatus.Creating;
|
||
process.LastError = null;
|
||
process.UpdateTime = claimedAt;
|
||
process.UpdateUserId = UserManager.UserId;
|
||
process.UpdateUser = UserManager.Name;
|
||
}
|
||
|
||
try
|
||
{
|
||
await StartOrResumeProcess(
|
||
task,
|
||
companyName,
|
||
taskContracts,
|
||
process);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
// 第三方已经完成签署、仅归档下载失败时,保留“已完成”状态,方便后续通过同步重试归档。
|
||
process.SignStatus = process.CompletedAt.HasValue
|
||
? ElectronicSignStatus.Completed
|
||
: ElectronicSignStatus.Failed;
|
||
process.LastError = LimitError(exception.Message);
|
||
process.RetryCount += 1;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
throw;
|
||
}
|
||
|
||
return await BuildDetail(task, process);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<OnboardingElectronicSignDetailOutput> Sync(
|
||
OnboardingElectronicSignTaskInput input)
|
||
{
|
||
var task = await GetAuthorizedTask(input.TaskId);
|
||
var process = await GetRequiredProcess(task.Id);
|
||
await SyncProcess(process, "manual_sync");
|
||
return await BuildDetail(task, process);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<OnboardingElectronicSignDetailOutput> GetSignLink(
|
||
OnboardingElectronicSignTaskInput input)
|
||
{
|
||
var task = await GetAuthorizedTask(input.TaskId);
|
||
var process = await GetRequiredProcess(task.Id);
|
||
EnsureCanOpenEmployeeSignPage(process);
|
||
|
||
var linkStillValid = !string.IsNullOrWhiteSpace(process.SignUrl)
|
||
&& process.SignUrlExpireAt > DateTime.Now.AddMinutes(1);
|
||
if (!linkStillValid)
|
||
{
|
||
process.SignUrl = await CreateSignPageUrl(process);
|
||
process.SignUrlExpireAt = DateTime.Now.AddMinutes(
|
||
NormalizeSignUrlExpireMinutes());
|
||
process.LastError = null;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
}
|
||
|
||
return await BuildDetail(task, process);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task Notice(OnboardingElectronicSignTaskInput input)
|
||
{
|
||
var task = await GetAuthorizedTask(input.TaskId);
|
||
var process = await GetRequiredProcess(task.Id);
|
||
if (process.SignStatus != ElectronicSignStatus.PendingEmployeeSign)
|
||
throw Oops.Bah("当前流程不在员工待签状态,无需催签");
|
||
|
||
await _qiyuesuoClient.Notice(process.ProviderContractId);
|
||
process.LastError = null;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task Invalid(OnboardingElectronicSignInvalidInput input)
|
||
{
|
||
var task = await GetAuthorizedTask(input.TaskId);
|
||
var process = await GetRequiredProcess(task.Id);
|
||
if (IsTerminal(process.SignStatus))
|
||
throw Oops.Bah("已完成、已归档或已结束的电子签流程不能作废");
|
||
if (string.IsNullOrWhiteSpace(process.ProviderContractId))
|
||
throw Oops.Bah("契约锁流程尚未创建,不能调用作废接口");
|
||
|
||
await _qiyuesuoClient.Invalid(
|
||
process.ProviderContractId,
|
||
input.Reason.Trim());
|
||
process.SignStatus = ElectronicSignStatus.Invalid;
|
||
process.ProviderStatus = "INVALIDED";
|
||
process.SignUrl = null;
|
||
process.SignUrlExpireAt = null;
|
||
process.LastError = null;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task HandleCallback(string payloadJson)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(payloadJson))
|
||
throw Oops.Bah("契约锁回调内容为空");
|
||
|
||
using var json = JsonDocument.Parse(payloadJson);
|
||
var root = json.RootElement;
|
||
var payloadHash = Convert.ToHexString(
|
||
SHA256.HashData(Encoding.UTF8.GetBytes(payloadJson)))
|
||
.ToLowerInvariant();
|
||
var callbackEvent = await _db.Queryable<ObElectronicSignEvent>()
|
||
.FirstAsync(item => item.PayloadHash == payloadHash);
|
||
if (callbackEvent?.ProcessResult == "处理成功")
|
||
return;
|
||
|
||
var providerContractId = FindString(root, "contractId");
|
||
var bizId = FindString(root, "bizId");
|
||
var eventType = FindString(
|
||
root,
|
||
"callbackType",
|
||
"eventType",
|
||
"type");
|
||
var providerStatus = FindString(
|
||
root,
|
||
"contractStatus",
|
||
"status");
|
||
var process = await FindProcess(providerContractId, bizId);
|
||
if (callbackEvent == null)
|
||
{
|
||
callbackEvent = new ObElectronicSignEvent
|
||
{
|
||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||
PayloadHash = payloadHash,
|
||
EventSource = "callback",
|
||
ReceivedAt = DateTime.Now,
|
||
CreateTime = DateTime.Now,
|
||
IsDelete = false
|
||
};
|
||
try
|
||
{
|
||
await _db.Insertable(callbackEvent).ExecuteCommandAsync();
|
||
}
|
||
catch
|
||
{
|
||
// 两次相同回调并发到达时,由唯一索引保证只保留一条事件记录。
|
||
callbackEvent = await _db.Queryable<ObElectronicSignEvent>()
|
||
.FirstAsync(item => item.PayloadHash == payloadHash);
|
||
if (callbackEvent == null)
|
||
throw;
|
||
if (callbackEvent.ProcessResult == "处理成功")
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 失败、未匹配的历史回调允许第三方用相同报文再次触发处理。
|
||
callbackEvent.SignProcessId = process?.Id;
|
||
callbackEvent.EventType = eventType;
|
||
callbackEvent.ProviderStatus = providerStatus;
|
||
callbackEvent.PayloadJson = payloadJson;
|
||
callbackEvent.ProcessResult = process == null ? "未匹配到本地电子签流程" : "处理中";
|
||
callbackEvent.ProcessedAt = null;
|
||
await _db.Updateable(callbackEvent).ExecuteCommandAsync();
|
||
if (process == null)
|
||
{
|
||
callbackEvent.ProcessedAt = DateTime.Now;
|
||
await _db.Updateable(callbackEvent).ExecuteCommandAsync();
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
process.LastCallbackAt = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
// 明文回调只作为刷新触发器,最终状态重新向契约锁查询,伪造回调不能改变业务状态。
|
||
await SyncProcess(process, "callback");
|
||
callbackEvent.ProviderStatus = process.ProviderStatus;
|
||
callbackEvent.ProcessResult = "处理成功";
|
||
callbackEvent.ProcessedAt = DateTime.Now;
|
||
await _db.Updateable(callbackEvent).ExecuteCommandAsync();
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
callbackEvent.ProcessResult = LimitError(exception.Message, 480);
|
||
callbackEvent.ProcessedAt = DateTime.Now;
|
||
await _db.Updateable(callbackEvent).ExecuteCommandAsync();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建任务的第一条本地电子签流程。
|
||
/// 当前流程唯一索引负责拦截并发双击,BizId 使用流程 Id 保证不会碰撞第三方历史合同。
|
||
/// </summary>
|
||
private async Task<(ObElectronicSignProcess Process, bool Created)> CreateProcess(
|
||
ObOnboardingTask task,
|
||
ObEmployee employee)
|
||
{
|
||
var process = BuildProcess(task, employee);
|
||
try
|
||
{
|
||
await _db.Insertable(process).ExecuteCommandAsync();
|
||
return (process, true);
|
||
}
|
||
catch
|
||
{
|
||
// 并发请求可能已经插入当前流程;读取胜出的记录,确保不会创建第二个收费流程。
|
||
var existing = await GetLatestProcess(task.Id);
|
||
if (existing != null)
|
||
return (existing, false);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将已经结束的当前流程转为历史记录,并在同一事务中创建新的当前流程。
|
||
/// 数据库唯一索引保证多实例或连续点击时每个任务最多只有一条当前流程。
|
||
/// </summary>
|
||
private async Task<ObElectronicSignProcess> CreateReplacementProcess(
|
||
ObOnboardingTask task,
|
||
ObEmployee employee,
|
||
ObElectronicSignProcess previousProcess,
|
||
string reason)
|
||
{
|
||
var process = BuildProcess(task, employee);
|
||
var now = DateTime.Now;
|
||
|
||
await _db.Ado.BeginTranAsync();
|
||
try
|
||
{
|
||
var replacedRows = await _db.Updateable<ObElectronicSignProcess>()
|
||
.SetColumns(item => new ObElectronicSignProcess
|
||
{
|
||
IsCurrent = null,
|
||
ReissueReason = LimitError(reason, 480),
|
||
ReissuedAt = now,
|
||
ReissuedByProcessId = process.Id,
|
||
UpdateTime = now,
|
||
UpdateUserId = UserManager.UserId,
|
||
UpdateUser = UserManager.Name
|
||
})
|
||
.Where(item => item.Id == previousProcess.Id
|
||
&& item.IsCurrent == true
|
||
&& item.SignStatus == previousProcess.SignStatus)
|
||
.ExecuteCommandAsync();
|
||
if (replacedRows != 1)
|
||
throw Oops.Bah("电子签流程状态已经变化,请刷新后再重新发起");
|
||
|
||
await _db.Insertable(process).ExecuteCommandAsync();
|
||
await _db.Ado.CommitTranAsync();
|
||
return process;
|
||
}
|
||
catch
|
||
{
|
||
await _db.Ado.RollbackTranAsync();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建尚未发送到契约锁的当前流程快照。
|
||
/// </summary>
|
||
private ObElectronicSignProcess BuildProcess(
|
||
ObOnboardingTask task,
|
||
ObEmployee employee)
|
||
{
|
||
var now = DateTime.Now;
|
||
var processId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||
return new ObElectronicSignProcess
|
||
{
|
||
Id = processId,
|
||
TaskId = task.Id,
|
||
EmployeeId = employee.Id,
|
||
Provider = ElectronicSignProvider.Qiyuesuo,
|
||
BizId = $"{task.TaskNo}-SIGN-{processId}",
|
||
IsCurrent = true,
|
||
CategoryId = _setting.CategoryId.Trim(),
|
||
SealId = _setting.SealId.Trim(),
|
||
Subject = $"{employee.EmployeeName}-{task.TaskNo}-入职合同",
|
||
EmployeeName = employee.EmployeeName.Trim(),
|
||
EmployeeMobile = employee.Phone.Trim(),
|
||
EmployeeIdCardNumber = employee.IdCardNumber?.Trim(),
|
||
CreatorName = _setting.CreatorName.Trim(),
|
||
CreatorMobile = _setting.CreatorMobile.Trim(),
|
||
SignStatus = ElectronicSignStatus.Creating,
|
||
StartedAt = now,
|
||
Status = CommonStatusConst.ENABLE,
|
||
CreateTime = now,
|
||
CreateUserId = UserManager.UserId,
|
||
CreateUser = UserManager.Name,
|
||
IsDelete = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从本地断点继续创建草稿、上传全部合同并发起一次多文档签约流程。
|
||
/// </summary>
|
||
private async Task StartOrResumeProcess(
|
||
ObOnboardingTask task,
|
||
string companyName,
|
||
List<ObTaskContract> taskContracts,
|
||
ObElectronicSignProcess process)
|
||
{
|
||
foreach (var contract in taskContracts)
|
||
{
|
||
contract.GeneratedDocxFileId ??= await _contractDocumentService.Generate(
|
||
new OnboardingGenerateContractInput
|
||
{
|
||
TaskContractId = contract.Id
|
||
});
|
||
}
|
||
|
||
QiyuesuoContractResult providerContract;
|
||
if (string.IsNullOrWhiteSpace(process.ProviderContractId))
|
||
{
|
||
providerContract = await _qiyuesuoClient.CreateDraft(
|
||
new QiyuesuoCreateDraftInput
|
||
{
|
||
BizId = process.BizId,
|
||
Subject = process.Subject,
|
||
CompanyName = companyName,
|
||
EmployeeName = process.EmployeeName,
|
||
EmployeeMobile = process.EmployeeMobile,
|
||
CategoryId = process.CategoryId,
|
||
SealId = process.SealId,
|
||
CreatorName = process.CreatorName,
|
||
CreatorMobile = process.CreatorMobile,
|
||
CallbackUrl = _setting.CallbackUrl
|
||
});
|
||
process.ProviderContractId = GetContractId(providerContract);
|
||
process.ProviderStatus = providerContract.Status ?? "DRAFT";
|
||
// ProviderStatus 可为 DRAFT,但本地仍保持“正在创建”,避免并发点击重复上传或发送。
|
||
process.SignStatus = ElectronicSignStatus.Creating;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
}
|
||
else
|
||
{
|
||
providerContract = await _qiyuesuoClient.GetContractDetail(
|
||
process.ProviderContractId);
|
||
var providerState = ResolveSignStatus(providerContract);
|
||
if (providerState != ElectronicSignStatus.Draft)
|
||
{
|
||
await SyncProcess(process, "resume");
|
||
return;
|
||
}
|
||
}
|
||
|
||
var documents = await _db.Queryable<ObElectronicSignDocument>()
|
||
.Where(item => item.SignProcessId == process.Id)
|
||
.ToListAsync();
|
||
for (var index = 0; index < taskContracts.Count; index++)
|
||
{
|
||
var taskContract = taskContracts[index];
|
||
if (documents.Any(item => item.TaskContractId == taskContract.Id))
|
||
continue;
|
||
|
||
var originalFileId = taskContract.GeneratedDocxFileId.Value;
|
||
var fileBytes = await ReadFile(originalFileId);
|
||
var documentName = string.IsNullOrWhiteSpace(taskContract.ContractName)
|
||
? $"合同{index + 1}"
|
||
: taskContract.ContractName.Trim();
|
||
var uploaded = await _qiyuesuoClient.AddDocument(
|
||
new QiyuesuoAddDocumentInput
|
||
{
|
||
ContractId = process.ProviderContractId,
|
||
BizId = process.BizId,
|
||
Title = documentName,
|
||
DocumentSort = index + 1,
|
||
FileName = $"{SafeFileName(documentName)}.docx",
|
||
FileBytes = fileBytes
|
||
});
|
||
var document = new ObElectronicSignDocument
|
||
{
|
||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||
SignProcessId = process.Id,
|
||
TaskContractId = taskContract.Id,
|
||
ProviderDocumentId = GetDocumentId(uploaded),
|
||
DocumentName = documentName,
|
||
DocumentSort = index + 1,
|
||
OriginalDocxFileId = originalFileId,
|
||
EmployeeSignKeyword = _setting.EmployeeSignKeyword.Trim(),
|
||
CompanySealKeyword = _setting.CompanySealKeyword.Trim(),
|
||
Status = CommonStatusConst.ENABLE,
|
||
CreateTime = DateTime.Now,
|
||
CreateUserId = process.CreateUserId,
|
||
CreateUser = process.CreateUser,
|
||
IsDelete = false
|
||
};
|
||
await _db.Insertable(document).ExecuteCommandAsync();
|
||
documents.Add(document);
|
||
}
|
||
|
||
await SendProcess(process, companyName, documents);
|
||
await _db.Updateable<ObTaskContract>()
|
||
.SetColumns(item => item.ContractStatus == TaskContractStatus.ElectronicSigning)
|
||
.Where(item => item.TaskId == task.Id)
|
||
.ExecuteCommandAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为每份文档分别配置员工签名与企业盖章位置,然后一次性发送整个流程。
|
||
/// </summary>
|
||
private async Task SendProcess(
|
||
ObElectronicSignProcess process,
|
||
string companyName,
|
||
List<ObElectronicSignDocument> documents)
|
||
{
|
||
var providerContract = await _qiyuesuoClient.GetContractDetail(
|
||
process.ProviderContractId);
|
||
var personalSignatory = providerContract.Signatories.FirstOrDefault(item =>
|
||
string.Equals(item.TenantType, "PERSONAL", StringComparison.OrdinalIgnoreCase))
|
||
?? throw Oops.Bah("契约锁草稿未返回员工签署方");
|
||
var companySignatory = providerContract.Signatories.FirstOrDefault(item =>
|
||
string.Equals(item.TenantType, "COMPANY", StringComparison.OrdinalIgnoreCase))
|
||
?? throw Oops.Bah("契约锁草稿未返回企业签署方");
|
||
var companyAction = companySignatory.Actions.FirstOrDefault(item =>
|
||
string.Equals(item.Type, "COMPANY", StringComparison.OrdinalIgnoreCase))
|
||
?? throw Oops.Bah("契约锁草稿未返回企业自动盖章动作");
|
||
var personalSignatoryId = GetSignatoryId(personalSignatory);
|
||
var companyActionId = GetActionId(companyAction);
|
||
var stampers = new List<QiyuesuoStamperInput>();
|
||
foreach (var document in documents.OrderBy(item => item.DocumentSort))
|
||
{
|
||
document.EmployeeActionId = personalSignatoryId;
|
||
document.CompanyActionId = companyActionId;
|
||
document.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(document).ExecuteCommandAsync();
|
||
stampers.Add(new QiyuesuoStamperInput
|
||
{
|
||
SignatoryId = personalSignatoryId,
|
||
Type = "PERSONAL",
|
||
DocumentId = document.ProviderDocumentId,
|
||
Keyword = document.EmployeeSignKeyword,
|
||
KeywordIndex = Math.Max(1, _setting.EmployeeSignKeywordIndex)
|
||
});
|
||
stampers.Add(new QiyuesuoStamperInput
|
||
{
|
||
ActionId = companyActionId,
|
||
Type = "COMPANY",
|
||
DocumentId = document.ProviderDocumentId,
|
||
SealId = process.SealId,
|
||
Keyword = document.CompanySealKeyword,
|
||
KeywordIndex = Math.Max(1, _setting.CompanySealKeywordIndex)
|
||
});
|
||
}
|
||
|
||
var sent = await _qiyuesuoClient.Send(new QiyuesuoSendContractInput
|
||
{
|
||
ContractId = process.ProviderContractId,
|
||
BizId = process.BizId,
|
||
CompanyName = companyName,
|
||
Stampers = stampers
|
||
});
|
||
process.ProviderStatus = sent.Status ?? "SIGNING";
|
||
process.SignStatus = ElectronicSignStatus.PendingEmployeeSign;
|
||
process.SentAt ??= DateTime.Now;
|
||
process.LastSyncedAt = DateTime.Now;
|
||
process.LastError = null;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
|
||
// 签署页获取失败不回滚已收费流程,管理员可通过“刷新签署链接”安全重试。
|
||
try
|
||
{
|
||
process.SignUrl = await CreateSignPageUrl(process);
|
||
process.SignUrlExpireAt = DateTime.Now.AddMinutes(
|
||
NormalizeSignUrlExpireMinutes());
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
process.LastError = $"签约已发起,但签署链接获取失败:{LimitError(exception.Message)}";
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 主动查询第三方详情并更新本地状态。
|
||
/// </summary>
|
||
private async Task SyncProcess(
|
||
ObElectronicSignProcess process,
|
||
string eventSource)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(process.ProviderContractId))
|
||
throw Oops.Bah("契约锁流程尚未创建完成,可点击重新发起继续处理");
|
||
|
||
try
|
||
{
|
||
var providerContract = await _qiyuesuoClient.GetContractDetail(
|
||
process.ProviderContractId);
|
||
await ApplyProviderDetail(process, providerContract);
|
||
process.LastSyncedAt = DateTime.Now;
|
||
process.LastError = null;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
if (process.SignStatus == ElectronicSignStatus.Completed)
|
||
await ArchiveCompletedProcess(process);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
process.LastError = $"{eventSource}:{LimitError(exception.Message)}";
|
||
process.LastSyncedAt = DateTime.Now;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将契约锁状态转换为本系统可展示的业务状态。
|
||
/// </summary>
|
||
private async Task ApplyProviderDetail(
|
||
ObElectronicSignProcess process,
|
||
QiyuesuoContractResult providerContract)
|
||
{
|
||
var localStatus = ResolveSignStatus(providerContract);
|
||
process.ProviderStatus = providerContract.Status;
|
||
process.SignStatus = localStatus;
|
||
if (localStatus == ElectronicSignStatus.Completed
|
||
|| localStatus == ElectronicSignStatus.Archived)
|
||
{
|
||
process.CompletedAt ??= DateTime.Now;
|
||
process.SignUrl = null;
|
||
process.SignUrlExpireAt = null;
|
||
await _db.Updateable<ObTaskContract>()
|
||
.SetColumns(item => item.ContractStatus == TaskContractStatus.ElectronicSigned)
|
||
.Where(item => item.TaskId == process.TaskId)
|
||
.ExecuteCommandAsync();
|
||
}
|
||
else if (IsTerminal(localStatus))
|
||
{
|
||
process.SignUrl = null;
|
||
process.SignUrlExpireAt = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 下载每份签署 PDF 和包含签署日志的 ZIP,并保存到业务 OSS。
|
||
/// </summary>
|
||
private async Task ArchiveCompletedProcess(ObElectronicSignProcess process)
|
||
{
|
||
var documents = await _db.Queryable<ObElectronicSignDocument>()
|
||
.Where(item => item.SignProcessId == process.Id)
|
||
.OrderBy(item => item.DocumentSort)
|
||
.ToListAsync();
|
||
if (documents.Count == 0)
|
||
throw Oops.Bah("电子签流程没有文档映射,无法归档");
|
||
|
||
foreach (var document in documents)
|
||
{
|
||
if (document.SignedPdfFileId.HasValue)
|
||
continue;
|
||
var signedPdf = await _qiyuesuoClient.DownloadDocument(
|
||
document.ProviderDocumentId);
|
||
var pdfFileId = await _fileService.StoreGeneratedFile(
|
||
SysDictConst.FILE_ENGINE_ALIYUN_OSS,
|
||
$"{SafeFileName(process.BizId)}_{document.DocumentSort:D2}_{SafeFileName(document.DocumentName)}_已签署.pdf",
|
||
signedPdf);
|
||
document.SignedPdfFileId = pdfFileId;
|
||
document.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(document).ExecuteCommandAsync();
|
||
await _db.Updateable<ObTaskContract>()
|
||
.SetColumns(item => new ObTaskContract
|
||
{
|
||
GeneratedPdfFileId = pdfFileId,
|
||
ContractStatus = TaskContractStatus.Archived
|
||
})
|
||
.Where(item => item.Id == document.TaskContractId)
|
||
.ExecuteCommandAsync();
|
||
}
|
||
|
||
if (!process.ArchiveFileId.HasValue)
|
||
{
|
||
var archive = await _qiyuesuoClient.DownloadArchive(
|
||
process.ProviderContractId);
|
||
process.ArchiveFileId = await _fileService.StoreGeneratedFile(
|
||
SysDictConst.FILE_ENGINE_ALIYUN_OSS,
|
||
$"{SafeFileName(process.BizId)}_电子签完整归档.zip",
|
||
archive);
|
||
}
|
||
|
||
process.SignStatus = ElectronicSignStatus.Archived;
|
||
process.ArchivedAt ??= DateTime.Now;
|
||
process.LastError = null;
|
||
process.UpdateTime = DateTime.Now;
|
||
await _db.Updateable(process).ExecuteCommandAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 组装管理端需要的当前流程、合同文件和历史签约记录。
|
||
/// </summary>
|
||
private async Task<OnboardingElectronicSignDetailOutput> BuildDetail(
|
||
ObOnboardingTask task,
|
||
ObElectronicSignProcess process = null)
|
||
{
|
||
process ??= await GetLatestProcess(task.Id);
|
||
var historyProcesses = await _db.Queryable<ObElectronicSignProcess>()
|
||
.Where(item => item.TaskId == task.Id && item.IsCurrent == null)
|
||
.OrderBy(item => item.StartedAt, OrderByType.Desc)
|
||
.ToListAsync();
|
||
|
||
var taskAllowed = task.EmployeeId.HasValue
|
||
&& (task.TaskStatus == OnboardingTaskStatus.Approved
|
||
|| task.TaskStatus == OnboardingTaskStatus.Archived);
|
||
var creatingAt = process?.UpdateTime ?? process?.StartedAt;
|
||
var processAllowed = process == null
|
||
|| process.SignStatus == ElectronicSignStatus.Failed
|
||
|| process.SignStatus == ElectronicSignStatus.Draft
|
||
|| (process.SignStatus == ElectronicSignStatus.Creating
|
||
&& creatingAt <= DateTime.Now.AddMinutes(-5));
|
||
var reinitiateAllowed = process != null
|
||
&& CanCreateReplacementProcess(process);
|
||
var result = new OnboardingElectronicSignDetailOutput
|
||
{
|
||
TaskId = task.Id,
|
||
TaskNo = task.TaskNo,
|
||
CanInitiate = taskAllowed && processAllowed,
|
||
CannotInitiateReason = ResolveCannotInitiateReason(
|
||
task,
|
||
taskAllowed,
|
||
processAllowed),
|
||
CanReinitiate = taskAllowed && reinitiateAllowed,
|
||
CannotReinitiateReason = ResolveCannotReinitiateReason(
|
||
task,
|
||
process,
|
||
taskAllowed,
|
||
reinitiateAllowed),
|
||
HasProcess = process != null,
|
||
HistoryProcesses = historyProcesses.Select(item =>
|
||
new OnboardingElectronicSignHistoryOutput
|
||
{
|
||
ProcessId = item.Id,
|
||
BizId = item.BizId,
|
||
ProviderContractId = item.ProviderContractId,
|
||
SignStatus = item.SignStatus,
|
||
ProviderStatus = item.ProviderStatus,
|
||
EmployeeName = item.EmployeeName,
|
||
EmployeeMobile = item.EmployeeMobile,
|
||
StartedAt = item.StartedAt,
|
||
ReissuedAt = item.ReissuedAt,
|
||
ReissueReason = item.ReissueReason,
|
||
LastError = item.LastError
|
||
}).ToList()
|
||
};
|
||
if (process == null)
|
||
{
|
||
var pendingContracts = await _db.Queryable<ObTaskContract>()
|
||
.Where(item => item.TaskId == task.Id)
|
||
.OrderBy(item => item.Id)
|
||
.ToListAsync();
|
||
result.Documents = pendingContracts.Select((item, index) =>
|
||
new OnboardingElectronicSignDocumentOutput
|
||
{
|
||
TaskContractId = item.Id,
|
||
DocumentName = item.ContractName,
|
||
DocumentSort = index + 1,
|
||
OriginalDocxFileId = item.GeneratedDocxFileId ?? 0,
|
||
SignedPdfFileId = item.GeneratedPdfFileId
|
||
}).ToList();
|
||
return result;
|
||
}
|
||
|
||
var documents = await _db.Queryable<ObElectronicSignDocument>()
|
||
.Where(item => item.SignProcessId == process.Id)
|
||
.OrderBy(item => item.DocumentSort)
|
||
.ToListAsync();
|
||
result.ProcessId = process.Id;
|
||
result.ProviderContractId = process.ProviderContractId;
|
||
result.BizId = process.BizId;
|
||
result.Subject = process.Subject;
|
||
result.SignStatus = process.SignStatus;
|
||
result.ProviderStatus = process.ProviderStatus;
|
||
result.EmployeeName = process.EmployeeName;
|
||
result.EmployeeMobile = process.EmployeeMobile;
|
||
result.SignUrl = process.SignUrl;
|
||
result.SignUrlExpireAt = process.SignUrlExpireAt;
|
||
result.SignUrlValid = !string.IsNullOrWhiteSpace(process.SignUrl)
|
||
&& process.SignUrlExpireAt > DateTime.Now.AddMinutes(1);
|
||
result.StartedAt = process.StartedAt;
|
||
result.SentAt = process.SentAt;
|
||
result.CompletedAt = process.CompletedAt;
|
||
result.ArchivedAt = process.ArchivedAt;
|
||
result.ArchiveFileId = process.ArchiveFileId;
|
||
result.LastError = process.LastError;
|
||
if (documents.Count > 0)
|
||
{
|
||
result.Documents = documents.Select(item =>
|
||
new OnboardingElectronicSignDocumentOutput
|
||
{
|
||
Id = item.Id,
|
||
TaskContractId = item.TaskContractId,
|
||
ProviderDocumentId = item.ProviderDocumentId,
|
||
DocumentName = item.DocumentName,
|
||
DocumentSort = item.DocumentSort,
|
||
OriginalDocxFileId = item.OriginalDocxFileId,
|
||
SignedPdfFileId = item.SignedPdfFileId
|
||
}).ToList();
|
||
}
|
||
else
|
||
{
|
||
// 草稿创建前失败时尚无电子签文档记录,仍回显任务合同,允许管理员从断点继续重试。
|
||
var pendingContracts = await _db.Queryable<ObTaskContract>()
|
||
.Where(item => item.TaskId == task.Id)
|
||
.OrderBy(item => item.Id)
|
||
.ToListAsync();
|
||
result.Documents = pendingContracts.Select((item, index) =>
|
||
new OnboardingElectronicSignDocumentOutput
|
||
{
|
||
TaskContractId = item.Id,
|
||
DocumentName = item.ContractName,
|
||
DocumentSort = index + 1,
|
||
OriginalDocxFileId = item.GeneratedDocxFileId ?? 0,
|
||
SignedPdfFileId = item.GeneratedPdfFileId
|
||
}).ToList();
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 电子签管理沿用入职审核人权限,不新增另一套权限来源。
|
||
/// </summary>
|
||
private async Task<ObOnboardingTask> GetAuthorizedTask(long taskId)
|
||
{
|
||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId)
|
||
?? throw Oops.Bah("入职任务不存在");
|
||
var config = await _configService.GetByConfigKey(
|
||
CateGoryConst.CONFIG_BIZ_DEFINE,
|
||
"onboard.reviewer.userIds")
|
||
?? throw Oops.Bah("请配置入职审核人");
|
||
var reviewerIds = OnboardingReviewerConfigParser.Parse(
|
||
config.ConfigValue,
|
||
task.CompanyId);
|
||
if (!reviewerIds.Contains(UserManager.UserId))
|
||
throw Oops.Bah("您不是该入职任务的审核人");
|
||
return task;
|
||
}
|
||
|
||
private async Task<ObElectronicSignProcess> GetLatestProcess(long taskId)
|
||
{
|
||
return await _db.Queryable<ObElectronicSignProcess>()
|
||
.Where(item => item.TaskId == taskId && item.IsCurrent == true)
|
||
.OrderBy(item => item.StartedAt, OrderByType.Desc)
|
||
.FirstAsync();
|
||
}
|
||
|
||
private async Task<ObElectronicSignProcess> GetRequiredProcess(long taskId)
|
||
{
|
||
return await GetLatestProcess(taskId)
|
||
?? throw Oops.Bah("该入职任务尚未发起电子签");
|
||
}
|
||
|
||
private async Task<ObElectronicSignProcess> FindProcess(
|
||
string providerContractId,
|
||
string bizId)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(providerContractId))
|
||
{
|
||
var byContract = await _db.Queryable<ObElectronicSignProcess>()
|
||
.FirstAsync(item => item.ProviderContractId == providerContractId);
|
||
if (byContract != null)
|
||
return byContract;
|
||
}
|
||
if (string.IsNullOrWhiteSpace(bizId))
|
||
return null;
|
||
return await _db.Queryable<ObElectronicSignProcess>()
|
||
.FirstAsync(item => item.BizId == bizId);
|
||
}
|
||
|
||
private async Task<byte[]> ReadFile(long fileId)
|
||
{
|
||
var file = await _fileService.Download(new BaseIdInput { Id = fileId })
|
||
?? throw Oops.Bah($"系统文件 {fileId} 不存在");
|
||
await using var stream = file.FileStream;
|
||
using var memory = new MemoryStream();
|
||
await stream.CopyToAsync(memory);
|
||
return memory.ToArray();
|
||
}
|
||
|
||
private Task<string> CreateSignPageUrl(ObElectronicSignProcess process)
|
||
{
|
||
return _qiyuesuoClient.GetSignPageUrl(new QiyuesuoSignPageInput
|
||
{
|
||
ContractId = process.ProviderContractId,
|
||
EmployeeMobile = process.EmployeeMobile,
|
||
CallbackPage = _setting.SignPageCallbackUrl
|
||
});
|
||
}
|
||
|
||
private static string ResolveSignStatus(
|
||
QiyuesuoContractResult providerContract)
|
||
{
|
||
var status = (providerContract.Status ?? string.Empty)
|
||
.Trim()
|
||
.ToUpperInvariant();
|
||
if (status is "COMPLETE" or "COMPLETED" or "FINISHED")
|
||
return ElectronicSignStatus.Completed;
|
||
if (status is "REJECTED" or "REFUSED")
|
||
return ElectronicSignStatus.Rejected;
|
||
if (status is "INVALID" or "INVALIDING" or "INVALIDED" or "RECALLED")
|
||
return ElectronicSignStatus.Invalid;
|
||
if (status == "EXPIRED")
|
||
return ElectronicSignStatus.Expired;
|
||
if (status == "DRAFT")
|
||
return ElectronicSignStatus.Draft;
|
||
|
||
var personal = providerContract.Signatories.FirstOrDefault(item =>
|
||
string.Equals(item.TenantType, "PERSONAL", StringComparison.OrdinalIgnoreCase));
|
||
var employeeSigned = string.Equals(
|
||
personal?.Status,
|
||
"SIGNED",
|
||
StringComparison.OrdinalIgnoreCase);
|
||
return employeeSigned
|
||
? ElectronicSignStatus.PendingCompanySeal
|
||
: ElectronicSignStatus.PendingEmployeeSign;
|
||
}
|
||
|
||
private static bool IsTerminal(string status)
|
||
{
|
||
return status == ElectronicSignStatus.Completed
|
||
|| status == ElectronicSignStatus.Archived
|
||
|| status == ElectronicSignStatus.Rejected
|
||
|| status == ElectronicSignStatus.Invalid
|
||
|| status == ElectronicSignStatus.Expired;
|
||
}
|
||
|
||
private static string GetContractId(QiyuesuoContractResult result)
|
||
{
|
||
var id = result?.Id ?? result?.ContractId;
|
||
return !string.IsNullOrWhiteSpace(id)
|
||
? id
|
||
: throw Oops.Bah("契约锁创建草稿后未返回合同流程 Id");
|
||
}
|
||
|
||
private static string GetDocumentId(QiyuesuoDocumentResult result)
|
||
{
|
||
var id = result?.Id ?? result?.DocumentId;
|
||
return !string.IsNullOrWhiteSpace(id)
|
||
? id
|
||
: throw Oops.Bah("契约锁上传文档后未返回文档 Id");
|
||
}
|
||
|
||
private static string GetSignatoryId(QiyuesuoSignatoryResult result)
|
||
{
|
||
var id = result?.Id ?? result?.SignatoryId;
|
||
return !string.IsNullOrWhiteSpace(id)
|
||
? id
|
||
: throw Oops.Bah("契约锁未返回员工签署方 Id");
|
||
}
|
||
|
||
private static string GetActionId(QiyuesuoActionResult result)
|
||
{
|
||
var id = result?.Id ?? result?.ActionId;
|
||
return !string.IsNullOrWhiteSpace(id)
|
||
? id
|
||
: throw Oops.Bah("契约锁未返回企业盖章动作 Id");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 校验正式员工主档与审核通过的入职登记资料是否属于同一实名人员。
|
||
/// 原始登记资料用于审计追溯,不允许在电子签发起时静默覆盖。
|
||
/// </summary>
|
||
private static void ValidateSignerIdentity(
|
||
ObEmployee employee,
|
||
ObTaskEmployeeInfo employeeInfo)
|
||
{
|
||
var differentFields = new List<string>();
|
||
if (!string.Equals(
|
||
NormalizeIdentityValue(employee.EmployeeName),
|
||
NormalizeIdentityValue(employeeInfo.EmployeeName),
|
||
StringComparison.Ordinal))
|
||
{
|
||
differentFields.Add("姓名");
|
||
}
|
||
if (!string.Equals(
|
||
NormalizeIdentityValue(employee.Phone),
|
||
NormalizeIdentityValue(employeeInfo.Phone),
|
||
StringComparison.Ordinal))
|
||
{
|
||
differentFields.Add("手机号");
|
||
}
|
||
if (!string.Equals(
|
||
NormalizeIdentityValue(employee.IdCardNumber).ToUpperInvariant(),
|
||
NormalizeIdentityValue(employeeInfo.IdCardNumber).ToUpperInvariant(),
|
||
StringComparison.Ordinal))
|
||
{
|
||
differentFields.Add("身份证号");
|
||
}
|
||
|
||
if (differentFields.Count > 0)
|
||
throw Oops.Bah($"正式员工档案的{string.Join("、", differentFields)}与审核通过的入职登记资料不一致,请先核对并修正后再发起电子签");
|
||
}
|
||
|
||
private static string NormalizeIdentityValue(string value)
|
||
{
|
||
return value?.Trim() ?? string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断当前流程能否在保留历史记录的前提下重新创建。
|
||
/// </summary>
|
||
private static bool CanCreateReplacementProcess(
|
||
ObElectronicSignProcess process)
|
||
{
|
||
if (process.SignStatus == ElectronicSignStatus.Invalid
|
||
|| process.SignStatus == ElectronicSignStatus.Rejected
|
||
|| process.SignStatus == ElectronicSignStatus.Expired)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
return process.SignStatus == ElectronicSignStatus.Failed
|
||
&& string.IsNullOrWhiteSpace(process.ProviderContractId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 拦截可能造成重复收费或破坏已完成档案的重新发起操作。
|
||
/// </summary>
|
||
private static void ValidateProcessCanReinitiate(
|
||
ObElectronicSignProcess process)
|
||
{
|
||
if (CanCreateReplacementProcess(process))
|
||
return;
|
||
|
||
if (process.SignStatus == ElectronicSignStatus.Completed
|
||
|| process.SignStatus == ElectronicSignStatus.Archived)
|
||
{
|
||
throw Oops.Bah("已经签署完成的电子签流程不能重新发起");
|
||
}
|
||
if (process.SignStatus == ElectronicSignStatus.Failed
|
||
&& !string.IsNullOrWhiteSpace(process.ProviderContractId))
|
||
{
|
||
throw Oops.Bah("当前失败流程已经在契约锁创建,请先重试或同步状态;仍需重开时请先作废原流程");
|
||
}
|
||
|
||
throw Oops.Bah("当前电子签流程仍在进行中,请先作废原流程后再重新发起");
|
||
}
|
||
|
||
private static void ValidateTaskCanSign(ObOnboardingTask task)
|
||
{
|
||
if (!task.EmployeeId.HasValue)
|
||
throw Oops.Bah("入职任务尚未生成正式员工档案,不能发起电子签");
|
||
if (task.TaskStatus != OnboardingTaskStatus.Approved
|
||
&& task.TaskStatus != OnboardingTaskStatus.Archived)
|
||
{
|
||
throw Oops.Bah("仅审核通过或已归档的入职任务可以发起电子签");
|
||
}
|
||
}
|
||
|
||
private void ValidateSettings()
|
||
{
|
||
if (!_setting.Enabled)
|
||
throw Oops.Bah("契约锁电子签功能尚未启用");
|
||
if (string.IsNullOrWhiteSpace(_setting.AppToken))
|
||
throw Oops.Bah("请先配置契约锁 AppToken");
|
||
if (string.IsNullOrWhiteSpace(_setting.AppSecret))
|
||
throw Oops.Bah("请先配置契约锁 AppSecret");
|
||
if (string.IsNullOrWhiteSpace(_setting.CategoryId))
|
||
throw Oops.Bah("请先配置契约锁默认业务分类 Id");
|
||
if (string.IsNullOrWhiteSpace(_setting.SealId))
|
||
throw Oops.Bah("请先配置契约锁企业印章 Id");
|
||
if (string.IsNullOrWhiteSpace(_setting.CreatorName)
|
||
|| string.IsNullOrWhiteSpace(_setting.CreatorMobile))
|
||
{
|
||
throw Oops.Bah("请配置契约锁企业成员姓名和手机号");
|
||
}
|
||
if (string.IsNullOrWhiteSpace(_setting.EmployeeSignKeyword)
|
||
|| string.IsNullOrWhiteSpace(_setting.CompanySealKeyword))
|
||
{
|
||
throw Oops.Bah("请配置员工签名和企业盖章定位关键字");
|
||
}
|
||
}
|
||
|
||
private static void EnsureCanOpenEmployeeSignPage(
|
||
ObElectronicSignProcess process)
|
||
{
|
||
if (process.SignStatus != ElectronicSignStatus.PendingEmployeeSign)
|
||
throw Oops.Bah("当前流程不在员工待签状态,不能获取员工签署链接");
|
||
}
|
||
|
||
private int NormalizeSignUrlExpireMinutes()
|
||
{
|
||
// 契约锁签署页面地址最长有效 10 分钟,本地不能继续返回已经失效的缓存链接。
|
||
return Math.Clamp(_setting.SignUrlExpireMinutes, 5, 10);
|
||
}
|
||
|
||
private static string ResolveCannotInitiateReason(
|
||
ObOnboardingTask task,
|
||
bool taskAllowed,
|
||
bool processAllowed)
|
||
{
|
||
if (!task.EmployeeId.HasValue)
|
||
return "审核通过并生成正式员工档案后才可发起电子签";
|
||
if (!taskAllowed)
|
||
return "当前入职任务状态不能发起电子签";
|
||
if (!processAllowed)
|
||
return "该任务已经存在电子签流程";
|
||
return null;
|
||
}
|
||
|
||
private static string ResolveCannotReinitiateReason(
|
||
ObOnboardingTask task,
|
||
ObElectronicSignProcess process,
|
||
bool taskAllowed,
|
||
bool reinitiateAllowed)
|
||
{
|
||
if (!task.EmployeeId.HasValue)
|
||
return "审核通过并生成正式员工档案后才可重新发起电子签";
|
||
if (!taskAllowed)
|
||
return "当前入职任务状态不能重新发起电子签";
|
||
if (process == null)
|
||
return "该任务尚未发起电子签";
|
||
if (reinitiateAllowed)
|
||
return null;
|
||
if (process.SignStatus == ElectronicSignStatus.Completed
|
||
|| process.SignStatus == ElectronicSignStatus.Archived)
|
||
{
|
||
return "已经签署完成的流程不能重新发起";
|
||
}
|
||
if (process.SignStatus == ElectronicSignStatus.Failed
|
||
&& !string.IsNullOrWhiteSpace(process.ProviderContractId))
|
||
{
|
||
return "当前失败流程已在契约锁创建,请先重试或同步状态";
|
||
}
|
||
|
||
return "当前流程仍在进行中,请先作废后再重新发起";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在未知回调层级中递归读取常见字段,兼容契约锁不同事件的数据包装。
|
||
/// </summary>
|
||
private static string FindString(
|
||
JsonElement element,
|
||
params string[] propertyNames)
|
||
{
|
||
if (element.ValueKind == JsonValueKind.Object)
|
||
{
|
||
foreach (var property in element.EnumerateObject())
|
||
{
|
||
if (propertyNames.Any(name => string.Equals(
|
||
name,
|
||
property.Name,
|
||
StringComparison.OrdinalIgnoreCase))
|
||
&& property.Value.ValueKind != JsonValueKind.Null)
|
||
{
|
||
return property.Value.ValueKind == JsonValueKind.String
|
||
? property.Value.GetString()
|
||
: property.Value.ToString();
|
||
}
|
||
}
|
||
foreach (var property in element.EnumerateObject())
|
||
{
|
||
var nested = FindString(property.Value, propertyNames);
|
||
if (!string.IsNullOrWhiteSpace(nested))
|
||
return nested;
|
||
}
|
||
}
|
||
else if (element.ValueKind == JsonValueKind.Array)
|
||
{
|
||
foreach (var item in element.EnumerateArray())
|
||
{
|
||
var nested = FindString(item, propertyNames);
|
||
if (!string.IsNullOrWhiteSpace(nested))
|
||
return nested;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static string SafeFileName(string value)
|
||
{
|
||
var source = string.IsNullOrWhiteSpace(value) ? "合同" : value.Trim();
|
||
var invalidChars = Path.GetInvalidFileNameChars();
|
||
return new string(source
|
||
.Select(character => invalidChars.Contains(character) ? '_' : character)
|
||
.ToArray());
|
||
}
|
||
|
||
private static string LimitError(string message, int maxLength = 1800)
|
||
{
|
||
var normalized = string.IsNullOrWhiteSpace(message)
|
||
? "未知错误"
|
||
: message.Trim();
|
||
return normalized.Length <= maxLength
|
||
? normalized
|
||
: normalized[..maxLength];
|
||
}
|
||
}
|