完善第三方签署
This commit is contained in:
@@ -17,17 +17,19 @@
|
||||
"AppToken": "rMKU99gkts",
|
||||
"AppSecret": "sjhcNcV1YTIhA124yrjLMdKRoBlP8O",
|
||||
"SignatureVersion": "",
|
||||
"CategoryId": "3533041076673835142",
|
||||
"SealId": "3533379903657587306",
|
||||
"CreatorName": "袁林",
|
||||
"CreatorMobile": "15123135384",
|
||||
"CallbackUrl": "https://undiffusively-hemielytral-myrtle.ngrok-free.dev",
|
||||
"CallbackUrl": "https://undiffusively-hemielytral-myrtle.ngrok-free.dev/api/open/onboarding/qiyuesuo/callback",
|
||||
"SignPageCallbackUrl": "",
|
||||
"SignUrlExpireMinutes": 10,
|
||||
"EmployeeSignKeyword": "乙方(劳动者)签字:",
|
||||
"EmployeeSignKeywordIndex": 1,
|
||||
"CompanySealKeyword": "甲方(盖章):",
|
||||
"CompanySealKeywordIndex": 1,
|
||||
"CompanySealOffsetX": 0,
|
||||
"CompanySealOffsetY": -0.05,
|
||||
"EnableAcrossPageSeal": true,
|
||||
"AcrossPageSealType": "ALL_DOCUMENT",
|
||||
"TimeoutSeconds": 90
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
"AppToken": "rMKU99gkts",
|
||||
"AppSecret": "sjhcNcV1YTIhA124yrjLMdKRoBlP8O",
|
||||
"SignatureVersion": "",
|
||||
"CategoryId": "3533041076673835142",
|
||||
"SealId": "3533042752893948009",
|
||||
"CreatorName": "袁林",
|
||||
"CreatorMobile": "15123135384",
|
||||
"CallbackUrl": "https://onboardx-api.cnzhyx.cn/api/open/onboarding/qiyuesuo/callback",
|
||||
@@ -28,6 +26,10 @@
|
||||
"EmployeeSignKeywordIndex": 1,
|
||||
"CompanySealKeyword": "甲方(盖章):",
|
||||
"CompanySealKeywordIndex": 1,
|
||||
"CompanySealOffsetX": 0,
|
||||
"CompanySealOffsetY": -0.12,
|
||||
"EnableAcrossPageSeal": true,
|
||||
"AcrossPageSealType": "ALL_DOCUMENT",
|
||||
"TimeoutSeconds": 90
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,16 @@ public class ObCompany : BaseEntity
|
||||
/// </summary>
|
||||
public string ContactPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 契约锁默认业务分类 Id;每个签约公司独立配置。
|
||||
/// </summary>
|
||||
public string QiyuesuoCategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 契约锁企业印章 Id;每个签约公司独立配置。
|
||||
/// </summary>
|
||||
public string QiyuesuoSealId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务备注。
|
||||
/// </summary>
|
||||
|
||||
@@ -176,7 +176,7 @@ public static class ElectronicSignStatus
|
||||
public const string PendingEmployeeSign = "pending_employee_sign";
|
||||
|
||||
/// <summary>
|
||||
/// 员工已签,等待企业自动盖章或平台完成流程。
|
||||
/// 员工已签,等待后台管理员核对文件并确认企业盖章。
|
||||
/// </summary>
|
||||
public const string PendingCompanySeal = "pending_company_seal";
|
||||
|
||||
|
||||
@@ -30,16 +30,6 @@ public class QiyuesuoSettingOptions
|
||||
/// </summary>
|
||||
public string SignatureVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认业务分类 Id。
|
||||
/// </summary>
|
||||
public string CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 企业自动盖章使用的印章 Id。
|
||||
/// </summary>
|
||||
public string SealId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 契约锁企业成员中的合同创建人姓名。
|
||||
/// </summary>
|
||||
@@ -85,6 +75,26 @@ public class QiyuesuoSettingOptions
|
||||
/// </summary>
|
||||
public int CompanySealKeywordIndex { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 企业盖章相对关键字的横向偏移量,范围 (-1, 1),正数向右、负数向左。
|
||||
/// </summary>
|
||||
public decimal CompanySealOffsetX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 企业盖章相对关键字的纵向偏移量,范围 (-1, 1),正数向上、负数向下。
|
||||
/// </summary>
|
||||
public decimal CompanySealOffsetY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否在后台确认企业盖章时同时加盖骑缝章。
|
||||
/// </summary>
|
||||
public bool EnableAcrossPageSeal { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 骑缝章文档范围:ALL_DOCUMENT(全部文档合并)或 SINGLE_DOCUMENT(每份文档单独)。
|
||||
/// </summary>
|
||||
public string AcrossPageSealType { get; set; } = "ALL_DOCUMENT";
|
||||
|
||||
/// <summary>
|
||||
/// 单次 HTTP 请求超时秒数。
|
||||
/// </summary>
|
||||
|
||||
@@ -43,6 +43,7 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
|
||||
/// </summary>
|
||||
public async Task Add(OnboardingCompanyAddInput input)
|
||||
{
|
||||
NormalizeAndValidateQiyuesuoConfig(input);
|
||||
await EnsureCodeUnique(input.CompanyCode, 0);
|
||||
await InsertAsync(input.Adapt<ObCompany>());
|
||||
}
|
||||
@@ -52,6 +53,7 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
|
||||
/// </summary>
|
||||
public async Task Edit(OnboardingCompanyEditInput input)
|
||||
{
|
||||
NormalizeAndValidateQiyuesuoConfig(input);
|
||||
await Detail(new BaseIdInput { Id = input.Id });
|
||||
await EnsureCodeUnique(input.CompanyCode, input.Id);
|
||||
await UpdateAsync(input.Adapt<ObCompany>());
|
||||
@@ -98,6 +100,24 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
|
||||
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
|
||||
}
|
||||
|
||||
private static void NormalizeAndValidateQiyuesuoConfig(ObCompany company)
|
||||
{
|
||||
company.QiyuesuoCategoryId = company.QiyuesuoCategoryId?.Trim();
|
||||
company.QiyuesuoSealId = company.QiyuesuoSealId?.Trim();
|
||||
|
||||
var hasCategoryId = !string.IsNullOrWhiteSpace(company.QiyuesuoCategoryId);
|
||||
var hasSealId = !string.IsNullOrWhiteSpace(company.QiyuesuoSealId);
|
||||
if (hasCategoryId != hasSealId)
|
||||
throw Oops.Bah("契约锁默认业务分类 Id 和企业印章 Id 必须同时填写");
|
||||
if (!hasCategoryId)
|
||||
return;
|
||||
if (!long.TryParse(company.QiyuesuoCategoryId, out _)
|
||||
|| !long.TryParse(company.QiyuesuoSealId, out _))
|
||||
{
|
||||
throw Oops.Bah("契约锁默认业务分类 Id 和企业印章 Id 只能填写数字");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureCodeUnique(string code, long id)
|
||||
{
|
||||
var exists = await Context.Queryable<ObCompany>()
|
||||
|
||||
+55
-18
@@ -17,25 +17,39 @@ public static class DocxTemplateProcessor
|
||||
/// </summary>
|
||||
public static List<string> ExtractVariableCodes(byte[] docx)
|
||||
{
|
||||
using var input = new MemoryStream(docx);
|
||||
using var archive = new global::System.IO.Compression.ZipArchive(
|
||||
input,
|
||||
global::System.IO.Compression.ZipArchiveMode.Read);
|
||||
var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(entry => IsWordTextXml(entry.FullName)))
|
||||
ValidateDocxHeader(docx);
|
||||
try
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
var document = global::System.Xml.Linq.XDocument.Load(stream);
|
||||
foreach (var paragraph in document.Descendants(WordNamespace + "p"))
|
||||
{
|
||||
var text = string.Concat(paragraph.Descendants(WordNamespace + "t").Select(node => node.Value));
|
||||
foreach (global::System.Text.RegularExpressions.Match match in PlaceholderRegex.Matches(text))
|
||||
codes.Add(match.Groups["code"].Value.Trim());
|
||||
}
|
||||
}
|
||||
using var input = new MemoryStream(docx);
|
||||
using var archive = new global::System.IO.Compression.ZipArchive(
|
||||
input,
|
||||
global::System.IO.Compression.ZipArchiveMode.Read);
|
||||
if (archive.GetEntry("word/document.xml") == null)
|
||||
throw Oops.Bah("上传文件不是有效的 DOCX 文档,请使用 Word 或 WPS 另存为“Word 文档(*.docx)”后重新上传");
|
||||
|
||||
return codes.OrderBy(code => code).ToList();
|
||||
var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var entry in archive.Entries.Where(entry => IsWordTextXml(entry.FullName)))
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
var document = global::System.Xml.Linq.XDocument.Load(stream);
|
||||
foreach (var paragraph in document.Descendants(WordNamespace + "p"))
|
||||
{
|
||||
var text = string.Concat(paragraph.Descendants(WordNamespace + "t").Select(node => node.Value));
|
||||
foreach (global::System.Text.RegularExpressions.Match match in PlaceholderRegex.Matches(text))
|
||||
codes.Add(match.Groups["code"].Value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return codes.OrderBy(code => code).ToList();
|
||||
}
|
||||
catch (global::System.IO.InvalidDataException)
|
||||
{
|
||||
throw Oops.Bah("DOCX 文件结构已经损坏或上传不完整,请重新另存为 DOCX 后上传");
|
||||
}
|
||||
catch (global::System.Xml.XmlException)
|
||||
{
|
||||
throw Oops.Bah("DOCX 文件内部内容损坏,Word 可以尝试修复该文件;请修复并重新另存为 DOCX 后上传");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,6 +57,7 @@ public static class DocxTemplateProcessor
|
||||
/// </summary>
|
||||
public static byte[] Render(byte[] docx, IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
ValidateDocxHeader(docx);
|
||||
using var input = new MemoryStream(docx);
|
||||
using var archive = new global::System.IO.Compression.ZipArchive(
|
||||
input,
|
||||
@@ -157,4 +172,26 @@ public static class DocxTemplateProcessor
|
||||
|| entryName.StartsWith("word/footer", StringComparison.OrdinalIgnoreCase))
|
||||
&& entryName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件头识别真实格式,避免只修改扩展名的旧版 DOC 在解压时变成系统异常。
|
||||
/// </summary>
|
||||
private static void ValidateDocxHeader(byte[] docx)
|
||||
{
|
||||
if (docx == null || docx.Length < 4)
|
||||
throw Oops.Bah("上传的 DOCX 文件为空或上传不完整,请重新选择文件上传");
|
||||
|
||||
// 旧版 Word 97-2003 二进制 DOC 使用 OLE Compound File 文件头:D0 CF 11 E0。
|
||||
if (docx[0] == 0xD0
|
||||
&& docx[1] == 0xCF
|
||||
&& docx[2] == 0x11
|
||||
&& docx[3] == 0xE0)
|
||||
{
|
||||
throw Oops.Bah("上传文件实际是旧版 DOC 格式,不能直接修改后缀名;请使用 Word 或 WPS 另存为“Word 文档(*.docx)”后重新上传");
|
||||
}
|
||||
|
||||
// DOCX 是 Open XML ZIP 包,正常文件以 PK 开头。
|
||||
if (docx[0] != 0x50 || docx[1] != 0x4B)
|
||||
throw Oops.Bah("上传文件的真实格式不是 DOCX,请使用 Word 或 WPS 另存为“Word 文档(*.docx)”后重新上传");
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -37,6 +37,7 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
|
||||
var page = await query
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
||||
.OrderByDescending(it => it.CreateTime)
|
||||
.OrderBy(it => it.TemplateName)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
await PopulateScopeDisplay(page.List.ToList());
|
||||
|
||||
+12
@@ -14,6 +14,18 @@ public class OnboardingElectronicSignTaskInput
|
||||
public long TaskId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员查看一份员工已签合同的参数。
|
||||
/// </summary>
|
||||
public class OnboardingElectronicSignPreviewInput : OnboardingElectronicSignTaskInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地电子签文档 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "电子签文档Id不能为空")]
|
||||
public long SignDocumentId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作废入职电子签流程的参数。
|
||||
/// </summary>
|
||||
|
||||
+15
@@ -85,6 +85,18 @@ public class OnboardingElectronicSignDetailOutput
|
||||
public List<OnboardingElectronicSignHistoryOutput> HistoryProcesses { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 契约锁员工已签文件只读预览结果。
|
||||
/// </summary>
|
||||
public class OnboardingElectronicSignPreviewOutput
|
||||
{
|
||||
/// <summary>本地电子签文档 Id。</summary>
|
||||
public long SignDocumentId { get; set; }
|
||||
|
||||
/// <summary>契约锁生成的只读浏览页面地址。</summary>
|
||||
public string PreviewUrl { get; set; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 已被新流程替代的历史电子签流程摘要。
|
||||
@@ -149,4 +161,7 @@ public class OnboardingElectronicSignDocumentOutput
|
||||
|
||||
/// <summary>签署完成后的 PDF 文件 Id。</summary>
|
||||
public long? SignedPdfFileId { get; set; }
|
||||
|
||||
/// <summary>当前管理员流程中是否已生成过员工签后预览。</summary>
|
||||
public bool EmployeeSignedPreviewed { get; set; }
|
||||
}
|
||||
|
||||
+8
@@ -24,6 +24,14 @@ public interface IOnboardingElectronicSignService : ITransient
|
||||
Task<OnboardingElectronicSignDetailOutput> GetSignLink(
|
||||
OnboardingElectronicSignTaskInput input);
|
||||
|
||||
/// <summary>获取一份员工已签合同的只读浏览页面。</summary>
|
||||
Task<OnboardingElectronicSignPreviewOutput> PreviewEmployeeSignedDocument(
|
||||
OnboardingElectronicSignPreviewInput input);
|
||||
|
||||
/// <summary>确认全部文件已核对后,由后台执行企业盖章。</summary>
|
||||
Task<OnboardingElectronicSignDetailOutput> CompanySeal(
|
||||
OnboardingElectronicSignTaskInput input);
|
||||
|
||||
/// <summary>催办当前待签员工。</summary>
|
||||
Task Notice(OnboardingElectronicSignTaskInput input);
|
||||
|
||||
|
||||
+358
-17
@@ -5,10 +5,14 @@ using System.Text.Json;
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职合同“员工先签、企业自动盖章”的契约锁电子签流程。
|
||||
/// 实现入职合同“员工先签、后台核对、企业人工盖章”的契约锁电子签流程。
|
||||
/// </summary>
|
||||
public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
{
|
||||
private const string AdminPreviewEventSource = "admin_preview";
|
||||
private const string AdminCompanySealEventSource = "admin_company_seal";
|
||||
private const string PreviewEventTypePrefix = "employee_signed_preview:";
|
||||
|
||||
private readonly SqlSugarScope _db;
|
||||
private readonly IQiyuesuoClient _qiyuesuoClient;
|
||||
private readonly IOnboardingContractDocumentService _contractDocumentService;
|
||||
@@ -68,11 +72,22 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
var context = await GetInitiationContext(task);
|
||||
if (process == null)
|
||||
{
|
||||
var created = await CreateProcess(task, context.Employee);
|
||||
var created = await CreateProcess(
|
||||
task,
|
||||
context.Employee,
|
||||
context.CategoryId,
|
||||
context.SealId);
|
||||
process = created.Process;
|
||||
isNewProcess = created.Created;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(process.ProviderContractId))
|
||||
{
|
||||
// 第三方合同尚未创建时允许采用公司资料中的最新配置,便于修正配置后直接重试。
|
||||
process.CategoryId = context.CategoryId;
|
||||
process.SealId = context.SealId;
|
||||
}
|
||||
|
||||
return await StartProcess(
|
||||
task,
|
||||
context.CompanyName,
|
||||
@@ -97,6 +112,8 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
var process = await CreateReplacementProcess(
|
||||
task,
|
||||
context.Employee,
|
||||
context.CategoryId,
|
||||
context.SealId,
|
||||
previousProcess,
|
||||
input.Reason.Trim());
|
||||
|
||||
@@ -114,6 +131,8 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
private async Task<(
|
||||
ObEmployee Employee,
|
||||
string CompanyName,
|
||||
string CategoryId,
|
||||
string SealId,
|
||||
List<ObTaskContract> TaskContracts)> GetInitiationContext(
|
||||
ObOnboardingTask task)
|
||||
{
|
||||
@@ -140,6 +159,15 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
if (string.IsNullOrWhiteSpace(companyName))
|
||||
throw Oops.Bah("任务所属公司法定名称为空,不能发起电子签");
|
||||
|
||||
var categoryId = company.QiyuesuoCategoryId?.Trim();
|
||||
var sealId = company.QiyuesuoSealId?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(categoryId))
|
||||
throw Oops.Bah($"公司“{companyName}”未配置契约锁默认业务分类 Id,请先到公司与岗位中编辑公司资料");
|
||||
if (string.IsNullOrWhiteSpace(sealId))
|
||||
throw Oops.Bah($"公司“{companyName}”未配置契约锁企业印章 Id,请先到公司与岗位中编辑公司资料");
|
||||
if (!long.TryParse(categoryId, out _) || !long.TryParse(sealId, out _))
|
||||
throw Oops.Bah($"公司“{companyName}”的契约锁业务分类或印章 Id 格式不正确");
|
||||
|
||||
var taskContracts = await _db.Queryable<ObTaskContract>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.OrderBy(item => item.Id)
|
||||
@@ -152,7 +180,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
if (unconfirmed != null)
|
||||
throw Oops.Bah($"合同“{unconfirmed.ContractName}”尚未由员工确认");
|
||||
|
||||
return (employee, companyName, taskContracts);
|
||||
return (employee, companyName, categoryId, sealId, taskContracts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -258,6 +286,159 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
return await BuildDetail(task, process);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingElectronicSignPreviewOutput> PreviewEmployeeSignedDocument(
|
||||
OnboardingElectronicSignPreviewInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
var process = await GetRequiredProcess(task.Id);
|
||||
|
||||
// 不信任前端缓存状态。每次预览前重新查询契约锁,确保员工确实已完成签署。
|
||||
await SyncProcess(process, "preview_employee_signed");
|
||||
if (process.SignStatus != ElectronicSignStatus.PendingCompanySeal)
|
||||
throw Oops.Bah("只有员工已签、企业待盖章的合同才能在此处核对");
|
||||
|
||||
var document = await _db.Queryable<ObElectronicSignDocument>()
|
||||
.FirstAsync(item =>
|
||||
item.Id == input.SignDocumentId
|
||||
&& item.SignProcessId == process.Id)
|
||||
?? throw Oops.Bah("电子签文档不存在或不属于当前签约流程");
|
||||
if (string.IsNullOrWhiteSpace(document.ProviderDocumentId))
|
||||
throw Oops.Bah("契约锁文档尚未创建完成,不能预览");
|
||||
|
||||
var companyName = await GetCompanyLegalName(task);
|
||||
var previewUrl = await _qiyuesuoClient.GetViewPageUrl(
|
||||
new QiyuesuoViewPageInput
|
||||
{
|
||||
ContractId = process.ProviderContractId,
|
||||
DocumentId = document.ProviderDocumentId,
|
||||
CompanyName = companyName
|
||||
});
|
||||
if (string.IsNullOrWhiteSpace(previewUrl)
|
||||
|| !Uri.TryCreate(previewUrl, UriKind.Absolute, out var previewUri)
|
||||
|| (previewUri.Scheme != Uri.UriSchemeHttps
|
||||
&& previewUri.Scheme != Uri.UriSchemeHttp))
|
||||
{
|
||||
throw Oops.Bah("契约锁返回的合同浏览地址无效");
|
||||
}
|
||||
|
||||
await RecordAdminOperation(
|
||||
process,
|
||||
AdminPreviewEventSource,
|
||||
BuildPreviewEventType(document.Id),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
signDocumentId = document.Id,
|
||||
providerDocumentId = document.ProviderDocumentId,
|
||||
document.DocumentName,
|
||||
operatorUserId = UserManager.UserId,
|
||||
operatorName = UserManager.Name
|
||||
}),
|
||||
"已生成预览地址");
|
||||
|
||||
return new OnboardingElectronicSignPreviewOutput
|
||||
{
|
||||
SignDocumentId = document.Id,
|
||||
PreviewUrl = previewUrl
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingElectronicSignDetailOutput> CompanySeal(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
var process = await GetRequiredProcess(task.Id);
|
||||
|
||||
// 先查第三方真实状态。重复点击时,如果契约锁已经完成,则直接返回最新详情。
|
||||
await SyncProcess(process, "company_seal_precheck");
|
||||
if (process.SignStatus is ElectronicSignStatus.Completed
|
||||
or ElectronicSignStatus.Archived)
|
||||
{
|
||||
return await BuildDetail(task, process);
|
||||
}
|
||||
if (process.SignStatus != ElectronicSignStatus.PendingCompanySeal)
|
||||
throw Oops.Bah("员工尚未完成签署,当前不能执行企业盖章");
|
||||
|
||||
var documents = await _db.Queryable<ObElectronicSignDocument>()
|
||||
.Where(item => item.SignProcessId == process.Id)
|
||||
.OrderBy(item => item.DocumentSort)
|
||||
.ToListAsync();
|
||||
if (documents.Count == 0)
|
||||
throw Oops.Bah("当前电子签流程没有可盖章的合同文件");
|
||||
|
||||
var previewedDocumentIds = await GetPreviewedDocumentIds(process.Id);
|
||||
var notPreviewed = documents
|
||||
.Where(item => !previewedDocumentIds.Contains(item.Id))
|
||||
.Select(item => item.DocumentName)
|
||||
.ToList();
|
||||
if (notPreviewed.Count > 0)
|
||||
{
|
||||
throw Oops.Bah(
|
||||
$"请先逐份查看员工已签文件:{string.Join("、", notPreviewed)}");
|
||||
}
|
||||
|
||||
var companyName = await GetCompanyLegalName(task);
|
||||
try
|
||||
{
|
||||
await _qiyuesuoClient.CompanySign(
|
||||
new QiyuesuoCompanySignInput
|
||||
{
|
||||
ContractId = process.ProviderContractId,
|
||||
CompanyName = companyName,
|
||||
SealId = process.SealId
|
||||
});
|
||||
}
|
||||
catch (Exception signException)
|
||||
{
|
||||
// POST 响应中断时结果可能不明确,立即反查一次,避免实际已盖章却提示用户反复重试。
|
||||
try
|
||||
{
|
||||
await SyncProcess(process, "company_seal_verify");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// SyncProcess 已保存同步或归档错误,下面仍按内存中的真实状态判断是否已经盖章。
|
||||
}
|
||||
if (process.SignStatus is ElectronicSignStatus.Completed
|
||||
or ElectronicSignStatus.Archived)
|
||||
{
|
||||
return await BuildDetail(task, process);
|
||||
}
|
||||
|
||||
process.LastError = $"企业盖章失败:{LimitError(signException.Message)}";
|
||||
process.UpdateTime = DateTime.Now;
|
||||
await _db.Updateable(process).ExecuteCommandAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
await RecordAdminOperation(
|
||||
process,
|
||||
AdminCompanySealEventSource,
|
||||
"company_seal_submitted",
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
process.ProviderContractId,
|
||||
companyName,
|
||||
process.SealId,
|
||||
operatorUserId = UserManager.UserId,
|
||||
operatorName = UserManager.Name
|
||||
}),
|
||||
"企业盖章请求已提交");
|
||||
|
||||
// 盖章接口成功即代表请求已受理;后续查询短暂失败不能误导管理员再次盖章。
|
||||
try
|
||||
{
|
||||
await SyncProcess(process, "company_seal_sync");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// LastError 已由 SyncProcess 保存,页面自动刷新或回调会继续完成归档。
|
||||
}
|
||||
|
||||
return await BuildDetail(task, process);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Notice(OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
@@ -284,6 +465,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
|
||||
await _qiyuesuoClient.Invalid(
|
||||
process.ProviderContractId,
|
||||
process.SealId,
|
||||
input.Reason.Trim());
|
||||
process.SignStatus = ElectronicSignStatus.Invalid;
|
||||
process.ProviderStatus = "INVALIDED";
|
||||
@@ -390,9 +572,11 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
/// </summary>
|
||||
private async Task<(ObElectronicSignProcess Process, bool Created)> CreateProcess(
|
||||
ObOnboardingTask task,
|
||||
ObEmployee employee)
|
||||
ObEmployee employee,
|
||||
string categoryId,
|
||||
string sealId)
|
||||
{
|
||||
var process = BuildProcess(task, employee);
|
||||
var process = BuildProcess(task, employee, categoryId, sealId);
|
||||
try
|
||||
{
|
||||
await _db.Insertable(process).ExecuteCommandAsync();
|
||||
@@ -415,10 +599,12 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
private async Task<ObElectronicSignProcess> CreateReplacementProcess(
|
||||
ObOnboardingTask task,
|
||||
ObEmployee employee,
|
||||
string categoryId,
|
||||
string sealId,
|
||||
ObElectronicSignProcess previousProcess,
|
||||
string reason)
|
||||
{
|
||||
var process = BuildProcess(task, employee);
|
||||
var process = BuildProcess(task, employee, categoryId, sealId);
|
||||
var now = DateTime.Now;
|
||||
|
||||
await _db.Ado.BeginTranAsync();
|
||||
@@ -458,7 +644,9 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
/// </summary>
|
||||
private ObElectronicSignProcess BuildProcess(
|
||||
ObOnboardingTask task,
|
||||
ObEmployee employee)
|
||||
ObEmployee employee,
|
||||
string categoryId,
|
||||
string sealId)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var processId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
@@ -470,8 +658,8 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
Provider = ElectronicSignProvider.Qiyuesuo,
|
||||
BizId = $"{task.TaskNo}-SIGN-{processId}",
|
||||
IsCurrent = true,
|
||||
CategoryId = _setting.CategoryId.Trim(),
|
||||
SealId = _setting.SealId.Trim(),
|
||||
CategoryId = categoryId,
|
||||
SealId = sealId,
|
||||
Subject = $"{employee.EmployeeName}-{task.TaskNo}-入职合同",
|
||||
EmployeeName = employee.EmployeeName.Trim(),
|
||||
EmployeeMobile = employee.Phone.Trim(),
|
||||
@@ -612,11 +800,12 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
?? throw Oops.Bah("契约锁草稿未返回企业签署方");
|
||||
var companyAction = companySignatory.Actions.FirstOrDefault(item =>
|
||||
string.Equals(item.Type, "COMPANY", StringComparison.OrdinalIgnoreCase))
|
||||
?? throw Oops.Bah("契约锁草稿未返回企业自动盖章动作");
|
||||
?? 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))
|
||||
var orderedDocuments = documents.OrderBy(item => item.DocumentSort).ToList();
|
||||
foreach (var document in orderedDocuments)
|
||||
{
|
||||
document.EmployeeActionId = personalSignatoryId;
|
||||
document.CompanyActionId = companyActionId;
|
||||
@@ -637,10 +826,50 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
DocumentId = document.ProviderDocumentId,
|
||||
SealId = process.SealId,
|
||||
Keyword = document.CompanySealKeyword,
|
||||
KeywordIndex = Math.Max(1, _setting.CompanySealKeywordIndex)
|
||||
KeywordIndex = Math.Max(1, _setting.CompanySealKeywordIndex),
|
||||
OffsetX = _setting.CompanySealOffsetX,
|
||||
OffsetY = _setting.CompanySealOffsetY
|
||||
});
|
||||
}
|
||||
|
||||
if (_setting.EnableAcrossPageSeal)
|
||||
{
|
||||
var providerDocumentIds = orderedDocuments
|
||||
.Select(item => item.ProviderDocumentId)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList();
|
||||
if (providerDocumentIds.Count == 0)
|
||||
throw Oops.Bah("契约锁未返回可用于骑缝章的合同文档 Id");
|
||||
|
||||
var acrossType = _setting.AcrossPageSealType.Trim().ToUpperInvariant();
|
||||
if (acrossType == "ALL_DOCUMENT")
|
||||
{
|
||||
stampers.Add(new QiyuesuoStamperInput
|
||||
{
|
||||
ActionId = companyActionId,
|
||||
Type = "ACROSS_PAGE",
|
||||
DocumentId = providerDocumentIds[0],
|
||||
DocumentIds = string.Join(",", providerDocumentIds),
|
||||
SealId = process.SealId,
|
||||
AcrossType = acrossType
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var providerDocumentId in providerDocumentIds)
|
||||
{
|
||||
stampers.Add(new QiyuesuoStamperInput
|
||||
{
|
||||
ActionId = companyActionId,
|
||||
Type = "ACROSS_PAGE",
|
||||
DocumentId = providerDocumentId,
|
||||
SealId = process.SealId,
|
||||
AcrossType = acrossType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sent = await _qiyuesuoClient.Send(new QiyuesuoSendContractInput
|
||||
{
|
||||
ContractId = process.ProviderContractId,
|
||||
@@ -861,6 +1090,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
.Where(item => item.SignProcessId == process.Id)
|
||||
.OrderBy(item => item.DocumentSort)
|
||||
.ToListAsync();
|
||||
var previewedDocumentIds = await GetPreviewedDocumentIds(process.Id);
|
||||
result.ProcessId = process.Id;
|
||||
result.ProviderContractId = process.ProviderContractId;
|
||||
result.BizId = process.BizId;
|
||||
@@ -890,7 +1120,8 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
DocumentName = item.DocumentName,
|
||||
DocumentSort = item.DocumentSort,
|
||||
OriginalDocxFileId = item.OriginalDocxFileId,
|
||||
SignedPdfFileId = item.SignedPdfFileId
|
||||
SignedPdfFileId = item.SignedPdfFileId,
|
||||
EmployeeSignedPreviewed = previewedDocumentIds.Contains(item.Id)
|
||||
}).ToList();
|
||||
}
|
||||
else
|
||||
@@ -913,6 +1144,108 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取任务当前所属公司的法定名称。集团共用令牌时,该名称用于明确子公司主体。
|
||||
/// </summary>
|
||||
private async Task<string> GetCompanyLegalName(ObOnboardingTask task)
|
||||
{
|
||||
var company = await _db.Queryable<ObCompany>()
|
||||
.InSingleAsync(task.CompanyId)
|
||||
?? throw Oops.Bah("任务所属公司不存在");
|
||||
var companyName = string.IsNullOrWhiteSpace(company.LegalName)
|
||||
? company.CompanyName
|
||||
: company.LegalName;
|
||||
if (string.IsNullOrWhiteSpace(companyName))
|
||||
throw Oops.Bah("任务所属公司法定名称为空");
|
||||
return companyName.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前流程中已由管理员生成过员工签后预览的文档 Id。
|
||||
/// </summary>
|
||||
private async Task<HashSet<long>> GetPreviewedDocumentIds(long processId)
|
||||
{
|
||||
var eventTypes = await _db.Queryable<ObElectronicSignEvent>()
|
||||
.Where(item =>
|
||||
item.SignProcessId == processId
|
||||
&& item.EventSource == AdminPreviewEventSource
|
||||
&& item.ProcessResult == "已生成预览地址")
|
||||
.Select(item => item.EventType)
|
||||
.ToListAsync();
|
||||
var result = new HashSet<long>();
|
||||
foreach (var eventType in eventTypes)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(eventType)
|
||||
&& eventType.StartsWith(
|
||||
PreviewEventTypePrefix,
|
||||
StringComparison.Ordinal)
|
||||
&& long.TryParse(
|
||||
eventType[PreviewEventTypePrefix.Length..],
|
||||
out var documentId))
|
||||
{
|
||||
result.Add(documentId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将管理员预览、确认盖章等关键操作写入现有电子签事件表。
|
||||
/// 固定操作键生成固定哈希,可防止双击或重试留下重复审计记录。
|
||||
/// </summary>
|
||||
private async Task RecordAdminOperation(
|
||||
ObElectronicSignProcess process,
|
||||
string eventSource,
|
||||
string eventType,
|
||||
string payloadJson,
|
||||
string processResult)
|
||||
{
|
||||
var eventKey = $"{eventSource}:{process.Id}:{eventType}";
|
||||
var payloadHash = Convert.ToHexString(
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(eventKey)))
|
||||
.ToLowerInvariant();
|
||||
var existing = await _db.Queryable<ObElectronicSignEvent>()
|
||||
.FirstAsync(item => item.PayloadHash == payloadHash);
|
||||
if (existing != null)
|
||||
return;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var operationEvent = new ObElectronicSignEvent
|
||||
{
|
||||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||
SignProcessId = process.Id,
|
||||
PayloadHash = payloadHash,
|
||||
EventSource = eventSource,
|
||||
EventType = eventType,
|
||||
ProviderStatus = process.ProviderStatus,
|
||||
PayloadJson = payloadJson,
|
||||
ProcessResult = processResult,
|
||||
ReceivedAt = now,
|
||||
ProcessedAt = now,
|
||||
CreateTime = now,
|
||||
CreateUserId = UserManager.UserId,
|
||||
CreateUser = UserManager.Name,
|
||||
IsDelete = false
|
||||
};
|
||||
try
|
||||
{
|
||||
await _db.Insertable(operationEvent).ExecuteCommandAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 并发重复操作由 PayloadHash 唯一索引兜底;其他插入错误继续抛出。
|
||||
existing = await _db.Queryable<ObElectronicSignEvent>()
|
||||
.FirstAsync(item => item.PayloadHash == payloadHash);
|
||||
if (existing == null)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildPreviewEventType(long documentId)
|
||||
{
|
||||
return $"{PreviewEventTypePrefix}{documentId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 电子签管理沿用入职审核人权限,不新增另一套权限来源。
|
||||
/// </summary>
|
||||
@@ -1151,10 +1484,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
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))
|
||||
{
|
||||
@@ -1165,6 +1495,17 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
{
|
||||
throw Oops.Bah("请配置员工签名和企业盖章定位关键字");
|
||||
}
|
||||
if (_setting.CompanySealOffsetX <= -1m || _setting.CompanySealOffsetX >= 1m
|
||||
|| _setting.CompanySealOffsetY <= -1m || _setting.CompanySealOffsetY >= 1m)
|
||||
{
|
||||
throw Oops.Bah("企业盖章 X/Y 偏移量必须在 -1 与 1 之间");
|
||||
}
|
||||
if (_setting.EnableAcrossPageSeal
|
||||
&& !string.Equals(_setting.AcrossPageSealType, "ALL_DOCUMENT", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(_setting.AcrossPageSealType, "SINGLE_DOCUMENT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw Oops.Bah("契约锁骑缝章模式只能配置为 ALL_DOCUMENT 或 SINGLE_DOCUMENT");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureCanOpenEmployeeSignPage(
|
||||
|
||||
+13
-2
@@ -6,7 +6,7 @@ namespace SimpleAdmin.Application;
|
||||
public interface IQiyuesuoClient : ITransient
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建包含个人签署方和企业自动盖章动作的合同草稿。
|
||||
/// 创建包含个人签署方和企业人工盖章动作的合同草稿。
|
||||
/// </summary>
|
||||
Task<QiyuesuoContractResult> CreateDraft(
|
||||
QiyuesuoCreateDraftInput input);
|
||||
@@ -29,6 +29,12 @@ public interface IQiyuesuoClient : ITransient
|
||||
Task<string> GetSignPageUrl(
|
||||
QiyuesuoSignPageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前合同中指定文档的只读浏览页面地址。
|
||||
/// </summary>
|
||||
Task<string> GetViewPageUrl(
|
||||
QiyuesuoViewPageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 查询契约锁合同详情。
|
||||
/// </summary>
|
||||
@@ -39,10 +45,15 @@ public interface IQiyuesuoClient : ITransient
|
||||
/// </summary>
|
||||
Task Notice(string contractId);
|
||||
|
||||
/// <summary>
|
||||
/// 由后台管理员确认后执行企业盖章节点。
|
||||
/// </summary>
|
||||
Task CompanySign(QiyuesuoCompanySignInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 作废尚未完成的合同流程。
|
||||
/// </summary>
|
||||
Task Invalid(string contractId, string reason);
|
||||
Task Invalid(string contractId, string sealId, string reason);
|
||||
|
||||
/// <summary>
|
||||
/// 下载单份签署完成的 PDF 文档。
|
||||
|
||||
+54
-7
@@ -103,9 +103,9 @@ public class QiyuesuoClient : IQiyuesuoClient
|
||||
new
|
||||
{
|
||||
type = "COMPANY",
|
||||
name = "企业自动盖章",
|
||||
name = "企业人工盖章",
|
||||
serialNo = 0,
|
||||
autoSign = true,
|
||||
autoSign = false,
|
||||
sealId = input.SealId,
|
||||
corpSealIds = new[] { sealId },
|
||||
actionNo = "company_seal"
|
||||
@@ -185,6 +185,33 @@ public class QiyuesuoClient : IQiyuesuoClient
|
||||
throw Oops.Bah("契约锁未返回员工签署页面地址");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> GetViewPageUrl(
|
||||
QiyuesuoViewPageInput input)
|
||||
{
|
||||
var result = await GetJsonElement(
|
||||
"/v2/contract/viewurl",
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["contractId"] = input.ContractId,
|
||||
["documentId"] = input.DocumentId,
|
||||
["tenantName"] = input.CompanyName,
|
||||
["downloadButton"] = "false",
|
||||
["printButton"] = "false"
|
||||
});
|
||||
|
||||
if (result.ValueKind == JsonValueKind.String)
|
||||
return result.GetString();
|
||||
if (TryGetString(result, "url", out var url)
|
||||
|| TryGetString(result, "viewUrl", out url)
|
||||
|| TryGetString(result, "pageUrl", out url))
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
throw Oops.Bah("契约锁未返回合同浏览页面地址");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<QiyuesuoContractResult> GetContractDetail(string contractId)
|
||||
{
|
||||
@@ -212,14 +239,27 @@ public class QiyuesuoClient : IQiyuesuoClient
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Invalid(string contractId, string reason)
|
||||
public async Task CompanySign(QiyuesuoCompanySignInput input)
|
||||
{
|
||||
await PostJsonElement(
|
||||
"/v2/contract/companysign",
|
||||
new
|
||||
{
|
||||
contractId = input.ContractId,
|
||||
tenantName = input.CompanyName,
|
||||
sealId = input.SealId
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Invalid(string contractId, string sealId, string reason)
|
||||
{
|
||||
await PostJsonElement(
|
||||
"/v2/contract/invalid",
|
||||
new
|
||||
{
|
||||
contractId,
|
||||
sealId = _setting.SealId,
|
||||
sealId,
|
||||
reason,
|
||||
deleteDoc = false,
|
||||
autoSign = true
|
||||
@@ -318,6 +358,15 @@ public class QiyuesuoClient : IQiyuesuoClient
|
||||
private async Task<T> GetJson<T>(
|
||||
string path,
|
||||
Dictionary<string, string> query)
|
||||
{
|
||||
var result = await GetJsonElement(path, query);
|
||||
return result.Deserialize<T>(JsonOptions)
|
||||
?? throw Oops.Bah("契约锁返回数据为空");
|
||||
}
|
||||
|
||||
private async Task<JsonElement> GetJsonElement(
|
||||
string path,
|
||||
Dictionary<string, string> query)
|
||||
{
|
||||
ValidateSettings();
|
||||
using var request = new HttpRequestMessage(
|
||||
@@ -328,9 +377,7 @@ public class QiyuesuoClient : IQiyuesuoClient
|
||||
path,
|
||||
HttpMethod.Get,
|
||||
query);
|
||||
var result = await SendForJsonResult(request);
|
||||
return result.Deserialize<T>(JsonOptions)
|
||||
?? throw Oops.Bah("契约锁返回数据为空");
|
||||
return await SendForJsonResult(request);
|
||||
}
|
||||
|
||||
private async Task<byte[]> Download(
|
||||
|
||||
+43
-1
@@ -77,6 +77,12 @@ public class QiyuesuoStamperInput
|
||||
/// <summary>契约锁文档 Id。</summary>
|
||||
public string DocumentId { get; set; }
|
||||
|
||||
/// <summary>骑缝章覆盖的契约锁文档 Id,多个 Id 使用英文逗号分隔。</summary>
|
||||
public string DocumentIds { get; set; }
|
||||
|
||||
/// <summary>骑缝章位置指定方式。</summary>
|
||||
public string AcrossType { get; set; }
|
||||
|
||||
/// <summary>企业印章 Id。</summary>
|
||||
public string SealId { get; set; }
|
||||
|
||||
@@ -84,7 +90,13 @@ public class QiyuesuoStamperInput
|
||||
public string Keyword { get; set; }
|
||||
|
||||
/// <summary>同名关键字的序号,从 1 开始。</summary>
|
||||
public int KeywordIndex { get; set; } = 1;
|
||||
public int? KeywordIndex { get; set; }
|
||||
|
||||
/// <summary>相对关键字的横向偏移量;契约锁要求必传,不偏移时为 0。</summary>
|
||||
public decimal OffsetX { get; set; }
|
||||
|
||||
/// <summary>相对关键字的纵向偏移量;契约锁要求必传,不偏移时为 0。</summary>
|
||||
public decimal OffsetY { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,6 +132,36 @@ public class QiyuesuoSignPageInput
|
||||
public string CallbackPage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取契约锁合同文档只读浏览页所需参数。
|
||||
/// </summary>
|
||||
public class QiyuesuoViewPageInput
|
||||
{
|
||||
/// <summary>契约锁合同流程 Id。</summary>
|
||||
public string ContractId { get; set; }
|
||||
|
||||
/// <summary>契约锁合同文档 Id。</summary>
|
||||
public string DocumentId { get; set; }
|
||||
|
||||
/// <summary>签署企业法定名称;集团模式下用于定位子公司。</summary>
|
||||
public string CompanyName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 契约锁企业人工盖章所需参数。
|
||||
/// </summary>
|
||||
public class QiyuesuoCompanySignInput
|
||||
{
|
||||
/// <summary>契约锁合同流程 Id。</summary>
|
||||
public string ContractId { get; set; }
|
||||
|
||||
/// <summary>签署企业法定名称;集团模式下用于定位子公司。</summary>
|
||||
public string CompanyName { get; set; }
|
||||
|
||||
/// <summary>当前流程使用的企业印章 Id 快照。</summary>
|
||||
public string SealId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 契约锁合同返回对象。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职业务岗位业务能力。
|
||||
@@ -17,6 +17,7 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.PositionName), it => it.PositionName.Contains(input.PositionName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
||||
.OrderByDescending(it => it.CreateTime)
|
||||
.OrderBy(it => it.PositionName)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
|
||||
|
||||
+5
@@ -64,6 +64,7 @@ public class OnboardingContractTemplateController
|
||||
/// 查询入职模板当前公司、部门及岗位可选择的合同模板。
|
||||
/// </summary>
|
||||
[HttpPost("list-available")]
|
||||
[DisplayName("查询可选合同模板")]
|
||||
public async Task<dynamic> ListAvailable([FromBody] OnboardingContractTemplateAvailableInput input)
|
||||
{
|
||||
return await _service.ListAvailable(input);
|
||||
@@ -113,6 +114,7 @@ public class OnboardingContractTemplateController
|
||||
/// 新增合同模板版本。
|
||||
/// </summary>
|
||||
[HttpPost("version/add")]
|
||||
[DisplayName("新增合同模板版本")]
|
||||
public async Task AddVersion([FromBody] OnboardingContractVersionAddInput input)
|
||||
{
|
||||
await _service.AddVersion(input);
|
||||
@@ -122,6 +124,7 @@ public class OnboardingContractTemplateController
|
||||
/// 设置合同模板版本状态。
|
||||
/// </summary>
|
||||
[HttpPost("version/set-status")]
|
||||
[DisplayName("设置合同模板版本状态")]
|
||||
public async Task SetVersionStatus([FromBody] OnboardingContractVersionStatusInput input)
|
||||
{
|
||||
await _service.SetVersionStatus(input);
|
||||
@@ -131,6 +134,7 @@ public class OnboardingContractTemplateController
|
||||
/// 删除未被入职模板或任务使用的合同模板版本。
|
||||
/// </summary>
|
||||
[HttpPost("version/delete")]
|
||||
[DisplayName("删除合同模板版本")]
|
||||
public async Task DeleteVersion([FromBody] BaseIdInput input)
|
||||
{
|
||||
await _service.DeleteVersion(input);
|
||||
@@ -150,6 +154,7 @@ public class OnboardingContractTemplateController
|
||||
/// 编辑模板变量。
|
||||
/// </summary>
|
||||
[HttpPost("variable/edit")]
|
||||
[DisplayName("编辑合同模板变量")]
|
||||
public async Task EditVariable([FromBody] OnboardingTemplateVariableEditInput input)
|
||||
{
|
||||
await _service.EditVariable(input);
|
||||
|
||||
+22
@@ -73,6 +73,28 @@ public class OnboardingElectronicSignController
|
||||
return await _service.GetSignLink(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取员工已签、企业未盖章文件的只读浏览页面。
|
||||
/// </summary>
|
||||
[HttpPost("preview-employee-signed")]
|
||||
[DisplayName("预览员工已签合同")]
|
||||
public async Task<OnboardingElectronicSignPreviewOutput> PreviewEmployeeSignedDocument(
|
||||
[FromBody] OnboardingElectronicSignPreviewInput input)
|
||||
{
|
||||
return await _service.PreviewEmployeeSignedDocument(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全部文件核对完成后,由后台管理员确认企业盖章。
|
||||
/// </summary>
|
||||
[HttpPost("company-seal")]
|
||||
[DisplayName("确认入职合同企业盖章")]
|
||||
public async Task<OnboardingElectronicSignDetailOutput> CompanySeal(
|
||||
[FromBody] OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
return await _service.CompanySeal(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 催办当前待签员工。
|
||||
/// </summary>
|
||||
|
||||
@@ -51,6 +51,7 @@ public class BizUserController : IDynamicApiController
|
||||
/// <returns></returns>
|
||||
[HttpGet(template: "template")]
|
||||
[SuppressMonitor]
|
||||
[DisplayName("下载人员导入模板")]
|
||||
public async Task<dynamic> Template()
|
||||
{
|
||||
return await _userService.Template();
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
-- OnboardX 契约锁公司级业务分类与印章配置
|
||||
-- 适用数据库:MySQL 8.0+
|
||||
-- 可重复执行;仅增加字段,不覆盖任何公司已有配置。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `ob_qys_company_add_column_if_missing`;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE `ob_qys_company_add_column_if_missing`(
|
||||
IN p_column_name VARCHAR(128),
|
||||
IN p_column_definition VARCHAR(1000)
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'ob_company'
|
||||
AND COLUMN_NAME = p_column_name
|
||||
) THEN
|
||||
SET @ddl = CONCAT(
|
||||
'ALTER TABLE `ob_company` ADD COLUMN `',
|
||||
p_column_name, '` ', p_column_definition
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL `ob_qys_company_add_column_if_missing`(
|
||||
'QiyuesuoCategoryId',
|
||||
'VARCHAR(100) NULL COMMENT ''契约锁默认业务分类Id(按公司独立配置)'' AFTER `ContactPhone`'
|
||||
);
|
||||
CALL `ob_qys_company_add_column_if_missing`(
|
||||
'QiyuesuoSealId',
|
||||
'VARCHAR(100) NULL COMMENT ''契约锁企业印章Id(按公司独立配置)'' AFTER `QiyuesuoCategoryId`'
|
||||
);
|
||||
|
||||
DROP PROCEDURE IF EXISTS `ob_qys_company_add_column_if_missing`;
|
||||
|
||||
SELECT
|
||||
Id,
|
||||
CompanyCode,
|
||||
CompanyName,
|
||||
LegalName,
|
||||
QiyuesuoCategoryId,
|
||||
QiyuesuoSealId,
|
||||
CASE
|
||||
WHEN NULLIF(TRIM(QiyuesuoCategoryId), '') IS NOT NULL
|
||||
AND NULLIF(TRIM(QiyuesuoSealId), '') IS NOT NULL THEN '已配置'
|
||||
ELSE '待配置'
|
||||
END AS QiyuesuoConfigStatus
|
||||
FROM ob_company
|
||||
WHERE IsDelete = 0 OR IsDelete IS NULL
|
||||
ORDER BY CompanyName;
|
||||
@@ -0,0 +1,215 @@
|
||||
-- OnboardX 入职人员与电子签约业务数据清理脚本
|
||||
-- 适用数据库:MySQL 8.0+
|
||||
--
|
||||
-- 会物理删除:
|
||||
-- 1. 正式员工档案;
|
||||
-- 2. 入职任务及候选人填写、材料、审核、合同快照等任务数据;
|
||||
-- 3. 契约锁流程、文档映射、回调及后台预览/盖章事件。
|
||||
--
|
||||
-- 会保留:
|
||||
-- 公司、部门、岗位、入职模板、合同模板及版本、模板适用范围、
|
||||
-- 合同变量字典、模板变量、上传资料类型及其他系统基础配置。
|
||||
--
|
||||
-- 特别注意:
|
||||
-- 1. 执行前请先备份数据库;本脚本提交后不可恢复。
|
||||
-- 2. 请先在契约锁作废仍在签署中的测试流程,或临时关闭回调;
|
||||
-- 本脚本只清理本地数据库,不能删除或作废契约锁平台上的合同。
|
||||
-- 3. 本脚本不删除 sys_file/dev_file 等系统文件记录,也不删除 OSS 对象,
|
||||
-- 避免误删合同模板源文件或其他模块共用文件。
|
||||
-- 4. DELETE 会同时清理正常数据和已经软删除的数据。
|
||||
|
||||
USE `OnboardX`;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `ob_reset_onboarding_personnel_and_sign_data`;
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
CREATE PROCEDURE `ob_reset_onboarding_personnel_and_sign_data`()
|
||||
BEGIN
|
||||
DECLARE old_foreign_key_checks INT DEFAULT 1;
|
||||
DECLARE finished INT DEFAULT 0;
|
||||
DECLARE target_table_name VARCHAR(64);
|
||||
DECLARE before_rows BIGINT DEFAULT 0;
|
||||
DECLARE after_rows BIGINT DEFAULT 0;
|
||||
|
||||
-- 只允许删除明确列出的流程数据表。新版本即使增加 ob_ 基础表,也不会被误删。
|
||||
DECLARE target_table_cursor CURSOR FOR
|
||||
SELECT `TABLE_NAME`
|
||||
FROM `information_schema`.`TABLES`
|
||||
WHERE `TABLE_SCHEMA` = DATABASE()
|
||||
AND `TABLE_TYPE` = 'BASE TABLE'
|
||||
AND `TABLE_NAME` IN (
|
||||
'ob_electronic_sign_event',
|
||||
'ob_electronic_sign_document',
|
||||
'ob_electronic_sign_process',
|
||||
'ob_task_operation_log',
|
||||
'ob_task_audit',
|
||||
'ob_task_material',
|
||||
'ob_task_form_value',
|
||||
'ob_task_employee_info',
|
||||
'ob_task_contract_variable',
|
||||
'ob_task_contract',
|
||||
'ob_task_link',
|
||||
'ob_task_material_requirement',
|
||||
'ob_task_form_requirement',
|
||||
'ob_onboarding_task',
|
||||
'ob_employee'
|
||||
)
|
||||
ORDER BY FIELD(
|
||||
`TABLE_NAME`,
|
||||
'ob_electronic_sign_event',
|
||||
'ob_electronic_sign_document',
|
||||
'ob_electronic_sign_process',
|
||||
'ob_task_operation_log',
|
||||
'ob_task_audit',
|
||||
'ob_task_material',
|
||||
'ob_task_form_value',
|
||||
'ob_task_employee_info',
|
||||
'ob_task_contract_variable',
|
||||
'ob_task_contract',
|
||||
'ob_task_link',
|
||||
'ob_task_material_requirement',
|
||||
'ob_task_form_requirement',
|
||||
'ob_onboarding_task',
|
||||
'ob_employee'
|
||||
);
|
||||
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
|
||||
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
SET FOREIGN_KEY_CHECKS = old_foreign_key_checks;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
IF LOWER(DATABASE()) <> 'onboardx' THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '当前数据库不是 OnboardX,已拒绝执行人员与签约数据清理';
|
||||
END IF;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS `tmp_ob_reset_result`;
|
||||
CREATE TEMPORARY TABLE `tmp_ob_reset_result` (
|
||||
`TableName` VARCHAR(64) NOT NULL,
|
||||
`BeforeRows` BIGINT NOT NULL,
|
||||
`DeletedRows` BIGINT NOT NULL,
|
||||
`AfterRows` BIGINT NOT NULL,
|
||||
PRIMARY KEY (`TableName`)
|
||||
);
|
||||
|
||||
SET old_foreign_key_checks = @@FOREIGN_KEY_CHECKS;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
OPEN target_table_cursor;
|
||||
|
||||
delete_loop: LOOP
|
||||
FETCH target_table_cursor INTO target_table_name;
|
||||
IF finished = 1 THEN
|
||||
LEAVE delete_loop;
|
||||
END IF;
|
||||
|
||||
SET @ob_reset_row_count = 0;
|
||||
SET @ob_reset_count_sql = CONCAT(
|
||||
'SELECT COUNT(*) INTO @ob_reset_row_count FROM `',
|
||||
REPLACE(target_table_name, '`', '``'),
|
||||
'`'
|
||||
);
|
||||
PREPARE ob_reset_count_statement FROM @ob_reset_count_sql;
|
||||
EXECUTE ob_reset_count_statement;
|
||||
DEALLOCATE PREPARE ob_reset_count_statement;
|
||||
SET before_rows = COALESCE(@ob_reset_row_count, 0);
|
||||
|
||||
SET @ob_reset_delete_sql = CONCAT(
|
||||
'DELETE FROM `',
|
||||
REPLACE(target_table_name, '`', '``'),
|
||||
'`'
|
||||
);
|
||||
PREPARE ob_reset_delete_statement FROM @ob_reset_delete_sql;
|
||||
EXECUTE ob_reset_delete_statement;
|
||||
DEALLOCATE PREPARE ob_reset_delete_statement;
|
||||
|
||||
SET @ob_reset_row_count = 0;
|
||||
PREPARE ob_reset_count_statement FROM @ob_reset_count_sql;
|
||||
EXECUTE ob_reset_count_statement;
|
||||
DEALLOCATE PREPARE ob_reset_count_statement;
|
||||
SET after_rows = COALESCE(@ob_reset_row_count, 0);
|
||||
|
||||
INSERT INTO `tmp_ob_reset_result` (
|
||||
`TableName`,
|
||||
`BeforeRows`,
|
||||
`DeletedRows`,
|
||||
`AfterRows`
|
||||
) VALUES (
|
||||
target_table_name,
|
||||
before_rows,
|
||||
before_rows - after_rows,
|
||||
after_rows
|
||||
);
|
||||
END LOOP;
|
||||
|
||||
CLOSE target_table_cursor;
|
||||
|
||||
COMMIT;
|
||||
SET FOREIGN_KEY_CHECKS = old_foreign_key_checks;
|
||||
|
||||
SELECT
|
||||
`TableName` AS `已清理表`,
|
||||
`BeforeRows` AS `清理前`,
|
||||
`DeletedRows` AS `删除数量`,
|
||||
`AfterRows` AS `清理后`
|
||||
FROM `tmp_ob_reset_result`
|
||||
ORDER BY FIELD(
|
||||
`TableName`,
|
||||
'ob_electronic_sign_event',
|
||||
'ob_electronic_sign_document',
|
||||
'ob_electronic_sign_process',
|
||||
'ob_task_operation_log',
|
||||
'ob_task_audit',
|
||||
'ob_task_material',
|
||||
'ob_task_form_value',
|
||||
'ob_task_employee_info',
|
||||
'ob_task_contract_variable',
|
||||
'ob_task_contract',
|
||||
'ob_task_link',
|
||||
'ob_task_material_requirement',
|
||||
'ob_task_form_requirement',
|
||||
'ob_onboarding_task',
|
||||
'ob_employee'
|
||||
);
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS `tmp_ob_reset_result`;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL `ob_reset_onboarding_personnel_and_sign_data`();
|
||||
DROP PROCEDURE IF EXISTS `ob_reset_onboarding_personnel_and_sign_data`;
|
||||
|
||||
-- 最终校验:下面存在的流程表都应为 0 行。
|
||||
SELECT
|
||||
`TABLE_NAME` AS `流程数据表`,
|
||||
`TABLE_ROWS` AS `估算剩余行数`
|
||||
FROM `information_schema`.`TABLES`
|
||||
WHERE `TABLE_SCHEMA` = DATABASE()
|
||||
AND `TABLE_TYPE` = 'BASE TABLE'
|
||||
AND `TABLE_NAME` IN (
|
||||
'ob_electronic_sign_event',
|
||||
'ob_electronic_sign_document',
|
||||
'ob_electronic_sign_process',
|
||||
'ob_task_operation_log',
|
||||
'ob_task_audit',
|
||||
'ob_task_material',
|
||||
'ob_task_form_value',
|
||||
'ob_task_employee_info',
|
||||
'ob_task_contract_variable',
|
||||
'ob_task_contract',
|
||||
'ob_task_link',
|
||||
'ob_task_material_requirement',
|
||||
'ob_task_form_requirement',
|
||||
'ob_onboarding_task',
|
||||
'ob_employee'
|
||||
)
|
||||
ORDER BY `TABLE_NAME`;
|
||||
@@ -0,0 +1,40 @@
|
||||
-- 契约锁测试环境公司配置初始化
|
||||
-- 先执行 migrations/20260908_qiyuesuo_company_config.sql。
|
||||
-- 本脚本包含测试环境 Id;不要直接用于契约锁正式环境。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
UPDATE ob_company
|
||||
SET QiyuesuoCategoryId = '3533041076673835142',
|
||||
QiyuesuoSealId = '3533379903657587306',
|
||||
UpdateTime = NOW(3)
|
||||
WHERE (IsDelete = 0 OR IsDelete IS NULL)
|
||||
AND (
|
||||
TRIM(CompanyName) = '重庆远轩教育科技公司'
|
||||
OR TRIM(LegalName) = '重庆远轩教育科技公司'
|
||||
);
|
||||
|
||||
UPDATE ob_company
|
||||
SET QiyuesuoCategoryId = '3534511390926574114',
|
||||
QiyuesuoSealId = '3534750711407686595',
|
||||
UpdateTime = NOW(3)
|
||||
WHERE (IsDelete = 0 OR IsDelete IS NULL)
|
||||
AND (
|
||||
TRIM(CompanyName) = '重庆海豚科技公司'
|
||||
OR TRIM(LegalName) = '重庆海豚科技公司'
|
||||
);
|
||||
|
||||
SELECT
|
||||
Id,
|
||||
CompanyCode,
|
||||
CompanyName,
|
||||
LegalName AS QiyuesuoTenantName,
|
||||
QiyuesuoCategoryId,
|
||||
QiyuesuoSealId
|
||||
FROM ob_company
|
||||
WHERE (IsDelete = 0 OR IsDelete IS NULL)
|
||||
AND (
|
||||
TRIM(CompanyName) IN ('重庆远轩教育科技公司', '重庆海豚科技公司')
|
||||
OR TRIM(LegalName) IN ('重庆远轩教育科技公司', '重庆海豚科技公司')
|
||||
)
|
||||
ORDER BY CompanyName;
|
||||
Reference in New Issue
Block a user