新增公司盖章与骑缝章配置
This commit is contained in:
@@ -52,6 +52,12 @@ public class ObContractTemplateVersion : BaseEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string VersionStatus { get; set; } = ContractTemplateVersionStatus.Draft;
|
public string VersionStatus { get; set; } = ContractTemplateVersionStatus.Draft;
|
||||||
|
|
||||||
|
/// <summary>本版本是否需要公司盖章;历史空值沿用需要盖章。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本版本是否需要骑缝章;历史空值沿用系统骑缝章配置。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 业务备注。
|
/// 业务备注。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ public class ObElectronicSignDocument : BaseEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string CompanySealKeyword { get; set; }
|
public string CompanySealKeyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>发起本次签约时冻结的公司盖章要求。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>发起本次签约时冻结的骑缝章要求。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 契约锁员工签署动作 Id。
|
/// 契约锁员工签署动作 Id。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ public class ObTaskContract : BaseEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int PrintCopies { get; set; } = 1;
|
public int PrintCopies { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>任务创建时冻结的公司盖章要求;历史空值沿用需要盖章。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>任务创建时冻结的骑缝章要求;历史空值沿用系统配置。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务合同当前状态。
|
/// 任务合同当前状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ public class QiyuesuoSettingOptions
|
|||||||
public decimal CompanySealOffsetY { get; set; }
|
public decimal CompanySealOffsetY { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 是否在后台确认企业盖章时同时加盖骑缝章。
|
/// 历史版本未单独设置开关时,是否在企业盖章时加盖骑缝章;新版本显式开关优先。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableAcrossPageSeal { get; set; } = true;
|
public bool EnableAcrossPageSeal { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 骑缝章文档范围:ALL_DOCUMENT(全部文档合并)或 SINGLE_DOCUMENT(每份文档单独)。
|
/// 骑缝章方式:ALL_DOCUMENT(所选文档合并)或 SINGLE_DOCUMENT(所选文档各自独立)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AcrossPageSealType { get; set; } = "ALL_DOCUMENT";
|
public string AcrossPageSealType { get; set; } = "ALL_DOCUMENT";
|
||||||
|
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>版本盖章配置局部更新,避免覆盖另一个开关、版本状态或历史任务快照。</summary>
|
||||||
|
internal static class ContractTemplateVersionSealConfiguration
|
||||||
|
{
|
||||||
|
internal static IUpdateable<ObContractTemplateVersion> BuildUpdate(
|
||||||
|
ISqlSugarClient db,
|
||||||
|
OnboardingContractVersionSealInput input)
|
||||||
|
{
|
||||||
|
if (input.Id <= 0)
|
||||||
|
throw Oops.Bah("版本Id不能为空");
|
||||||
|
if (!input.CompanySealEnabled.HasValue && !input.AcrossPageSealEnabled.HasValue)
|
||||||
|
throw Oops.Bah("请指定需要修改的盖章开关");
|
||||||
|
|
||||||
|
var update = db.Updateable<ObContractTemplateVersion>()
|
||||||
|
.Where(item => item.Id == input.Id);
|
||||||
|
if (input.CompanySealEnabled.HasValue)
|
||||||
|
update = update.SetColumns(item => item.CompanySealEnabled == input.CompanySealEnabled);
|
||||||
|
if (input.AcrossPageSealEnabled.HasValue)
|
||||||
|
update = update.SetColumns(item => item.AcrossPageSealEnabled == input.AcrossPageSealEnabled);
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -55,6 +55,12 @@ public class OnboardingContractVersionAddInput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? ExpiryDate { get; set; }
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本版本是否需要公司盖章;旧客户端未传时沿用原规则。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本版本是否需要骑缝章;旧客户端未传时沿用系统配置。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 业务备注。
|
/// 业务备注。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -78,3 +84,19 @@ public class OnboardingContractVersionStatusInput
|
|||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "版本状态不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "版本状态不能为空")]
|
||||||
public string VersionStatus { get; set; }
|
public string VersionStatus { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改合同模板版本的盖章配置;未传的开关保持不变。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractVersionSealInput
|
||||||
|
{
|
||||||
|
/// <summary>合同版本 Id。</summary>
|
||||||
|
[IdNotNull(ErrorMessage = "版本Id不能为空")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否需要公司盖章。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否需要骑缝章。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
+5
@@ -50,6 +50,11 @@ public interface IOnboardingContractTemplateService : ITransient
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task SetVersionStatus(OnboardingContractVersionStatusInput input);
|
Task SetVersionStatus(OnboardingContractVersionStatusInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改合同模板版本的盖章配置,仅影响后续新建任务。
|
||||||
|
/// </summary>
|
||||||
|
Task SetVersionSeals(OnboardingContractVersionSealInput input);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 删除未被入职模板或任务使用的合同版本。
|
/// 删除未被入职模板或任务使用的合同版本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+26
-2
@@ -272,11 +272,21 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input)
|
public async Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input)
|
||||||
{
|
{
|
||||||
return await Context.Queryable<ObContractTemplateVersion>()
|
var page = await Context.Queryable<ObContractTemplateVersion>()
|
||||||
.Where(it => it.TemplateId == input.TemplateId)
|
.Where(it => it.TemplateId == input.TemplateId)
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(input.VersionStatus), it => it.VersionStatus == input.VersionStatus)
|
.WhereIF(!string.IsNullOrWhiteSpace(input.VersionStatus), it => it.VersionStatus == input.VersionStatus)
|
||||||
.OrderByDescending(it => it.CreateTime)
|
.OrderByDescending(it => it.CreateTime)
|
||||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||||
|
|
||||||
|
// 仅解析返回值,不回写历史空值,使列表开关显示真实生效的规则。
|
||||||
|
var setting = App.GetConfig<QiyuesuoSettingOptions>("QiyuesuoSetting", true)
|
||||||
|
?? new QiyuesuoSettingOptions();
|
||||||
|
foreach (var version in page.List)
|
||||||
|
{
|
||||||
|
version.CompanySealEnabled = QiyuesuoContractRequestBuilder.CompanySealEnabled(version.CompanySealEnabled);
|
||||||
|
version.AcrossPageSealEnabled = QiyuesuoContractRequestBuilder.AcrossPageSealEnabled(version.AcrossPageSealEnabled, setting);
|
||||||
|
}
|
||||||
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -395,7 +405,21 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
|||||||
}
|
}
|
||||||
|
|
||||||
version.VersionStatus = input.VersionStatus;
|
version.VersionStatus = input.VersionStatus;
|
||||||
await Context.Updateable(version).ExecuteCommandAsync();
|
await Context.Updateable(version)
|
||||||
|
.UpdateColumns(item => new { item.VersionStatus, item.UpdateTime, item.UpdateUserId, item.UpdateUser })
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改版本盖章开关;任务和电子签约已经保存独立快照,不随版本变更。
|
||||||
|
/// </summary>
|
||||||
|
public async Task SetVersionSeals(OnboardingContractVersionSealInput input)
|
||||||
|
{
|
||||||
|
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
|
||||||
|
?? throw Oops.Bah("模板版本不存在");
|
||||||
|
await EnsureTemplateCanManage(version.TemplateId);
|
||||||
|
|
||||||
|
await ContractTemplateVersionSealConfiguration.BuildUpdate(Context, input).ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+6
@@ -164,4 +164,10 @@ public class OnboardingElectronicSignDocumentOutput
|
|||||||
|
|
||||||
/// <summary>当前管理员流程中是否已生成过员工签后预览。</summary>
|
/// <summary>当前管理员流程中是否已生成过员工签后预览。</summary>
|
||||||
public bool EmployeeSignedPreviewed { get; set; }
|
public bool EmployeeSignedPreviewed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本份文件是否需要公司盖章。</summary>
|
||||||
|
public bool CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本份文件是否需要骑缝章。</summary>
|
||||||
|
public bool AcrossPageSealEnabled { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-84
@@ -805,7 +805,10 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
SealId = process.SealId,
|
SealId = process.SealId,
|
||||||
CreatorName = process.CreatorName,
|
CreatorName = process.CreatorName,
|
||||||
CreatorMobile = process.CreatorMobile,
|
CreatorMobile = process.CreatorMobile,
|
||||||
CallbackUrl = _setting.CallbackUrl
|
CallbackUrl = _setting.CallbackUrl,
|
||||||
|
RequiresCompanySign = taskContracts.Any(item =>
|
||||||
|
QiyuesuoContractRequestBuilder.RequiresCompanySign(
|
||||||
|
item.CompanySealEnabled, item.AcrossPageSealEnabled, _setting))
|
||||||
});
|
});
|
||||||
process.ProviderContractId = GetContractId(providerContract);
|
process.ProviderContractId = GetContractId(providerContract);
|
||||||
process.ProviderStatus = providerContract.Status ?? "DRAFT";
|
process.ProviderStatus = providerContract.Status ?? "DRAFT";
|
||||||
@@ -860,7 +863,9 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
DocumentSort = index + 1,
|
DocumentSort = index + 1,
|
||||||
OriginalDocxFileId = originalFileId,
|
OriginalDocxFileId = originalFileId,
|
||||||
EmployeeSignKeyword = _setting.EmployeeSignKeyword.Trim(),
|
EmployeeSignKeyword = _setting.EmployeeSignKeyword.Trim(),
|
||||||
CompanySealKeyword = _setting.CompanySealKeyword.Trim(),
|
CompanySealKeyword = _setting.CompanySealKeyword?.Trim(),
|
||||||
|
CompanySealEnabled = QiyuesuoContractRequestBuilder.CompanySealEnabled(taskContract.CompanySealEnabled),
|
||||||
|
AcrossPageSealEnabled = QiyuesuoContractRequestBuilder.AcrossPageSealEnabled(taskContract.AcrossPageSealEnabled, _setting),
|
||||||
Status = CommonStatusConst.ENABLE,
|
Status = CommonStatusConst.ENABLE,
|
||||||
CreateTime = DateTime.Now,
|
CreateTime = DateTime.Now,
|
||||||
CreateUserId = process.CreateUserId,
|
CreateUserId = process.CreateUserId,
|
||||||
@@ -879,7 +884,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为每份文档分别配置员工签名与企业盖章位置,然后一次性发送整个流程。
|
/// 全部文档配置员工签名,按每份文件的快照配置企业盖章及骑缝章位置。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SendProcess(
|
private async Task SendProcess(
|
||||||
ObElectronicSignProcess process,
|
ObElectronicSignProcess process,
|
||||||
@@ -891,79 +896,40 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
var personalSignatory = providerContract.Signatories.FirstOrDefault(item =>
|
var personalSignatory = providerContract.Signatories.FirstOrDefault(item =>
|
||||||
string.Equals(item.TenantType, "PERSONAL", StringComparison.OrdinalIgnoreCase))
|
string.Equals(item.TenantType, "PERSONAL", StringComparison.OrdinalIgnoreCase))
|
||||||
?? throw Oops.Bah("契约锁草稿未返回员工签署方");
|
?? 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>();
|
|
||||||
var orderedDocuments = documents.OrderBy(item => item.DocumentSort).ToList();
|
var orderedDocuments = documents.OrderBy(item => item.DocumentSort).ToList();
|
||||||
|
var requiresCompanySign = orderedDocuments.Any(item =>
|
||||||
|
QiyuesuoContractRequestBuilder.RequiresCompanySign(
|
||||||
|
item.CompanySealEnabled, item.AcrossPageSealEnabled, _setting));
|
||||||
|
var companyAction = providerContract.Signatories
|
||||||
|
.Where(item => string.Equals(item.TenantType, "COMPANY", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.SelectMany(item => item.Actions)
|
||||||
|
.FirstOrDefault(item => string.Equals(item.Type, "COMPANY", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (requiresCompanySign && companyAction == null)
|
||||||
|
throw Oops.Bah("契约锁草稿未返回企业盖章动作");
|
||||||
|
if (!requiresCompanySign && companyAction != null)
|
||||||
|
throw Oops.Bah("本次材料仅需员工签字,但契约锁业务分类仍预设企业盖章节点,请先调整该业务分类的签署流程");
|
||||||
|
|
||||||
|
var personalSignatoryId = GetSignatoryId(personalSignatory);
|
||||||
|
var companyActionId = companyAction == null ? null : GetActionId(companyAction);
|
||||||
|
var signingDocuments = orderedDocuments.Select(item =>
|
||||||
|
new OnboardingSigningDocument(
|
||||||
|
item.ProviderDocumentId,
|
||||||
|
item.EmployeeSignKeyword,
|
||||||
|
item.CompanySealKeyword,
|
||||||
|
item.CompanySealEnabled,
|
||||||
|
item.AcrossPageSealEnabled)).ToList();
|
||||||
|
var stampers = QiyuesuoContractRequestBuilder.BuildStampers(
|
||||||
|
signingDocuments, personalSignatoryId, companyActionId, process.SealId, _setting);
|
||||||
|
|
||||||
foreach (var document in orderedDocuments)
|
foreach (var document in orderedDocuments)
|
||||||
{
|
{
|
||||||
document.EmployeeActionId = personalSignatoryId;
|
document.EmployeeActionId = personalSignatoryId;
|
||||||
document.CompanyActionId = companyActionId;
|
document.CompanyActionId = QiyuesuoContractRequestBuilder.RequiresCompanySign(
|
||||||
|
document.CompanySealEnabled, document.AcrossPageSealEnabled, _setting)
|
||||||
|
? companyActionId
|
||||||
|
: null;
|
||||||
document.UpdateTime = DateTime.Now;
|
document.UpdateTime = DateTime.Now;
|
||||||
await _db.Updateable(document).ExecuteCommandAsync();
|
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),
|
|
||||||
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
|
var sent = await _qiyuesuoClient.Send(new QiyuesuoSendContractInput
|
||||||
@@ -1179,7 +1145,9 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
DocumentName = item.ContractName,
|
DocumentName = item.ContractName,
|
||||||
DocumentSort = index + 1,
|
DocumentSort = index + 1,
|
||||||
OriginalDocxFileId = item.GeneratedDocxFileId ?? 0,
|
OriginalDocxFileId = item.GeneratedDocxFileId ?? 0,
|
||||||
SignedPdfFileId = item.GeneratedPdfFileId
|
SignedPdfFileId = item.GeneratedPdfFileId,
|
||||||
|
CompanySealEnabled = QiyuesuoContractRequestBuilder.CompanySealEnabled(item.CompanySealEnabled),
|
||||||
|
AcrossPageSealEnabled = QiyuesuoContractRequestBuilder.AcrossPageSealEnabled(item.AcrossPageSealEnabled, _setting)
|
||||||
}).ToList();
|
}).ToList();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -1219,6 +1187,8 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
DocumentSort = item.DocumentSort,
|
DocumentSort = item.DocumentSort,
|
||||||
OriginalDocxFileId = item.OriginalDocxFileId,
|
OriginalDocxFileId = item.OriginalDocxFileId,
|
||||||
SignedPdfFileId = item.SignedPdfFileId,
|
SignedPdfFileId = item.SignedPdfFileId,
|
||||||
|
CompanySealEnabled = QiyuesuoContractRequestBuilder.CompanySealEnabled(item.CompanySealEnabled),
|
||||||
|
AcrossPageSealEnabled = QiyuesuoContractRequestBuilder.AcrossPageSealEnabled(item.AcrossPageSealEnabled, _setting),
|
||||||
EmployeeSignedPreviewed = previewedDocumentIds.Contains(item.Id)
|
EmployeeSignedPreviewed = previewedDocumentIds.Contains(item.Id)
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
@@ -1236,7 +1206,9 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
DocumentName = item.ContractName,
|
DocumentName = item.ContractName,
|
||||||
DocumentSort = index + 1,
|
DocumentSort = index + 1,
|
||||||
OriginalDocxFileId = item.GeneratedDocxFileId ?? 0,
|
OriginalDocxFileId = item.GeneratedDocxFileId ?? 0,
|
||||||
SignedPdfFileId = item.GeneratedPdfFileId
|
SignedPdfFileId = item.GeneratedPdfFileId,
|
||||||
|
CompanySealEnabled = QiyuesuoContractRequestBuilder.CompanySealEnabled(item.CompanySealEnabled),
|
||||||
|
AcrossPageSealEnabled = QiyuesuoContractRequestBuilder.AcrossPageSealEnabled(item.AcrossPageSealEnabled, _setting)
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -1431,15 +1403,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
if (status == "DRAFT")
|
if (status == "DRAFT")
|
||||||
return ElectronicSignStatus.Draft;
|
return ElectronicSignStatus.Draft;
|
||||||
|
|
||||||
var personal = providerContract.Signatories.FirstOrDefault(item =>
|
return QiyuesuoContractRequestBuilder.ResolvePendingStatus(providerContract);
|
||||||
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)
|
private static bool IsTerminal(string status)
|
||||||
@@ -1588,10 +1552,9 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
|||||||
{
|
{
|
||||||
throw Oops.Bah("请配置契约锁企业成员姓名和手机号");
|
throw Oops.Bah("请配置契约锁企业成员姓名和手机号");
|
||||||
}
|
}
|
||||||
if (string.IsNullOrWhiteSpace(_setting.EmployeeSignKeyword)
|
if (string.IsNullOrWhiteSpace(_setting.EmployeeSignKeyword))
|
||||||
|| string.IsNullOrWhiteSpace(_setting.CompanySealKeyword))
|
|
||||||
{
|
{
|
||||||
throw Oops.Bah("请配置员工签名和企业盖章定位关键字");
|
throw Oops.Bah("请配置员工签名定位关键字");
|
||||||
}
|
}
|
||||||
if (_setting.CompanySealOffsetX <= -1m || _setting.CompanySealOffsetX >= 1m
|
if (_setting.CompanySealOffsetX <= -1m || _setting.CompanySealOffsetX >= 1m
|
||||||
|| _setting.CompanySealOffsetY <= -1m || _setting.CompanySealOffsetY >= 1m)
|
|| _setting.CompanySealOffsetY <= -1m || _setting.CompanySealOffsetY >= 1m)
|
||||||
|
|||||||
+1
-76
@@ -40,84 +40,9 @@ public class QiyuesuoClient : IQiyuesuoClient
|
|||||||
public async Task<QiyuesuoContractResult> CreateDraft(
|
public async Task<QiyuesuoContractResult> CreateDraft(
|
||||||
QiyuesuoCreateDraftInput input)
|
QiyuesuoCreateDraftInput input)
|
||||||
{
|
{
|
||||||
if (!long.TryParse(input.SealId, out var sealId))
|
|
||||||
throw Oops.Bah("契约锁 SealId 配置格式不正确");
|
|
||||||
|
|
||||||
var body = new
|
|
||||||
{
|
|
||||||
bizId = input.BizId,
|
|
||||||
subject = input.Subject,
|
|
||||||
tenantName = input.CompanyName,
|
|
||||||
description = "OnboardX 入职电子签",
|
|
||||||
send = false,
|
|
||||||
callbackUrl = NullIfBlank(input.CallbackUrl),
|
|
||||||
category = new
|
|
||||||
{
|
|
||||||
id = input.CategoryId
|
|
||||||
},
|
|
||||||
creator = new
|
|
||||||
{
|
|
||||||
name = input.CreatorName,
|
|
||||||
contact = input.CreatorMobile,
|
|
||||||
contactType = "MOBILE"
|
|
||||||
},
|
|
||||||
signatories = new object[]
|
|
||||||
{
|
|
||||||
new
|
|
||||||
{
|
|
||||||
tenantType = "PERSONAL",
|
|
||||||
tenantName = input.EmployeeName,
|
|
||||||
receiver = new
|
|
||||||
{
|
|
||||||
name = input.EmployeeName,
|
|
||||||
contact = input.EmployeeMobile,
|
|
||||||
contactType = "MOBILE"
|
|
||||||
},
|
|
||||||
serialNo = 1,
|
|
||||||
signatoryNo = "employee",
|
|
||||||
actions = new object[]
|
|
||||||
{
|
|
||||||
new
|
|
||||||
{
|
|
||||||
type = "PERSONAL",
|
|
||||||
name = "员工本人签署",
|
|
||||||
serialNo = 0,
|
|
||||||
actionNo = "employee_sign"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new
|
|
||||||
{
|
|
||||||
tenantType = "COMPANY",
|
|
||||||
tenantName = input.CompanyName,
|
|
||||||
receiver = new
|
|
||||||
{
|
|
||||||
name = input.CreatorName,
|
|
||||||
contact = input.CreatorMobile,
|
|
||||||
contactType = "MOBILE"
|
|
||||||
},
|
|
||||||
serialNo = 2,
|
|
||||||
signatoryNo = "company",
|
|
||||||
actions = new object[]
|
|
||||||
{
|
|
||||||
new
|
|
||||||
{
|
|
||||||
type = "COMPANY",
|
|
||||||
name = "企业人工盖章",
|
|
||||||
serialNo = 0,
|
|
||||||
autoSign = false,
|
|
||||||
sealId = input.SealId,
|
|
||||||
corpSealIds = new[] { sealId },
|
|
||||||
actionNo = "company_seal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return await PostJson<QiyuesuoContractResult>(
|
return await PostJson<QiyuesuoContractResult>(
|
||||||
"/v2/contract/draft",
|
"/v2/contract/draft",
|
||||||
body);
|
QiyuesuoContractRequestBuilder.BuildDraft(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
+3
@@ -34,6 +34,9 @@ public class QiyuesuoCreateDraftInput
|
|||||||
|
|
||||||
/// <summary>契约锁服务器回调地址。</summary>
|
/// <summary>契约锁服务器回调地址。</summary>
|
||||||
public string CallbackUrl { get; set; }
|
public string CallbackUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否需要企业签章节点;仅员工签字的流程不创建该节点。</summary>
|
||||||
|
public bool RequiresCompanySign { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>发起签约前按文件冻结的签字、盖章要求。</summary>
|
||||||
|
internal sealed record OnboardingSigningDocument(
|
||||||
|
string DocumentId,
|
||||||
|
string EmployeeSignKeyword,
|
||||||
|
string CompanySealKeyword,
|
||||||
|
bool? CompanySealEnabled,
|
||||||
|
bool? AcrossPageSealEnabled);
|
||||||
|
|
||||||
|
/// <summary>构造契约锁请求;无数据库和网络访问,员工签字始终覆盖全部文件。</summary>
|
||||||
|
internal static class QiyuesuoContractRequestBuilder
|
||||||
|
{
|
||||||
|
internal static bool CompanySealEnabled(bool? enabled) => enabled ?? true;
|
||||||
|
|
||||||
|
internal static bool AcrossPageSealEnabled(bool? enabled, QiyuesuoSettingOptions setting) =>
|
||||||
|
enabled ?? setting.EnableAcrossPageSeal;
|
||||||
|
|
||||||
|
internal static bool RequiresCompanySign(
|
||||||
|
bool? companySealEnabled,
|
||||||
|
bool? acrossPageSealEnabled,
|
||||||
|
QiyuesuoSettingOptions setting) =>
|
||||||
|
CompanySealEnabled(companySealEnabled) || AcrossPageSealEnabled(acrossPageSealEnabled, setting);
|
||||||
|
|
||||||
|
internal static object BuildDraft(QiyuesuoCreateDraftInput input)
|
||||||
|
{
|
||||||
|
long sealId = 0;
|
||||||
|
if (input.RequiresCompanySign && !long.TryParse(input.SealId, out sealId))
|
||||||
|
throw Oops.Bah("契约锁 SealId 配置格式不正确");
|
||||||
|
|
||||||
|
var signatories = new List<object>
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
tenantType = "PERSONAL",
|
||||||
|
tenantName = input.EmployeeName,
|
||||||
|
receiver = new
|
||||||
|
{
|
||||||
|
name = input.EmployeeName,
|
||||||
|
contact = input.EmployeeMobile,
|
||||||
|
contactType = "MOBILE"
|
||||||
|
},
|
||||||
|
serialNo = 1,
|
||||||
|
signatoryNo = "employee",
|
||||||
|
actions = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
type = "PERSONAL",
|
||||||
|
name = "员工本人签署",
|
||||||
|
serialNo = 0,
|
||||||
|
actionNo = "employee_sign"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (input.RequiresCompanySign)
|
||||||
|
{
|
||||||
|
signatories.Add(new
|
||||||
|
{
|
||||||
|
tenantType = "COMPANY",
|
||||||
|
tenantName = input.CompanyName,
|
||||||
|
receiver = new
|
||||||
|
{
|
||||||
|
name = input.CreatorName,
|
||||||
|
contact = input.CreatorMobile,
|
||||||
|
contactType = "MOBILE"
|
||||||
|
},
|
||||||
|
serialNo = 2,
|
||||||
|
signatoryNo = "company",
|
||||||
|
actions = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
type = "COMPANY",
|
||||||
|
name = "企业人工盖章",
|
||||||
|
serialNo = 0,
|
||||||
|
autoSign = false,
|
||||||
|
sealId = input.SealId,
|
||||||
|
corpSealIds = new[] { sealId },
|
||||||
|
actionNo = "company_seal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
bizId = input.BizId,
|
||||||
|
subject = input.Subject,
|
||||||
|
tenantName = input.CompanyName,
|
||||||
|
description = "OnboardX 入职电子签",
|
||||||
|
send = false,
|
||||||
|
callbackUrl = string.IsNullOrWhiteSpace(input.CallbackUrl) ? null : input.CallbackUrl.Trim(),
|
||||||
|
category = new { id = input.CategoryId },
|
||||||
|
creator = new
|
||||||
|
{
|
||||||
|
name = input.CreatorName,
|
||||||
|
contact = input.CreatorMobile,
|
||||||
|
contactType = "MOBILE"
|
||||||
|
},
|
||||||
|
signatories
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static List<QiyuesuoStamperInput> BuildStampers(
|
||||||
|
IReadOnlyList<OnboardingSigningDocument> documents,
|
||||||
|
string personalSignatoryId,
|
||||||
|
string companyActionId,
|
||||||
|
string sealId,
|
||||||
|
QiyuesuoSettingOptions setting)
|
||||||
|
{
|
||||||
|
if (documents.Count == 0)
|
||||||
|
throw Oops.Bah("当前电子签流程没有可签署的文件");
|
||||||
|
if (documents.Any(item => string.IsNullOrWhiteSpace(item.DocumentId)))
|
||||||
|
throw Oops.Bah("契约锁未返回签署文档 Id");
|
||||||
|
if (string.IsNullOrWhiteSpace(personalSignatoryId))
|
||||||
|
throw Oops.Bah("契约锁未返回员工签署方 Id");
|
||||||
|
if (documents.Any(item => RequiresCompanySign(item.CompanySealEnabled, item.AcrossPageSealEnabled, setting))
|
||||||
|
&& string.IsNullOrWhiteSpace(companyActionId))
|
||||||
|
throw Oops.Bah("契约锁草稿未返回企业盖章动作");
|
||||||
|
|
||||||
|
var stampers = new List<QiyuesuoStamperInput>();
|
||||||
|
foreach (var document in documents)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(document.EmployeeSignKeyword))
|
||||||
|
throw Oops.Bah("请配置员工签名定位关键字");
|
||||||
|
stampers.Add(new QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
SignatoryId = personalSignatoryId,
|
||||||
|
Type = "PERSONAL",
|
||||||
|
DocumentId = document.DocumentId,
|
||||||
|
Keyword = document.EmployeeSignKeyword,
|
||||||
|
KeywordIndex = Math.Max(1, setting.EmployeeSignKeywordIndex)
|
||||||
|
});
|
||||||
|
if (CompanySealEnabled(document.CompanySealEnabled))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(document.CompanySealKeyword))
|
||||||
|
throw Oops.Bah("需要公司盖章的文件未配置企业盖章定位关键字");
|
||||||
|
stampers.Add(new QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
ActionId = companyActionId,
|
||||||
|
Type = "COMPANY",
|
||||||
|
DocumentId = document.DocumentId,
|
||||||
|
SealId = sealId,
|
||||||
|
Keyword = document.CompanySealKeyword,
|
||||||
|
KeywordIndex = Math.Max(1, setting.CompanySealKeywordIndex),
|
||||||
|
OffsetX = setting.CompanySealOffsetX,
|
||||||
|
OffsetY = setting.CompanySealOffsetY
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并骑缝章也只能包含已开启该开关的文件,不能把只需个人签字的材料混入。
|
||||||
|
var acrossDocuments = documents
|
||||||
|
.Where(item => AcrossPageSealEnabled(item.AcrossPageSealEnabled, setting))
|
||||||
|
.Select(item => item.DocumentId)
|
||||||
|
.ToList();
|
||||||
|
if (acrossDocuments.Count == 0)
|
||||||
|
return stampers;
|
||||||
|
var acrossType = setting.AcrossPageSealType?.Trim().ToUpperInvariant();
|
||||||
|
if (acrossType is not ("ALL_DOCUMENT" or "SINGLE_DOCUMENT"))
|
||||||
|
throw Oops.Bah("契约锁骑缝章模式只能配置为 ALL_DOCUMENT 或 SINGLE_DOCUMENT");
|
||||||
|
if (acrossType == "ALL_DOCUMENT")
|
||||||
|
{
|
||||||
|
stampers.Add(new QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
ActionId = companyActionId,
|
||||||
|
Type = "ACROSS_PAGE",
|
||||||
|
DocumentId = acrossDocuments[0],
|
||||||
|
DocumentIds = string.Join(",", acrossDocuments),
|
||||||
|
SealId = sealId,
|
||||||
|
AcrossType = acrossType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var documentId in acrossDocuments)
|
||||||
|
{
|
||||||
|
stampers.Add(new QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
ActionId = companyActionId,
|
||||||
|
Type = "ACROSS_PAGE",
|
||||||
|
DocumentId = documentId,
|
||||||
|
SealId = sealId,
|
||||||
|
AcrossType = acrossType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stampers;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string ResolvePendingStatus(QiyuesuoContractResult contract)
|
||||||
|
{
|
||||||
|
var employeeSigned = contract.Signatories.Any(item =>
|
||||||
|
string.Equals(item.TenantType, "PERSONAL", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(item.Status, "SIGNED", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var hasCompanyAction = contract.Signatories.Any(item =>
|
||||||
|
string.Equals(item.TenantType, "COMPANY", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& item.Actions.Any(action => string.Equals(action.Type, "COMPANY", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
// 纯员工签字流程等待平台 COMPLETE 回执,不制造不存在的企业盖章阶段。
|
||||||
|
return employeeSigned && hasCompanyAction
|
||||||
|
? ElectronicSignStatus.PendingCompanySeal
|
||||||
|
: ElectronicSignStatus.PendingEmployeeSign;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -171,6 +171,7 @@ public class OnboardingTaskService : IOnboardingTaskService
|
|||||||
var contracts = contractConfigs.Select(config =>
|
var contracts = contractConfigs.Select(config =>
|
||||||
{
|
{
|
||||||
var template = contractTemplates.First(item => item.Id == config.ContractTemplateId);
|
var template = contractTemplates.First(item => item.Id == config.ContractTemplateId);
|
||||||
|
var version = taskVersions.First(item => item.Id == config.ContractTemplateVersionId);
|
||||||
return new ObTaskContract
|
return new ObTaskContract
|
||||||
{
|
{
|
||||||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
@@ -180,6 +181,8 @@ public class OnboardingTaskService : IOnboardingTaskService
|
|||||||
ContractName = template.TemplateName,
|
ContractName = template.TemplateName,
|
||||||
ContractType = template.ContractType,
|
ContractType = template.ContractType,
|
||||||
Required = config.Required,
|
Required = config.Required,
|
||||||
|
CompanySealEnabled = version.CompanySealEnabled,
|
||||||
|
AcrossPageSealEnabled = version.AcrossPageSealEnabled,
|
||||||
PrintCopies = config.PrintCopies
|
PrintCopies = config.PrintCopies
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
@@ -447,6 +450,8 @@ public class OnboardingTaskService : IOnboardingTaskService
|
|||||||
ContractName = source.ContractName,
|
ContractName = source.ContractName,
|
||||||
ContractType = source.ContractType,
|
ContractType = source.ContractType,
|
||||||
Required = source.Required,
|
Required = source.Required,
|
||||||
|
CompanySealEnabled = source.CompanySealEnabled,
|
||||||
|
AcrossPageSealEnabled = source.AcrossPageSealEnabled,
|
||||||
PrintCopies = source.PrintCopies
|
PrintCopies = source.PrintCopies
|
||||||
}).ToList();
|
}).ToList();
|
||||||
await _db.Insertable(copiedContracts).ExecuteCommandAsync();
|
await _db.Insertable(copiedContracts).ExecuteCommandAsync();
|
||||||
|
|||||||
+10
@@ -135,6 +135,16 @@ public class OnboardingContractTemplateController
|
|||||||
await _service.SetVersionStatus(input);
|
await _service.SetVersionStatus(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改合同版本的公司盖章及骑缝章开关。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("version/set-seals")]
|
||||||
|
[DisplayName("设置合同版本盖章配置")]
|
||||||
|
public async Task SetVersionSeals([FromBody] OnboardingContractVersionSealInput input)
|
||||||
|
{
|
||||||
|
await _service.SetVersionSeals(input);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 删除未被入职模板或任务使用的合同模板版本。
|
/// 删除未被入职模板或任务使用的合同模板版本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
-- 签署材料版本的公司盖章/骑缝章开关及任务、电子签快照。MySQL 8.0+
|
||||||
|
-- 上线顺序:先在目标数据库执行本文件,再部署后端及后台前端。
|
||||||
|
-- 可重复执行;只新增可空列,不回填或覆盖历史业务数据。
|
||||||
|
-- NULL 表示历史规则:公司盖章需要,骑缝章沿用 QiyuesuoSetting.EnableAcrossPageSeal。
|
||||||
|
-- 新上传版本显式保存 true/false;任务及电子签文档分别保存快照。
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS ob_upgrade_contract_version_seal_switches;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE ob_upgrade_contract_version_seal_switches()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'ob_contract_template_version'
|
||||||
|
AND COLUMN_NAME = 'CompanySealEnabled'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `ob_contract_template_version`
|
||||||
|
ADD COLUMN `CompanySealEnabled` TINYINT(1) NULL DEFAULT NULL
|
||||||
|
COMMENT '公司盖章开关;NULL沿用历史需要盖章';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'ob_contract_template_version'
|
||||||
|
AND COLUMN_NAME = 'AcrossPageSealEnabled'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `ob_contract_template_version`
|
||||||
|
ADD COLUMN `AcrossPageSealEnabled` TINYINT(1) NULL DEFAULT NULL
|
||||||
|
COMMENT '骑缝章开关;NULL沿用历史系统配置';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'ob_task_contract'
|
||||||
|
AND COLUMN_NAME = 'CompanySealEnabled'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `ob_task_contract`
|
||||||
|
ADD COLUMN `CompanySealEnabled` TINYINT(1) NULL DEFAULT NULL
|
||||||
|
COMMENT '公司盖章开关;NULL沿用历史需要盖章';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'ob_task_contract'
|
||||||
|
AND COLUMN_NAME = 'AcrossPageSealEnabled'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `ob_task_contract`
|
||||||
|
ADD COLUMN `AcrossPageSealEnabled` TINYINT(1) NULL DEFAULT NULL
|
||||||
|
COMMENT '骑缝章开关;NULL沿用历史系统配置';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'ob_electronic_sign_document'
|
||||||
|
AND COLUMN_NAME = 'CompanySealEnabled'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `ob_electronic_sign_document`
|
||||||
|
ADD COLUMN `CompanySealEnabled` TINYINT(1) NULL DEFAULT NULL
|
||||||
|
COMMENT '公司盖章开关;NULL沿用历史需要盖章';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'ob_electronic_sign_document'
|
||||||
|
AND COLUMN_NAME = 'AcrossPageSealEnabled'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `ob_electronic_sign_document`
|
||||||
|
ADD COLUMN `AcrossPageSealEnabled` TINYINT(1) NULL DEFAULT NULL
|
||||||
|
COMMENT '骑缝章开关;NULL沿用历史系统配置';
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
CALL ob_upgrade_contract_version_seal_switches();
|
||||||
|
DROP PROCEDURE IF EXISTS ob_upgrade_contract_version_seal_switches;
|
||||||
@@ -68,6 +68,39 @@ ContractTemplateVersionValidity.ValidateRange(validityNow, validityNow.AddSecond
|
|||||||
Reject(() => ContractTemplateVersionValidity.ValidateRange(validityNow, validityNow), "equal start/end rejected");
|
Reject(() => ContractTemplateVersionValidity.ValidateRange(validityNow, validityNow), "equal start/end rejected");
|
||||||
Reject(() => ContractTemplateVersionValidity.ValidateRange(validityNow, validityNow.AddSeconds(-1)), "end before start rejected");
|
Reject(() => ContractTemplateVersionValidity.ValidateRange(validityNow, validityNow.AddSeconds(-1)), "end before start rejected");
|
||||||
|
|
||||||
|
// Uses the real partial-update builder and ORM SQL generation, never executes the SQL.
|
||||||
|
Reject(() => ContractTemplateVersionSealConfiguration.BuildUpdate(db, new() { Id = 42 }),
|
||||||
|
"seal update rejects an empty patch instead of resetting both flags");
|
||||||
|
Reject(() => ContractTemplateVersionSealConfiguration.BuildUpdate(db, new() { Id = 0, CompanySealEnabled = false }),
|
||||||
|
"seal update requires a specific version");
|
||||||
|
foreach (var enabled in new[] { false, true })
|
||||||
|
{
|
||||||
|
foreach (var companySeal in new[] { true, false })
|
||||||
|
{
|
||||||
|
var patch = new OnboardingContractVersionSealInput { Id = 42 };
|
||||||
|
var changedField = companySeal ? "CompanySealEnabled" : "AcrossPageSealEnabled";
|
||||||
|
var untouchedField = companySeal ? "AcrossPageSealEnabled" : "CompanySealEnabled";
|
||||||
|
if (companySeal) patch.CompanySealEnabled = enabled;
|
||||||
|
else patch.AcrossPageSealEnabled = enabled;
|
||||||
|
var generated = ContractTemplateVersionSealConfiguration.BuildUpdate(db, patch).ToSql();
|
||||||
|
var setClause = System.Text.RegularExpressions.Regex.Match(generated.Key, @"\bSET\b([\s\S]*?)\bWHERE\b",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase).Groups[1].Value;
|
||||||
|
Check(setClause.Contains(changedField) && !setClause.Contains(untouchedField)
|
||||||
|
&& !setClause.Contains("VersionStatus") && !System.Text.RegularExpressions.Regex.IsMatch(setClause, @"\bId\b"),
|
||||||
|
$"{changedField}={enabled} changes only its own column, never the other flag, status or primary key");
|
||||||
|
Check(generated.Value.Any(item => item.Value is bool flag && flag == enabled),
|
||||||
|
$"{changedField}={enabled} preserves the explicit boolean value in parameterized SQL");
|
||||||
|
Check(System.Text.RegularExpressions.Regex.IsMatch(generated.Key, @"\bWHERE\b[\s\S]*\bId\b",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase)
|
||||||
|
&& generated.Value.Any(item => item.Value is long id && id == 42),
|
||||||
|
$"{changedField} update is scoped to the requested version ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var bothSealUpdates = ContractTemplateVersionSealConfiguration.BuildUpdate(db,
|
||||||
|
new() { Id = 42, CompanySealEnabled = false, AcrossPageSealEnabled = true }).ToSql().Key;
|
||||||
|
Check(bothSealUpdates.Contains("CompanySealEnabled") && bothSealUpdates.Contains("AcrossPageSealEnabled"),
|
||||||
|
"both seal flags can be explicitly updated without overwriting unrelated version data");
|
||||||
|
|
||||||
// Exercises the real async gate with a fake external lease. Does NOT claim MySQL GET_LOCK integration.
|
// Exercises the real async gate with a fake external lease. Does NOT claim MySQL GET_LOCK integration.
|
||||||
var leases = new FakeLeases();
|
var leases = new FakeLeases();
|
||||||
Task<int> Gate(string key, Func<Task<int>> action) => OnboardingOperationGate.Run(key, () => leases.Acquire(key), action);
|
Task<int> Gate(string key, Func<Task<int>> action) => OnboardingOperationGate.Run(key, () => leases.Acquire(key), action);
|
||||||
@@ -91,6 +124,105 @@ Check(leases.AcquisitionCount - before == 2, "reentrant gate acquires each resou
|
|||||||
try { await Gate("failure", () => throw new ApplicationException("synthetic fault")); } catch (ApplicationException) {}
|
try { await Gate("failure", () => throw new ApplicationException("synthetic fault")); } catch (ApplicationException) {}
|
||||||
Check(await Gate("failure", () => Task.FromResult(4)) == 4, "lock released on exception");
|
Check(await Gate("failure", () => Task.FromResult(4)) == 4, "lock released on exception");
|
||||||
Check(leases.ActiveCount == 0, "no leaked leases or caller async context");
|
Check(leases.ActiveCount == 0, "no leaked leases or caller async context");
|
||||||
|
// Uses the real request builder and provider models. No provider or database requests.
|
||||||
|
var sealSettings = new QiyuesuoSettingOptions { EnableAcrossPageSeal = true, AcrossPageSealType = "SINGLE_DOCUMENT" };
|
||||||
|
var signDocuments = new[]
|
||||||
|
{
|
||||||
|
new OnboardingSigningDocument("labor", "员工签字", "公司盖章", true, true),
|
||||||
|
new OnboardingSigningDocument("confidentiality", "员工签字", "公司盖章", true, true),
|
||||||
|
new OnboardingSigningDocument("notice", "员工签字", null, false, false),
|
||||||
|
new OnboardingSigningDocument("no_release_certificate", "员工签字", null, false, false)
|
||||||
|
};
|
||||||
|
var positions = QiyuesuoContractRequestBuilder.BuildStampers(signDocuments, "person", "company", "123", sealSettings);
|
||||||
|
Check(positions.Count(item => item.Type == "PERSONAL") == 4, "all four materials require employee signatures");
|
||||||
|
Check(positions.Where(item => item.Type == "PERSONAL").Select(item => item.DocumentId).SequenceEqual(signDocuments.Select(item => item.DocumentId)),
|
||||||
|
"employee signatures retain each distinct material and its order");
|
||||||
|
Check(positions.Count(item => item.Type == "COMPANY") == 2, "only labor contract and confidentiality agreement receive company seals");
|
||||||
|
Check(positions.Count(item => item.Type == "ACROSS_PAGE") == 2, "selected materials receive separate across-page seals");
|
||||||
|
Check(positions.Where(item => item.Type != "PERSONAL").All(item => item.DocumentId is "labor" or "confidentiality"),
|
||||||
|
"employee-only materials never receive any company signing position");
|
||||||
|
Check(positions.Where(item => item.Type == "COMPANY").All(item => item.ActionId == "company" && item.SealId == "123"),
|
||||||
|
"company positions use the intended action and company seal");
|
||||||
|
|
||||||
|
sealSettings.AcrossPageSealType = "ALL_DOCUMENT";
|
||||||
|
var reorderedDocuments = new[] { signDocuments[2], signDocuments[0], signDocuments[3], signDocuments[1] };
|
||||||
|
positions = QiyuesuoContractRequestBuilder.BuildStampers(reorderedDocuments, "person", "company", "123", sealSettings);
|
||||||
|
var mergedAcross = positions.Single(item => item.Type == "ACROSS_PAGE");
|
||||||
|
Check(mergedAcross.DocumentId == "labor", "merged across-page anchor is the first selected material, not the first uploaded file");
|
||||||
|
Check(mergedAcross.DocumentIds == "labor,confidentiality", "merged across-page seal excludes both employee-only materials");
|
||||||
|
|
||||||
|
sealSettings.EnableAcrossPageSeal = false;
|
||||||
|
var legacyDocuments = new[] { new OnboardingSigningDocument("legacy", "员工签字", "公司盖章", null, null) };
|
||||||
|
positions = QiyuesuoContractRequestBuilder.BuildStampers(legacyDocuments, "person", "company", "123", sealSettings);
|
||||||
|
Check(positions.Count(item => item.Type == "COMPANY") == 1 && positions.All(item => item.Type != "ACROSS_PAGE"),
|
||||||
|
"old versions retain company seals and the disabled global across-page setting");
|
||||||
|
sealSettings.EnableAcrossPageSeal = true;
|
||||||
|
positions = QiyuesuoContractRequestBuilder.BuildStampers(legacyDocuments, "person", "company", "123", sealSettings);
|
||||||
|
Check(positions.Count(item => item.Type == "ACROSS_PAGE") == 1, "old versions retain the enabled global across-page setting");
|
||||||
|
sealSettings.EnableAcrossPageSeal = false;
|
||||||
|
positions = QiyuesuoContractRequestBuilder.BuildStampers(signDocuments, "person", "company", "123", sealSettings);
|
||||||
|
Check(positions.Count(item => item.Type == "ACROSS_PAGE") == 1, "new explicit across-page switch overrides legacy global fallback");
|
||||||
|
|
||||||
|
var acrossOnly = new[] { new OnboardingSigningDocument("across_only", "员工签字", null, false, true) };
|
||||||
|
positions = QiyuesuoContractRequestBuilder.BuildStampers(acrossOnly, "person", "company", "123", sealSettings);
|
||||||
|
Check(positions.Count(item => item.Type == "COMPANY") == 0 && positions.Count(item => item.Type == "ACROSS_PAGE") == 1,
|
||||||
|
"company seal and across-page seal switches operate independently");
|
||||||
|
Check(QiyuesuoContractRequestBuilder.RequiresCompanySign(false, true, sealSettings),
|
||||||
|
"across-page-only materials still require a company signing action");
|
||||||
|
var companyOnly = new[] { new OnboardingSigningDocument("company_only", "员工签字", "公司盖章", true, false) };
|
||||||
|
positions = QiyuesuoContractRequestBuilder.BuildStampers(companyOnly, "person", "company", "123", sealSettings);
|
||||||
|
Check(positions.Count(item => item.Type == "COMPANY") == 1 && positions.All(item => item.Type != "ACROSS_PAGE"),
|
||||||
|
"company seal without across-page seal is supported");
|
||||||
|
var personalOnly = new[] { new OnboardingSigningDocument("personal_only", "员工签字", null, false, false) };
|
||||||
|
positions = QiyuesuoContractRequestBuilder.BuildStampers(personalOnly, "person", null, null, sealSettings);
|
||||||
|
Check(positions.Count == 1 && positions[0].Type == "PERSONAL", "all switches off produces only personal signature positions");
|
||||||
|
Check(!QiyuesuoContractRequestBuilder.RequiresCompanySign(false, false, sealSettings),
|
||||||
|
"employee-only signing needs no company action");
|
||||||
|
Reject(() => QiyuesuoContractRequestBuilder.BuildStampers(signDocuments, "person", null, "123", sealSettings),
|
||||||
|
"company action cannot be silently omitted for materials requiring seals");
|
||||||
|
Reject(() => QiyuesuoContractRequestBuilder.BuildStampers(
|
||||||
|
new[] { new OnboardingSigningDocument(null, "员工签字", "公司盖章", true, true) }, "person", "company", "123", sealSettings),
|
||||||
|
"missing document ID is rejected instead of broadening seal scope");
|
||||||
|
sealSettings.AcrossPageSealType = "INVALID";
|
||||||
|
Reject(() => QiyuesuoContractRequestBuilder.BuildStampers(acrossOnly, "person", "company", "123", sealSettings),
|
||||||
|
"invalid across-page mode is checked even when the global fallback is off");
|
||||||
|
|
||||||
|
var draftInput = new QiyuesuoCreateDraftInput
|
||||||
|
{
|
||||||
|
RequiresCompanySign = false, CompanyName = "测试企业", EmployeeName = "测试员工",
|
||||||
|
EmployeeMobile = "13800138000", CreatorName = "测试经办人", CreatorMobile = "13900139000",
|
||||||
|
CategoryId = "456", CallbackUrl = " https://example.invalid/callback "
|
||||||
|
};
|
||||||
|
var employeeOnlyDraft = System.Text.Json.JsonSerializer.SerializeToElement(QiyuesuoContractRequestBuilder.BuildDraft(draftInput));
|
||||||
|
Check(employeeOnlyDraft.GetProperty("signatories").GetArrayLength() == 1
|
||||||
|
&& employeeOnlyDraft.GetProperty("signatories")[0].GetProperty("tenantType").GetString() == "PERSONAL",
|
||||||
|
"employee-only draft never creates a company signature node");
|
||||||
|
Check(employeeOnlyDraft.GetProperty("tenantName").GetString() == "测试企业",
|
||||||
|
"employee-only workflow retains its sponsoring company");
|
||||||
|
Check(employeeOnlyDraft.GetProperty("callbackUrl").GetString() == "https://example.invalid/callback",
|
||||||
|
"draft builder preserves normalized server callback");
|
||||||
|
draftInput.RequiresCompanySign = true;
|
||||||
|
Reject(() => QiyuesuoContractRequestBuilder.BuildDraft(draftInput), "company signing draft requires a valid seal");
|
||||||
|
draftInput.SealId = "123";
|
||||||
|
var companyDraft = System.Text.Json.JsonSerializer.SerializeToElement(QiyuesuoContractRequestBuilder.BuildDraft(draftInput));
|
||||||
|
Check(companyDraft.GetProperty("signatories").GetArrayLength() == 2
|
||||||
|
&& companyDraft.GetProperty("signatories")[1].GetProperty("actions")[0].GetProperty("type").GetString() == "COMPANY",
|
||||||
|
"mixed materials keep one company signing action after employee signing");
|
||||||
|
Check(!companyDraft.GetProperty("signatories")[1].GetProperty("actions")[0].GetProperty("autoSign").GetBoolean(),
|
||||||
|
"company seal remains manually triggered");
|
||||||
|
|
||||||
|
var employeeOnlyProvider = new QiyuesuoContractResult { Signatories = new()
|
||||||
|
{
|
||||||
|
new QiyuesuoSignatoryResult { TenantType = "PERSONAL", Status = "SIGNED" }
|
||||||
|
}};
|
||||||
|
Check(QiyuesuoContractRequestBuilder.ResolvePendingStatus(employeeOnlyProvider) != ElectronicSignStatus.PendingCompanySeal,
|
||||||
|
"employee-only workflow cannot become stuck in a nonexistent company-seal stage");
|
||||||
|
employeeOnlyProvider.Signatories.Add(new QiyuesuoSignatoryResult { TenantType = "COMPANY", Actions = new()
|
||||||
|
{
|
||||||
|
new QiyuesuoActionResult { Type = "COMPANY", Status = "START" }
|
||||||
|
}});
|
||||||
|
Check(QiyuesuoContractRequestBuilder.ResolvePendingStatus(employeeOnlyProvider) == ElectronicSignStatus.PendingCompanySeal,
|
||||||
|
"mixed workflow waits for company seal after employee signing");
|
||||||
Console.WriteLine("ALL " + passed + " CHECKS PASSED (offline; no business database or provider contacted)");
|
Console.WriteLine("ALL " + passed + " CHECKS PASSED (offline; no business database or provider contacted)");
|
||||||
|
|
||||||
[SugarTable("test_row")]
|
[SugarTable("test_row")]
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ dotnet run --project tests/SecurityRegression/SecurityRegression.csproj -p:NuGet
|
|||||||
- 签约终态、防回退、归档状态保持。
|
- 签约终态、防回退、归档状态保持。
|
||||||
- 64 个竞争调用与同任务互斥;不同任务互不阻塞。
|
- 64 个竞争调用与同任务互斥;不同任务互不阻塞。
|
||||||
- 同调用链嵌套任务锁可重入;正常/异常结束释放锁;调用方上下文不泄漏。
|
- 同调用链嵌套任务锁可重入;正常/异常结束释放锁;调用方上下文不泄漏。
|
||||||
|
- 按版本选择公司盖章及骑缝章:四份材料全部员工签字,仅指定两份盖章。
|
||||||
|
- 单份/合并骑缝章只覆盖已选文件;两个开关独立;历史空值兼容全局设置。
|
||||||
|
- 纯员工签字的草稿无企业盖章节点;混合材料仍由后台人工触发企业盖章。
|
||||||
|
- 版本盖章开关局部更新:开启/关闭均保留布尔值,只更新传入的字段,版本 ID 只用作条件,不更新主键、另一个开关或版本状态;空请求被拒绝。
|
||||||
|
- 局部更新测试直接链接生产 SQL 构造代码和请求 DTO;版本实体仅使用带主键映射的测试替身,不代表真实数据库写入或权限集成测试。
|
||||||
|
- 请求构造器直接链接生产代码,只校验构造出的参数,不向契约锁发起真实签约。
|
||||||
|
|
||||||
## 仍需测试环境验证
|
## 仍需测试环境验证
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,11 @@
|
|||||||
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/Common/OnboardingOperationGate.cs" Link="OnboardingOperationGate.cs"/>
|
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/Common/OnboardingOperationGate.cs" Link="OnboardingOperationGate.cs"/>
|
||||||
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/ElectronicSign/OnboardingSignStateGuard.cs" Link="OnboardingSignStateGuard.cs"/>
|
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/ElectronicSign/OnboardingSignStateGuard.cs" Link="OnboardingSignStateGuard.cs"/>
|
||||||
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/Common/ContractTemplateVersionValidity.cs" Link="ContractTemplateVersionValidity.cs"/>
|
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/Common/ContractTemplateVersionValidity.cs" Link="ContractTemplateVersionValidity.cs"/>
|
||||||
|
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/Common/ContractTemplateVersionSealConfiguration.cs" Link="ContractTemplateVersionSealConfiguration.cs"/>
|
||||||
|
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractVersionInput.cs" Link="OnboardingContractVersionInput.cs"/>
|
||||||
<Compile Include="../../SimpleAdmin.Application/Entity/Onboarding/OnboardingStatus.cs" Link="OnboardingStatus.cs"/>
|
<Compile Include="../../SimpleAdmin.Application/Entity/Onboarding/OnboardingStatus.cs" Link="OnboardingStatus.cs"/>
|
||||||
|
<Compile Include="../../SimpleAdmin.Application/Options/QiyuesuoSettingOptions.cs" Link="QiyuesuoSettingOptions.cs"/>
|
||||||
|
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/ElectronicSign/Qiyuesuo/QiyuesuoClientModels.cs" Link="QiyuesuoClientModels.cs"/>
|
||||||
|
<Compile Include="../../SimpleAdmin.Application/Services/Onboarding/ElectronicSign/Qiyuesuo/QiyuesuoContractRequestBuilder.cs" Link="QiyuesuoContractRequestBuilder.cs"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
global using System.ComponentModel.DataAnnotations;
|
global using System.ComponentModel.DataAnnotations;
|
||||||
|
global using SimpleAdmin.Core;
|
||||||
namespace SimpleAdmin.Core {
|
namespace SimpleAdmin.Core {
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public sealed class DataValidationAttribute : Attribute { public DataValidationAttribute(ValidationTypes type) {} }
|
public sealed class DataValidationAttribute : Attribute { public DataValidationAttribute(ValidationTypes type) {} }
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class IdNotNullAttribute : Attribute { public string ErrorMessage { get; set; } }
|
||||||
public enum ValidationTypes { Numeric }
|
public enum ValidationTypes { Numeric }
|
||||||
public static class Oops { public static Exception Bah(string text) => new InvalidOperationException(text); }
|
public static class Oops { public static Exception Bah(string text) => new InvalidOperationException(text); }
|
||||||
}
|
}
|
||||||
namespace SimpleAdmin.Application {
|
namespace SimpleAdmin.Application {
|
||||||
public static class Oops { public static Exception Bah(string text) => new InvalidOperationException(text); }
|
public static class Oops { public static Exception Bah(string text) => new InvalidOperationException(text); }
|
||||||
public sealed class ObContractTemplateVersion { public string VersionStatus { get; set; } public DateTime? EffectiveDate { get; set; } public DateTime? ExpiryDate { get; set; } }
|
[SqlSugar.SugarTable("ob_contract_template_version")]
|
||||||
|
public sealed class ObContractTemplateVersion {
|
||||||
|
[SqlSugar.SugarColumn(IsPrimaryKey = true)] public long Id { get; set; }
|
||||||
|
public string VersionStatus { get; set; }
|
||||||
|
public DateTime? EffectiveDate { get; set; }
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user