电子签基础功能
This commit is contained in:
@@ -6,8 +6,28 @@
|
|||||||
"Title": "业务模块" //配置分组标题,string 类型
|
"Title": "业务模块" //配置分组标题,string 类型
|
||||||
},
|
},
|
||||||
//系统层设置
|
//系统层设置
|
||||||
"ApplicationSettings": {
|
"ApplicationSettings": {
|
||||||
"InitTable": false, //是否初始化表结构
|
"InitTable": false, //是否初始化表结构
|
||||||
"InitSeedData": false //是否初始化种子数据
|
"InitSeedData": false //是否初始化种子数据
|
||||||
|
},
|
||||||
|
// 契约锁测试环境配置。AppToken、AppSecret 由部署人员填写,禁止提交真实密钥。
|
||||||
|
"QiyuesuoSetting": {
|
||||||
|
"Enabled": true,
|
||||||
|
"ServerUrl": "https://openapi.qiyuesuo.cn",
|
||||||
|
"AppToken": "rMKU99gkts",
|
||||||
|
"AppSecret": "sjhcNcV1YTIhA124yrjLMdKRoBlP8O",
|
||||||
|
"SignatureVersion": "",
|
||||||
|
"CategoryId": "3533041076673835142",
|
||||||
|
"SealId": "3533379903657587306",
|
||||||
|
"CreatorName": "袁林",
|
||||||
|
"CreatorMobile": "15123135384",
|
||||||
|
"CallbackUrl": "https://undiffusively-hemielytral-myrtle.ngrok-free.dev",
|
||||||
|
"SignPageCallbackUrl": "",
|
||||||
|
"SignUrlExpireMinutes": 10,
|
||||||
|
"EmployeeSignKeyword": "乙方(劳动者)签字:",
|
||||||
|
"EmployeeSignKeywordIndex": 1,
|
||||||
|
"CompanySealKeyword": "甲方(盖章):",
|
||||||
|
"CompanySealKeywordIndex": 1,
|
||||||
|
"TimeoutSeconds": 90
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,26 @@
|
|||||||
//系统层设置
|
//系统层设置
|
||||||
"ApplicationSettings": {
|
"ApplicationSettings": {
|
||||||
"InitTable": false, //是否初始化表结构
|
"InitTable": false, //是否初始化表结构
|
||||||
"InitSeedData": false, //是否初始化种子数据0
|
"InitSeedData": false //是否初始化种子数据
|
||||||
}
|
},
|
||||||
}
|
// 契约锁正式环境配置。上线前填写正式 AppToken/AppSecret,并把 ServerUrl 切换为 .com。
|
||||||
|
"QiyuesuoSetting": {
|
||||||
|
"Enabled": true,
|
||||||
|
"ServerUrl": "https://openapi.qiyuesuo.com",
|
||||||
|
"AppToken": "rMKU99gkts",
|
||||||
|
"AppSecret": "sjhcNcV1YTIhA124yrjLMdKRoBlP8O",
|
||||||
|
"SignatureVersion": "",
|
||||||
|
"CategoryId": "3533041076673835142",
|
||||||
|
"SealId": "3533042752893948009",
|
||||||
|
"CreatorName": "袁林",
|
||||||
|
"CreatorMobile": "15123135384",
|
||||||
|
"CallbackUrl": "https://onboardx-api.cnzhyx.cn/api/open/onboarding/qiyuesuo/callback",
|
||||||
|
"SignPageCallbackUrl": "",
|
||||||
|
"SignUrlExpireMinutes": 10,
|
||||||
|
"EmployeeSignKeyword": "乙方(劳动者)签字:",
|
||||||
|
"EmployeeSignKeywordIndex": 1,
|
||||||
|
"CompanySealKeyword": "甲方(盖章):",
|
||||||
|
"CompanySealKeywordIndex": 1,
|
||||||
|
"TimeoutSeconds": 90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 电子签流程中的合同文件映射,一条任务合同对应一个契约锁文档。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_electronic_sign_document", TableDescription = "入职电子签文档")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObElectronicSignDocument : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属电子签流程 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long SignProcessId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对应的入职任务合同 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long TaskContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁文档 Id。
|
||||||
|
/// </summary>
|
||||||
|
public string ProviderDocumentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发起签署时冻结的文档名称。
|
||||||
|
/// </summary>
|
||||||
|
public string DocumentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文档在同一签署流程中的顺序。
|
||||||
|
/// </summary>
|
||||||
|
public int DocumentSort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上传契约锁的原始 DOCX 系统文件 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long OriginalDocxFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签署完成后归档的 PDF 系统文件 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? SignedPdfFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工签名关键字快照。
|
||||||
|
/// </summary>
|
||||||
|
public string EmployeeSignKeyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 企业盖章关键字快照。
|
||||||
|
/// </summary>
|
||||||
|
public string CompanySealKeyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁员工签署动作 Id。
|
||||||
|
/// </summary>
|
||||||
|
public string EmployeeActionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁企业盖章动作 Id。
|
||||||
|
/// </summary>
|
||||||
|
public string CompanyActionId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁回调与主动同步事件,用于幂等处理和完整审计。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_electronic_sign_event", TableDescription = "入职电子签事件")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObElectronicSignEvent : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属电子签流程 Id;无法识别流程的异常回调可为空。
|
||||||
|
/// </summary>
|
||||||
|
public long? SignProcessId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 回调内容 SHA-256,用于识别完全重复的通知。
|
||||||
|
/// </summary>
|
||||||
|
public string PayloadHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 事件来源,例如 callback 或 manual_sync。
|
||||||
|
/// </summary>
|
||||||
|
public string EventSource { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁回调事件类型。
|
||||||
|
/// </summary>
|
||||||
|
public string EventType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 回调携带或主动查询得到的契约锁状态。
|
||||||
|
/// </summary>
|
||||||
|
public string ProviderStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁原始请求内容,保留用于审计排查。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string PayloadJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次事件处理结果。
|
||||||
|
/// </summary>
|
||||||
|
public string ProcessResult { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 接收到事件的时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ReceivedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 事件处理完成时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ProcessedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务的一次电子签流程,一次流程可以包含多份合同文件。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_electronic_sign_process", TableDescription = "入职电子签流程")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObElectronicSignProcess : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过后生成的正式员工 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long EmployeeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 电子签服务商编码。
|
||||||
|
/// </summary>
|
||||||
|
public string Provider { get; set; } = ElectronicSignProvider.Qiyuesuo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本系统生成的幂等业务编号。
|
||||||
|
/// </summary>
|
||||||
|
public string BizId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否为该入职任务当前使用的电子签流程。
|
||||||
|
/// 历史流程保存为空,当前流程保存为 true,并通过数据库唯一索引防止重复发起。
|
||||||
|
/// </summary>
|
||||||
|
public bool? IsCurrent { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁合同流程 Id。
|
||||||
|
/// </summary>
|
||||||
|
public string ProviderContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁业务分类 Id 快照。
|
||||||
|
/// </summary>
|
||||||
|
public string CategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁企业印章 Id 快照。
|
||||||
|
/// </summary>
|
||||||
|
public string SealId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 电子签流程标题。
|
||||||
|
/// </summary>
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工姓名快照。
|
||||||
|
/// </summary>
|
||||||
|
public string EmployeeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工手机号码快照。
|
||||||
|
/// </summary>
|
||||||
|
public string EmployeeMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工身份证号码快照,仅用于锁定本次电子签署人的实名信息。
|
||||||
|
/// </summary>
|
||||||
|
public string EmployeeIdCardNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁合同创建人姓名快照。
|
||||||
|
/// </summary>
|
||||||
|
public string CreatorName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁合同创建人手机号快照。
|
||||||
|
/// </summary>
|
||||||
|
public string CreatorMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本系统电子签状态。
|
||||||
|
/// </summary>
|
||||||
|
public string SignStatus { get; set; } = ElectronicSignStatus.Creating;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁返回的原始合同状态。
|
||||||
|
/// </summary>
|
||||||
|
public string ProviderStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工签署页面地址。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string SignUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工签署页面地址到期时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? SignUrlExpireAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 管理员点击发起电子签的时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime StartedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同流程正式发送的时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? SentAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所有签署方完成签署的时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签署文件与签署日志保存到 OSS 的时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ArchivedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁完整归档 ZIP 在系统文件表中的 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? ArchiveFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最后一次主动查询契约锁状态的时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastSyncedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最后一次收到契约锁回调的时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastCallbackAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最近一次失败原因,供管理员排查和重试。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string LastError { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建或同步失败后的重试次数。
|
||||||
|
/// </summary>
|
||||||
|
public int RetryCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 管理员重新发起本任务电子签时填写的原因。
|
||||||
|
/// </summary>
|
||||||
|
public string ReissueReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前流程被新流程替代的时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ReissuedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替代当前流程的新电子签流程 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? ReissuedByProcessId { get; set; }
|
||||||
|
}
|
||||||
@@ -128,12 +128,89 @@ public static class TaskContractStatus
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string OfflinePendingSign = "offline_pending_sign";
|
public const string OfflinePendingSign = "offline_pending_sign";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已发起第三方电子签,等待员工签署或企业盖章。
|
||||||
|
/// </summary>
|
||||||
|
public const string ElectronicSigning = "electronic_signing";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 第三方电子签已完成,签署文件正在或已经归档。
|
||||||
|
/// </summary>
|
||||||
|
public const string ElectronicSigned = "electronic_signed";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已归档,任务流程结束且历史数据只读。
|
/// 已归档,任务流程结束且历史数据只读。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Archived = "archived";
|
public const string Archived = "archived";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 电子签服务商。
|
||||||
|
/// </summary>
|
||||||
|
public static class ElectronicSignProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁开放平台。
|
||||||
|
/// </summary>
|
||||||
|
public const string Qiyuesuo = "qiyuesuo";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职电子签流程状态。
|
||||||
|
/// </summary>
|
||||||
|
public static class ElectronicSignStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 正在调用第三方创建流程。
|
||||||
|
/// </summary>
|
||||||
|
public const string Creating = "creating";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已创建草稿,尚未成功发送。
|
||||||
|
/// </summary>
|
||||||
|
public const string Draft = "draft";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已发送,等待员工签署。
|
||||||
|
/// </summary>
|
||||||
|
public const string PendingEmployeeSign = "pending_employee_sign";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工已签,等待企业自动盖章或平台完成流程。
|
||||||
|
/// </summary>
|
||||||
|
public const string PendingCompanySeal = "pending_company_seal";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所有签署已完成,等待文件归档。
|
||||||
|
/// </summary>
|
||||||
|
public const string Completed = "completed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签署文件和签署日志已经归档。
|
||||||
|
/// </summary>
|
||||||
|
public const string Archived = "archived";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 流程被拒签。
|
||||||
|
/// </summary>
|
||||||
|
public const string Rejected = "rejected";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 流程已作废。
|
||||||
|
/// </summary>
|
||||||
|
public const string Invalid = "invalid";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 流程已过期。
|
||||||
|
/// </summary>
|
||||||
|
public const string Expired = "expired";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建、发送、同步或归档失败,可由管理员重试。
|
||||||
|
/// </summary>
|
||||||
|
public const string Failed = "failed";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 合同变量类型。
|
/// 合同变量类型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁开放平台连接及入职电子签业务配置。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoSettingOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用契约锁电子签功能。
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁开放平台地址;测试环境为 https://openapi.qiyuesuo.cn。
|
||||||
|
/// </summary>
|
||||||
|
public string ServerUrl { get; set; } = "https://openapi.qiyuesuo.cn";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁应用 App Token。
|
||||||
|
/// </summary>
|
||||||
|
public string AppToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁应用 App Secret。
|
||||||
|
/// </summary>
|
||||||
|
public string AppSecret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签名版本;留空使用兼容性最好的 MD5,设置 HMACSHA256 使用新版签名。
|
||||||
|
/// </summary>
|
||||||
|
public string SignatureVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认业务分类 Id。
|
||||||
|
/// </summary>
|
||||||
|
public string CategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 企业自动盖章使用的印章 Id。
|
||||||
|
/// </summary>
|
||||||
|
public string SealId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁企业成员中的合同创建人姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string CreatorName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁企业成员中的合同创建人手机号。
|
||||||
|
/// </summary>
|
||||||
|
public string CreatorMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁合同状态回调地址。
|
||||||
|
/// </summary>
|
||||||
|
public string CallbackUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工从契约锁签署页面返回时跳转的前端地址;可为空。
|
||||||
|
/// </summary>
|
||||||
|
public string SignPageCallbackUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工签署页链接有效分钟数。
|
||||||
|
/// </summary>
|
||||||
|
public int SignUrlExpireMinutes { get; set; } = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DOCX 中定位员工签名位置的关键字。
|
||||||
|
/// </summary>
|
||||||
|
public string EmployeeSignKeyword { get; set; } = "乙方(劳动者)签字:";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工签名关键字在单份文档中重复时使用第几个,从 1 开始。
|
||||||
|
/// </summary>
|
||||||
|
public int EmployeeSignKeywordIndex { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DOCX 中定位企业盖章位置的关键字。
|
||||||
|
/// </summary>
|
||||||
|
public string CompanySealKeyword { get; set; } = "甲方(盖章):";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 企业盖章关键字在单份文档中重复时使用第几个,从 1 开始。
|
||||||
|
/// </summary>
|
||||||
|
public int CompanySealKeywordIndex { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单次 HTTP 请求超时秒数。
|
||||||
|
/// </summary>
|
||||||
|
public int TimeoutSeconds { get; set; } = 90;
|
||||||
|
}
|
||||||
+5
@@ -41,6 +41,11 @@ public class OnboardingContractPreviewInput
|
|||||||
[IdNotNull(ErrorMessage = "入职模板Id不能为空")]
|
[IdNotNull(ErrorMessage = "入职模板Id不能为空")]
|
||||||
public long OnboardingTemplateId { get; set; }
|
public long OnboardingTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次需要预览的合同模板版本 Id;批量预览时无需传入。
|
||||||
|
/// </summary>
|
||||||
|
public long? ContractTemplateVersionId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 人事当前在创建表单中填写的合同变量。
|
/// 人事当前在创建表单中填写的合同变量。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+6
@@ -11,6 +11,12 @@ public interface IOnboardingContractDocumentService : ITransient
|
|||||||
Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
|
Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
|
||||||
OnboardingContractPreviewInput input);
|
OnboardingContractPreviewInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建入职任务前,按模板顺序生成全部合同预览并打包为 ZIP。
|
||||||
|
/// </summary>
|
||||||
|
Task<OnboardingContractPreviewOutput> PreviewAllBeforeCreate(
|
||||||
|
OnboardingContractPreviewInput input);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据已审核通过的任务合同生成并保存 DOCX 文件。
|
/// 根据已审核通过的任务合同生成并保存 DOCX 文件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+170
-50
@@ -15,10 +15,62 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在任务创建前生成单份临时 DOCX,仅通过响应返回,不写文件表和 OSS。
|
/// 在任务创建前生成指定合同的临时 DOCX,仅通过响应返回,不写文件表和 OSS。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
|
public async Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
|
||||||
OnboardingContractPreviewInput input)
|
OnboardingContractPreviewInput input)
|
||||||
|
{
|
||||||
|
var context = await LoadPreviewContext(input);
|
||||||
|
var contractConfig = input.ContractTemplateVersionId.HasValue
|
||||||
|
? context.ContractConfigs.FirstOrDefault(item =>
|
||||||
|
item.ContractTemplateVersionId == input.ContractTemplateVersionId.Value)
|
||||||
|
: context.ContractConfigs.FirstOrDefault();
|
||||||
|
if (contractConfig == null)
|
||||||
|
throw Oops.Bah("所选合同不属于当前入职模板或已被移除");
|
||||||
|
|
||||||
|
return await RenderPreview(input, context, contractConfig, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在任务创建前生成全部合同预览,并按模板顺序打包为 ZIP。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<OnboardingContractPreviewOutput> PreviewAllBeforeCreate(
|
||||||
|
OnboardingContractPreviewInput input)
|
||||||
|
{
|
||||||
|
var context = await LoadPreviewContext(input);
|
||||||
|
using var zipBytes = new MemoryStream();
|
||||||
|
using (var archive = new global::System.IO.Compression.ZipArchive(
|
||||||
|
zipBytes,
|
||||||
|
global::System.IO.Compression.ZipArchiveMode.Create,
|
||||||
|
true))
|
||||||
|
{
|
||||||
|
for (var index = 0; index < context.ContractConfigs.Count; index++)
|
||||||
|
{
|
||||||
|
var preview = await RenderPreview(
|
||||||
|
input,
|
||||||
|
context,
|
||||||
|
context.ContractConfigs[index],
|
||||||
|
index + 1);
|
||||||
|
var entry = archive.CreateEntry(
|
||||||
|
preview.FileName,
|
||||||
|
global::System.IO.Compression.CompressionLevel.Fastest);
|
||||||
|
await using var entryStream = entry.Open();
|
||||||
|
await entryStream.WriteAsync(preview.FileBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OnboardingContractPreviewOutput
|
||||||
|
{
|
||||||
|
FileBytes = zipBytes.ToArray(),
|
||||||
|
FileName = $"全部合同_人事预览_{DateTime.Now:yyyyMMddHHmmss}.zip"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验预览上下文,并读取入职模板锁定的全部合同配置。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<ContractPreviewContext> LoadPreviewContext(
|
||||||
|
OnboardingContractPreviewInput input)
|
||||||
{
|
{
|
||||||
var company = await _db.Queryable<ObCompany>()
|
var company = await _db.Queryable<ObCompany>()
|
||||||
.FirstAsync(item => item.Id == input.CompanyId
|
.FirstAsync(item => item.Id == input.CompanyId
|
||||||
@@ -53,32 +105,97 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
if (!supportsPosition)
|
if (!supportsPosition)
|
||||||
throw Oops.Bah("所选入职模板不适用于该岗位");
|
throw Oops.Bah("所选入职模板不适用于该岗位");
|
||||||
|
|
||||||
// 当前业务只预览入职模板中排序第一的合同,后续多合同可在此扩展。
|
var contractConfigs = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||||
var contractConfig = await _db.Queryable<ObOnboardingTemplateContract>()
|
|
||||||
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
|
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
|
||||||
.OrderBy(item => item.SortCode)
|
.OrderBy(item => item.SortCode)
|
||||||
.FirstAsync()
|
.OrderBy(item => item.Id)
|
||||||
?? throw Oops.Bah("入职模板尚未配置合同文件");
|
.ToListAsync();
|
||||||
|
if (contractConfigs.Count == 0)
|
||||||
|
throw Oops.Bah("入职模板尚未配置合同文件");
|
||||||
|
|
||||||
var contractTemplate = await _db.Queryable<ObContractTemplate>()
|
var duplicateVersion = contractConfigs
|
||||||
.FirstAsync(item => item.Id == contractConfig.ContractTemplateId
|
.GroupBy(item => item.ContractTemplateVersionId)
|
||||||
&& item.CompanyId == input.CompanyId
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
&& item.DepartmentId == input.DepartmentId
|
if (duplicateVersion != null)
|
||||||
&& item.Status == CommonStatusConst.ENABLE)
|
throw Oops.Bah("入职模板中存在重复的合同版本,请先调整模板配置");
|
||||||
?? throw Oops.Bah("合同模板不存在、未启用或不属于当前公司、部门");
|
|
||||||
|
|
||||||
var contractSupportsPosition = await _db.Queryable<ObContractTemplatePosition>()
|
var templateIds = contractConfigs
|
||||||
.AnyAsync(item => item.ContractTemplateId == contractTemplate.Id
|
.Select(item => item.ContractTemplateId)
|
||||||
&& item.PositionId == input.PositionId);
|
.Distinct()
|
||||||
if (!contractSupportsPosition)
|
.ToList();
|
||||||
throw Oops.Bah("合同模板不适用于当前岗位");
|
var contractTemplates = await _db.Queryable<ObContractTemplate>()
|
||||||
|
.Where(item => templateIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (contractTemplates.Count != templateIds.Count
|
||||||
|
|| contractTemplates.Any(item => item.CompanyId != input.CompanyId
|
||||||
|
|| item.DepartmentId != input.DepartmentId
|
||||||
|
|| !string.Equals(
|
||||||
|
item.Status,
|
||||||
|
CommonStatusConst.ENABLE,
|
||||||
|
global::System.StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
throw Oops.Bah("入职模板包含未启用或不属于当前公司、部门的合同模板");
|
||||||
|
}
|
||||||
|
|
||||||
var version = await _db.Queryable<ObContractTemplateVersion>()
|
var supportedTemplateCount = await _db.Queryable<ObContractTemplatePosition>()
|
||||||
.FirstAsync(item => item.Id == contractConfig.ContractTemplateVersionId
|
.CountAsync(item => templateIds.Contains(item.ContractTemplateId)
|
||||||
&& item.TemplateId == contractTemplate.Id
|
&& item.PositionId == input.PositionId);
|
||||||
&& item.VersionStatus == ContractTemplateVersionStatus.Enabled)
|
if (supportedTemplateCount != templateIds.Count)
|
||||||
?? throw Oops.Bah("合同版本不存在、未启用或不属于当前合同模板");
|
throw Oops.Bah("入职模板包含不适用于当前岗位的合同模板");
|
||||||
|
|
||||||
|
var versionIds = contractConfigs
|
||||||
|
.Select(item => item.ContractTemplateVersionId)
|
||||||
|
.ToList();
|
||||||
|
var versions = await _db.Queryable<ObContractTemplateVersion>()
|
||||||
|
.Where(item => versionIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var versionsValid = versions.Count == versionIds.Count
|
||||||
|
&& contractConfigs.All(config => versions.Any(version =>
|
||||||
|
version.Id == config.ContractTemplateVersionId
|
||||||
|
&& version.TemplateId == config.ContractTemplateId
|
||||||
|
&& version.VersionStatus == ContractTemplateVersionStatus.Enabled));
|
||||||
|
if (!versionsValid)
|
||||||
|
throw Oops.Bah("入职模板包含不存在、未启用或归属错误的合同版本");
|
||||||
|
|
||||||
|
var inputs = input.ContractVariables
|
||||||
|
?? new List<OnboardingInitialContractVariableInput>();
|
||||||
|
OnboardingTaskService.ValidateInitialContractDateRanges(inputs);
|
||||||
|
var duplicateInput = inputs
|
||||||
|
.GroupBy(item => new
|
||||||
|
{
|
||||||
|
item.ContractTemplateVersionId,
|
||||||
|
VariableCode = item.VariableCode?.ToUpperInvariant()
|
||||||
|
})
|
||||||
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
|
if (duplicateInput != null)
|
||||||
|
throw Oops.Bah($"合同变量“{duplicateInput.Key.VariableCode}”重复提交");
|
||||||
|
var unknownVersionInput = inputs.FirstOrDefault(item =>
|
||||||
|
!versionIds.Contains(item.ContractTemplateVersionId));
|
||||||
|
if (unknownVersionInput != null)
|
||||||
|
throw Oops.Bah($"合同变量“{unknownVersionInput.VariableCode}”不属于当前入职模板");
|
||||||
|
|
||||||
|
return new ContractPreviewContext
|
||||||
|
{
|
||||||
|
Company = company,
|
||||||
|
ContractConfigs = contractConfigs,
|
||||||
|
ContractTemplates = contractTemplates,
|
||||||
|
Versions = versions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据一条合同配置和当前人事参数渲染一份 DOCX。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<OnboardingContractPreviewOutput> RenderPreview(
|
||||||
|
OnboardingContractPreviewInput input,
|
||||||
|
ContractPreviewContext context,
|
||||||
|
ObOnboardingTemplateContract contractConfig,
|
||||||
|
int? fileOrder)
|
||||||
|
{
|
||||||
|
var contractTemplate = context.ContractTemplates.First(item =>
|
||||||
|
item.Id == contractConfig.ContractTemplateId);
|
||||||
|
var version = context.Versions.First(item =>
|
||||||
|
item.Id == contractConfig.ContractTemplateVersionId);
|
||||||
var definitions = await _db.Queryable<ObTemplateVariable>()
|
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||||
.Where(item => item.TemplateVersionId == version.Id)
|
.Where(item => item.TemplateVersionId == version.Id)
|
||||||
.OrderBy(item => item.SortCode)
|
.OrderBy(item => item.SortCode)
|
||||||
@@ -89,31 +206,20 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
if (duplicateDefinition != null)
|
if (duplicateDefinition != null)
|
||||||
throw Oops.Bah($"合同变量“{duplicateDefinition.Key}”存在重复定义");
|
throw Oops.Bah($"合同变量“{duplicateDefinition.Key}”存在重复定义");
|
||||||
|
|
||||||
var inputs = input.ContractVariables
|
var inputs = (input.ContractVariables
|
||||||
?? new List<OnboardingInitialContractVariableInput>();
|
?? new List<OnboardingInitialContractVariableInput>())
|
||||||
OnboardingTaskService.ValidateInitialContractDateRanges(inputs);
|
.Where(item => item.ContractTemplateVersionId == version.Id)
|
||||||
|
.ToList();
|
||||||
var duplicateInput = inputs
|
|
||||||
.GroupBy(item => new
|
|
||||||
{
|
|
||||||
item.ContractTemplateVersionId,
|
|
||||||
VariableCode = item.VariableCode?.ToUpperInvariant()
|
|
||||||
})
|
|
||||||
.FirstOrDefault(group => group.Count() > 1);
|
|
||||||
if (duplicateInput != null)
|
|
||||||
throw Oops.Bah($"合同变量“{duplicateInput.Key.VariableCode}”重复提交");
|
|
||||||
|
|
||||||
var editableDefinitions = definitions
|
var editableDefinitions = definitions
|
||||||
.Where(IsHrEditableDefinition)
|
.Where(IsHrEditableDefinition)
|
||||||
.ToList();
|
.ToList();
|
||||||
foreach (var submitted in inputs)
|
foreach (var submitted in inputs)
|
||||||
{
|
{
|
||||||
var definitionExists = submitted.ContractTemplateVersionId == version.Id
|
var definitionExists = editableDefinitions.Any(definition =>
|
||||||
&& editableDefinitions.Any(definition =>
|
string.Equals(
|
||||||
string.Equals(
|
definition.VariableCode,
|
||||||
definition.VariableCode,
|
submitted.VariableCode,
|
||||||
submitted.VariableCode,
|
StringComparison.OrdinalIgnoreCase));
|
||||||
StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (!definitionExists)
|
if (!definitionExists)
|
||||||
throw Oops.Bah($"合同变量“{submitted.VariableCode}”不存在或不允许人事填写");
|
throw Oops.Bah($"合同变量“{submitted.VariableCode}”不存在或不允许人事填写");
|
||||||
}
|
}
|
||||||
@@ -124,8 +230,7 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var submitted = inputs.FirstOrDefault(item =>
|
var submitted = inputs.FirstOrDefault(item =>
|
||||||
item.ContractTemplateVersionId == version.Id
|
string.Equals(
|
||||||
&& string.Equals(
|
|
||||||
item.VariableCode,
|
item.VariableCode,
|
||||||
definition.VariableCode,
|
definition.VariableCode,
|
||||||
StringComparison.OrdinalIgnoreCase));
|
StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -150,8 +255,7 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
definition =>
|
definition =>
|
||||||
{
|
{
|
||||||
var submitted = inputs.FirstOrDefault(item =>
|
var submitted = inputs.FirstOrDefault(item =>
|
||||||
item.ContractTemplateVersionId == version.Id
|
string.Equals(
|
||||||
&& string.Equals(
|
|
||||||
item.VariableCode,
|
item.VariableCode,
|
||||||
definition.VariableCode,
|
definition.VariableCode,
|
||||||
StringComparison.OrdinalIgnoreCase));
|
StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -162,7 +266,7 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
definition,
|
definition,
|
||||||
null,
|
null,
|
||||||
emptyDynamicValues,
|
emptyDynamicValues,
|
||||||
company,
|
context.Company,
|
||||||
previewTask,
|
previewTask,
|
||||||
true)
|
true)
|
||||||
?? string.Empty;
|
?? string.Empty;
|
||||||
@@ -181,7 +285,7 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
FileBytes = DocxTemplateProcessor.Render(
|
FileBytes = DocxTemplateProcessor.Render(
|
||||||
sourceBytes.ToArray(),
|
sourceBytes.ToArray(),
|
||||||
values),
|
values),
|
||||||
FileName = BuildPreviewFileName(contractTemplate.TemplateName)
|
FileName = BuildPreviewFileName(contractTemplate.TemplateName, fileOrder)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +301,8 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
var statusAllowsGeneration = task.TaskStatus == OnboardingTaskStatus.PendingFill
|
var statusAllowsGeneration = task.TaskStatus == OnboardingTaskStatus.PendingFill
|
||||||
|| task.TaskStatus == OnboardingTaskStatus.Rejected
|
|| task.TaskStatus == OnboardingTaskStatus.Rejected
|
||||||
|| task.TaskStatus == OnboardingTaskStatus.Reviewing
|
|| task.TaskStatus == OnboardingTaskStatus.Reviewing
|
||||||
|| task.TaskStatus == OnboardingTaskStatus.Approved;
|
|| task.TaskStatus == OnboardingTaskStatus.Approved
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Archived;
|
||||||
if (!statusAllowsGeneration)
|
if (!statusAllowsGeneration)
|
||||||
throw Oops.Bah("当前任务状态不能生成合同");
|
throw Oops.Bah("当前任务状态不能生成合同");
|
||||||
if (contract.GeneratedDocxFileId.HasValue)
|
if (contract.GeneratedDocxFileId.HasValue)
|
||||||
@@ -275,12 +380,27 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成兼容 Windows 和 Linux 的合同预览下载文件名。
|
/// 生成兼容 Windows 和 Linux 的合同预览下载文件名。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string BuildPreviewFileName(string contractName)
|
private static string BuildPreviewFileName(string contractName, int? fileOrder = null)
|
||||||
{
|
{
|
||||||
var invalidChars = global::System.IO.Path.GetInvalidFileNameChars();
|
var invalidChars = global::System.IO.Path.GetInvalidFileNameChars();
|
||||||
var safeName = new string((contractName ?? "合同")
|
var safeName = new string((contractName ?? "合同")
|
||||||
.Select(character => invalidChars.Contains(character) ? '_' : character)
|
.Select(character => invalidChars.Contains(character) ? '_' : character)
|
||||||
.ToArray());
|
.ToArray());
|
||||||
return $"{safeName}_人事预览_{DateTime.Now:yyyyMMddHHmmss}.docx";
|
var orderPrefix = fileOrder.HasValue ? $"{fileOrder.Value:D2}_" : string.Empty;
|
||||||
|
return $"{orderPrefix}{safeName}_人事预览_{DateTime.Now:yyyyMMddHHmmss}.docx";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建任务前预览合同所需的已校验数据。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ContractPreviewContext
|
||||||
|
{
|
||||||
|
public ObCompany Company { get; init; }
|
||||||
|
|
||||||
|
public List<ObOnboardingTemplateContract> ContractConfigs { get; init; }
|
||||||
|
|
||||||
|
public List<ObContractTemplate> ContractTemplates { get; init; }
|
||||||
|
|
||||||
|
public List<ObContractTemplateVersion> Versions { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对指定入职任务执行电子签操作的参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingElectronicSignTaskInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "入职任务Id不能为空")]
|
||||||
|
public long TaskId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 作废入职电子签流程的参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingElectronicSignInvalidInput : OnboardingElectronicSignTaskInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 作废原因。
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "请填写作废原因")]
|
||||||
|
public string Reason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重新发起入职电子签流程的参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingElectronicSignReinitiateInput : OnboardingElectronicSignTaskInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 管理员确认重新发起的原因,随旧流程保留用于审计。
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "请填写重新发起原因")]
|
||||||
|
public string Reason { get; set; }
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 管理端查看的入职电子签流程详情。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingElectronicSignDetailOutput
|
||||||
|
{
|
||||||
|
/// <summary>入职任务 Id。</summary>
|
||||||
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>入职任务编号。</summary>
|
||||||
|
public string TaskNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>当前是否允许发起或重试电子签。</summary>
|
||||||
|
public bool CanInitiate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>不允许发起时的原因。</summary>
|
||||||
|
public string CannotInitiateReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>当前流程是否允许作为一次新签约重新发起。</summary>
|
||||||
|
public bool CanReinitiate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>不允许重新发起时的原因。</summary>
|
||||||
|
public string CannotReinitiateReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否已经存在电子签流程。</summary>
|
||||||
|
public bool HasProcess { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本地电子签流程 Id。</summary>
|
||||||
|
public long? ProcessId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁合同流程 Id。</summary>
|
||||||
|
public string ProviderContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本系统幂等业务编号。</summary>
|
||||||
|
public string BizId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>电子签流程标题。</summary>
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本系统电子签状态。</summary>
|
||||||
|
public string SignStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁原始状态。</summary>
|
||||||
|
public string ProviderStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工姓名。</summary>
|
||||||
|
public string EmployeeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工手机号。</summary>
|
||||||
|
public string EmployeeMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工签署页面地址。</summary>
|
||||||
|
public string SignUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工签署页面到期时间。</summary>
|
||||||
|
public DateTime? SignUrlExpireAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工签署页面当前是否有效。</summary>
|
||||||
|
public bool SignUrlValid { get; set; }
|
||||||
|
|
||||||
|
/// <summary>发起时间。</summary>
|
||||||
|
public DateTime? StartedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>发送时间。</summary>
|
||||||
|
public DateTime? SentAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署完成时间。</summary>
|
||||||
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>归档完成时间。</summary>
|
||||||
|
public DateTime? ArchivedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>完整归档 ZIP 的系统文件 Id。</summary>
|
||||||
|
public long? ArchiveFileId { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>最近一次失败原因。</summary>
|
||||||
|
public string LastError { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签约流程包含的文档。</summary>
|
||||||
|
public List<OnboardingElectronicSignDocumentOutput> Documents { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>该入职任务已经结束的历史电子签流程。</summary>
|
||||||
|
public List<OnboardingElectronicSignHistoryOutput> HistoryProcesses { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已被新流程替代的历史电子签流程摘要。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingElectronicSignHistoryOutput
|
||||||
|
{
|
||||||
|
/// <summary>本地电子签流程 Id。</summary>
|
||||||
|
public long ProcessId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本系统幂等业务编号。</summary>
|
||||||
|
public string BizId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁合同流程 Id。</summary>
|
||||||
|
public string ProviderContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本系统电子签状态。</summary>
|
||||||
|
public string SignStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁原始状态。</summary>
|
||||||
|
public string ProviderStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工姓名快照。</summary>
|
||||||
|
public string EmployeeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工手机号快照。</summary>
|
||||||
|
public string EmployeeMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>被新流程替代的时间。</summary>
|
||||||
|
public DateTime? ReissuedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>管理员填写的重新发起原因。</summary>
|
||||||
|
public string ReissueReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>流程发起时间。</summary>
|
||||||
|
public DateTime? StartedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>流程结束或失败原因。</summary>
|
||||||
|
public string LastError { get; set; }
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 电子签流程中的单份合同文档。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingElectronicSignDocumentOutput
|
||||||
|
{
|
||||||
|
/// <summary>本地电子签文档 Id。</summary>
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>任务合同 Id。</summary>
|
||||||
|
public long TaskContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁文档 Id。</summary>
|
||||||
|
public string ProviderDocumentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文档名称。</summary>
|
||||||
|
public string DocumentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文档顺序。</summary>
|
||||||
|
public int DocumentSort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>发起签约的原始 DOCX 文件 Id。</summary>
|
||||||
|
public long OriginalDocxFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署完成后的 PDF 文件 Id。</summary>
|
||||||
|
public long? SignedPdfFileId { get; set; }
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义入职电子签流程管理能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingElectronicSignService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>查询指定入职任务的电子签详情。</summary>
|
||||||
|
Task<OnboardingElectronicSignDetailOutput> Detail(BaseIdInput input);
|
||||||
|
|
||||||
|
/// <summary>创建或重试电子签流程。</summary>
|
||||||
|
Task<OnboardingElectronicSignDetailOutput> Initiate(
|
||||||
|
OnboardingElectronicSignTaskInput input);
|
||||||
|
|
||||||
|
/// <summary>保留旧流程并创建一次全新的电子签流程。</summary>
|
||||||
|
Task<OnboardingElectronicSignDetailOutput> Reinitiate(
|
||||||
|
OnboardingElectronicSignReinitiateInput input);
|
||||||
|
|
||||||
|
/// <summary>主动同步契约锁状态并在完成后归档。</summary>
|
||||||
|
Task<OnboardingElectronicSignDetailOutput> Sync(
|
||||||
|
OnboardingElectronicSignTaskInput input);
|
||||||
|
|
||||||
|
/// <summary>获取或刷新员工签署地址。</summary>
|
||||||
|
Task<OnboardingElectronicSignDetailOutput> GetSignLink(
|
||||||
|
OnboardingElectronicSignTaskInput input);
|
||||||
|
|
||||||
|
/// <summary>催办当前待签员工。</summary>
|
||||||
|
Task Notice(OnboardingElectronicSignTaskInput input);
|
||||||
|
|
||||||
|
/// <summary>作废尚未结束的签约流程。</summary>
|
||||||
|
Task Invalid(OnboardingElectronicSignInvalidInput input);
|
||||||
|
|
||||||
|
/// <summary>处理契约锁服务器明文回调。</summary>
|
||||||
|
Task HandleCallback(string payloadJson);
|
||||||
|
}
|
||||||
+1284
File diff suppressed because it is too large
Load Diff
+56
@@ -0,0 +1,56 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义契约锁开放平台所需的最小调用能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IQiyuesuoClient : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建包含个人签署方和企业自动盖章动作的合同草稿。
|
||||||
|
/// </summary>
|
||||||
|
Task<QiyuesuoContractResult> CreateDraft(
|
||||||
|
QiyuesuoCreateDraftInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向合同草稿中上传一份 DOCX 文件。
|
||||||
|
/// </summary>
|
||||||
|
Task<QiyuesuoDocumentResult> AddDocument(
|
||||||
|
QiyuesuoAddDocumentInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发起合同并为每份文档指定员工签名与企业盖章位置。
|
||||||
|
/// </summary>
|
||||||
|
Task<QiyuesuoContractResult> Send(
|
||||||
|
QiyuesuoSendContractInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取员工签署页面地址。
|
||||||
|
/// </summary>
|
||||||
|
Task<string> GetSignPageUrl(
|
||||||
|
QiyuesuoSignPageInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询契约锁合同详情。
|
||||||
|
/// </summary>
|
||||||
|
Task<QiyuesuoContractResult> GetContractDetail(string contractId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 催办当前待签署人。
|
||||||
|
/// </summary>
|
||||||
|
Task Notice(string contractId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 作废尚未完成的合同流程。
|
||||||
|
/// </summary>
|
||||||
|
Task Invalid(string contractId, string reason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载单份签署完成的 PDF 文档。
|
||||||
|
/// </summary>
|
||||||
|
Task<byte[]> DownloadDocument(string documentId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载合同原文、签署文件与签署日志组成的完整归档 ZIP。
|
||||||
|
/// </summary>
|
||||||
|
Task<byte[]> DownloadArchive(string contractId);
|
||||||
|
}
|
||||||
+692
@@ -0,0 +1,692 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直接调用契约锁开放平台,不依赖旧版 .NET Framework SDK。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoClient : IQiyuesuoClient
|
||||||
|
{
|
||||||
|
private const string SignatureHmacSha256 = "HMACSHA256";
|
||||||
|
private const string AccessTokenHeader = "x-qys-open-accesstoken";
|
||||||
|
private const string TimestampHeader = "x-qys-open-timestamp";
|
||||||
|
private const string NonceHeader = "x-qys-open-nonce";
|
||||||
|
private const string SignatureHeader = "x-qys-open-signature";
|
||||||
|
private const string SignatureVersionHeader = "x-qys-open-signature-version";
|
||||||
|
private const string RequestBodySignatureKey = "x-qys-open-body";
|
||||||
|
private static readonly HttpClient HttpClient = new()
|
||||||
|
{
|
||||||
|
Timeout = Timeout.InfiniteTimeSpan
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
|
||||||
|
|
||||||
|
private readonly QiyuesuoSettingOptions _setting;
|
||||||
|
|
||||||
|
public QiyuesuoClient()
|
||||||
|
{
|
||||||
|
_setting = App.GetConfig<QiyuesuoSettingOptions>(
|
||||||
|
"QiyuesuoSetting",
|
||||||
|
true)
|
||||||
|
?? new QiyuesuoSettingOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<QiyuesuoContractResult> CreateDraft(
|
||||||
|
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 = true,
|
||||||
|
sealId = input.SealId,
|
||||||
|
corpSealIds = new[] { sealId },
|
||||||
|
actionNo = "company_seal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return await PostJson<QiyuesuoContractResult>(
|
||||||
|
"/v2/contract/draft",
|
||||||
|
body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<QiyuesuoDocumentResult> AddDocument(
|
||||||
|
QiyuesuoAddDocumentInput input)
|
||||||
|
{
|
||||||
|
var fields = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["contractId"] = input.ContractId,
|
||||||
|
["bizId"] = input.BizId,
|
||||||
|
["title"] = input.Title,
|
||||||
|
["fileSuffix"] = "docx",
|
||||||
|
["documentSort"] = input.DocumentSort.ToString(
|
||||||
|
CultureInfo.InvariantCulture)
|
||||||
|
};
|
||||||
|
return await PostFile<QiyuesuoDocumentResult>(
|
||||||
|
"/v2/document/addbyfile",
|
||||||
|
fields,
|
||||||
|
input.FileBytes,
|
||||||
|
input.FileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<QiyuesuoContractResult> Send(
|
||||||
|
QiyuesuoSendContractInput input)
|
||||||
|
{
|
||||||
|
return PostJson<QiyuesuoContractResult>(
|
||||||
|
"/v2/contract/send",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
contractId = input.ContractId,
|
||||||
|
bizId = input.BizId,
|
||||||
|
tenantName = input.CompanyName,
|
||||||
|
locateAllStamperKeywords = false,
|
||||||
|
stampers = input.Stampers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<string> GetSignPageUrl(
|
||||||
|
QiyuesuoSignPageInput input)
|
||||||
|
{
|
||||||
|
var result = await PostJsonElement(
|
||||||
|
"/v2/contract/pageurl",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
contractId = input.ContractId,
|
||||||
|
user = new
|
||||||
|
{
|
||||||
|
contact = input.EmployeeMobile,
|
||||||
|
contactType = "MOBILE"
|
||||||
|
},
|
||||||
|
callbackPage = NullIfBlank(input.CallbackPage)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ValueKind == JsonValueKind.String)
|
||||||
|
return result.GetString();
|
||||||
|
if (TryGetString(result, "url", out var url)
|
||||||
|
|| TryGetString(result, "pageUrl", out url)
|
||||||
|
|| TryGetString(result, "signUrl", out url))
|
||||||
|
{
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Oops.Bah("契约锁未返回员工签署页面地址");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<QiyuesuoContractResult> GetContractDetail(string contractId)
|
||||||
|
{
|
||||||
|
return GetJson<QiyuesuoContractResult>(
|
||||||
|
"/v2/contract/detail",
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["contractId"] = contractId,
|
||||||
|
["queryActionOperate"] = "true",
|
||||||
|
["queryActionOperator"] = "true",
|
||||||
|
["queryLocation"] = "true"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Notice(string contractId)
|
||||||
|
{
|
||||||
|
await PostJsonElement(
|
||||||
|
"/v2/contract/notice",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
contractId,
|
||||||
|
onlySigning = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Invalid(string contractId, string reason)
|
||||||
|
{
|
||||||
|
await PostJsonElement(
|
||||||
|
"/v2/contract/invalid",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
contractId,
|
||||||
|
sealId = _setting.SealId,
|
||||||
|
reason,
|
||||||
|
deleteDoc = false,
|
||||||
|
autoSign = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<byte[]> DownloadDocument(string documentId)
|
||||||
|
{
|
||||||
|
return Download(
|
||||||
|
"/v2/document/download",
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["documentId"] = documentId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<byte[]> DownloadArchive(string contractId)
|
||||||
|
{
|
||||||
|
return Download(
|
||||||
|
"/v2/contract/download",
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["contractId"] = contractId,
|
||||||
|
["downloadItems"] = "CONTRACT,SIGNLOG",
|
||||||
|
["needCompressForOneFile"] = "true"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> PostJson<T>(string path, object body)
|
||||||
|
{
|
||||||
|
var result = await PostJsonElement(path, body);
|
||||||
|
return result.Deserialize<T>(JsonOptions)
|
||||||
|
?? throw Oops.Bah("契约锁返回数据为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<JsonElement> PostJsonElement(
|
||||||
|
string path,
|
||||||
|
object body)
|
||||||
|
{
|
||||||
|
ValidateSettings();
|
||||||
|
var json = JsonSerializer.Serialize(body, JsonOptions);
|
||||||
|
using var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Post,
|
||||||
|
BuildUrl(path));
|
||||||
|
request.Content = new StringContent(
|
||||||
|
json,
|
||||||
|
Encoding.UTF8,
|
||||||
|
"application/json");
|
||||||
|
AddAuthorizationHeaders(
|
||||||
|
request,
|
||||||
|
path,
|
||||||
|
HttpMethod.Post,
|
||||||
|
jsonBody: json);
|
||||||
|
return await SendForJsonResult(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> PostFile<T>(
|
||||||
|
string path,
|
||||||
|
Dictionary<string, string> fields,
|
||||||
|
byte[] fileBytes,
|
||||||
|
string fileName)
|
||||||
|
{
|
||||||
|
ValidateSettings();
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
foreach (var field in fields)
|
||||||
|
{
|
||||||
|
content.Add(
|
||||||
|
new StringContent(field.Value ?? string.Empty, Encoding.UTF8),
|
||||||
|
field.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileContent = new ByteArrayContent(fileBytes);
|
||||||
|
fileContent.Headers.ContentType = new MediaTypeHeaderValue(
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||||
|
content.Add(fileContent, "file", fileName);
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Post,
|
||||||
|
BuildUrl(path))
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
AddAuthorizationHeaders(
|
||||||
|
request,
|
||||||
|
path,
|
||||||
|
HttpMethod.Post,
|
||||||
|
fields,
|
||||||
|
fileBytes);
|
||||||
|
var result = await SendForJsonResult(request);
|
||||||
|
return result.Deserialize<T>(JsonOptions)
|
||||||
|
?? throw Oops.Bah("契约锁返回数据为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> GetJson<T>(
|
||||||
|
string path,
|
||||||
|
Dictionary<string, string> query)
|
||||||
|
{
|
||||||
|
ValidateSettings();
|
||||||
|
using var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Get,
|
||||||
|
BuildUrl(path, query));
|
||||||
|
AddAuthorizationHeaders(
|
||||||
|
request,
|
||||||
|
path,
|
||||||
|
HttpMethod.Get,
|
||||||
|
query);
|
||||||
|
var result = await SendForJsonResult(request);
|
||||||
|
return result.Deserialize<T>(JsonOptions)
|
||||||
|
?? throw Oops.Bah("契约锁返回数据为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> Download(
|
||||||
|
string path,
|
||||||
|
Dictionary<string, string> query)
|
||||||
|
{
|
||||||
|
ValidateSettings();
|
||||||
|
using var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Get,
|
||||||
|
BuildUrl(path, query));
|
||||||
|
AddAuthorizationHeaders(
|
||||||
|
request,
|
||||||
|
path,
|
||||||
|
HttpMethod.Get,
|
||||||
|
query);
|
||||||
|
using var response = await Send(request);
|
||||||
|
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||||
|
if (!response.IsSuccessStatusCode || LooksLikeJson(response, bytes))
|
||||||
|
{
|
||||||
|
ThrowProviderError(bytes, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes.Length == 0)
|
||||||
|
throw Oops.Bah("契约锁下载文件内容为空");
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<JsonElement> SendForJsonResult(
|
||||||
|
HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
using var response = await Send(request);
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(responseText))
|
||||||
|
throw Oops.Bah("契约锁返回内容为空");
|
||||||
|
|
||||||
|
using var document = JsonDocument.Parse(responseText);
|
||||||
|
var root = document.RootElement;
|
||||||
|
var hasCode = root.TryGetProperty("code", out var codeElement);
|
||||||
|
var code = hasCode ? ReadCode(codeElement) : 0;
|
||||||
|
var hasResponseCode = TryGetString(
|
||||||
|
root,
|
||||||
|
"responseCode",
|
||||||
|
out var responseCode);
|
||||||
|
var responseCodeSucceeded = !hasResponseCode
|
||||||
|
|| responseCode == "0"
|
||||||
|
|| responseCode == "00000000";
|
||||||
|
if (!response.IsSuccessStatusCode
|
||||||
|
|| (hasCode && code != 0)
|
||||||
|
|| !responseCodeSucceeded)
|
||||||
|
{
|
||||||
|
var hasMessage = TryGetString(
|
||||||
|
root,
|
||||||
|
"message",
|
||||||
|
out var providerMessage)
|
||||||
|
|| TryGetString(
|
||||||
|
root,
|
||||||
|
"responseMessage",
|
||||||
|
out providerMessage);
|
||||||
|
var message = hasMessage
|
||||||
|
? providerMessage
|
||||||
|
: $"HTTP {(int)response.StatusCode},响应码 {responseCode ?? code.ToString()}";
|
||||||
|
var providerCode = hasResponseCode
|
||||||
|
? responseCode
|
||||||
|
: hasCode
|
||||||
|
? code.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
throw Oops.Bah($"契约锁请求失败:{message}(响应码:{providerCode})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("result", out var result))
|
||||||
|
return root.Clone();
|
||||||
|
return result.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> Send(
|
||||||
|
HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
using var timeout = new CancellationTokenSource(
|
||||||
|
TimeSpan.FromSeconds(Math.Max(10, _setting.TimeoutSeconds)));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await HttpClient.SendAsync(
|
||||||
|
request,
|
||||||
|
HttpCompletionOption.ResponseHeadersRead,
|
||||||
|
timeout.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("契约锁请求超时,请稍后重试");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException exception)
|
||||||
|
{
|
||||||
|
throw Oops.Bah($"无法连接契约锁:{exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddAuthorizationHeaders(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
string path,
|
||||||
|
HttpMethod method,
|
||||||
|
Dictionary<string, string> parameters = null,
|
||||||
|
byte[] fileBytes = null,
|
||||||
|
string jsonBody = null)
|
||||||
|
{
|
||||||
|
var timestamp = DateTimeOffset.UtcNow
|
||||||
|
.ToUnixTimeMilliseconds()
|
||||||
|
.ToString(CultureInfo.InvariantCulture);
|
||||||
|
var nonce = Guid.NewGuid().ToString();
|
||||||
|
var signature = string.Equals(
|
||||||
|
_setting.SignatureVersion,
|
||||||
|
SignatureHmacSha256,
|
||||||
|
StringComparison.OrdinalIgnoreCase)
|
||||||
|
? BuildHmacSha256Signature(
|
||||||
|
path,
|
||||||
|
method,
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
parameters,
|
||||||
|
fileBytes,
|
||||||
|
jsonBody)
|
||||||
|
: BuildMd5Signature(timestamp, nonce);
|
||||||
|
|
||||||
|
request.Headers.TryAddWithoutValidation(
|
||||||
|
AccessTokenHeader,
|
||||||
|
_setting.AppToken.Trim());
|
||||||
|
request.Headers.TryAddWithoutValidation(TimestampHeader, timestamp);
|
||||||
|
request.Headers.TryAddWithoutValidation(NonceHeader, nonce);
|
||||||
|
request.Headers.TryAddWithoutValidation(SignatureHeader, signature);
|
||||||
|
request.Headers.TryAddWithoutValidation("version", "OnboardX-1.0");
|
||||||
|
if (!string.IsNullOrWhiteSpace(_setting.SignatureVersion))
|
||||||
|
{
|
||||||
|
request.Headers.TryAddWithoutValidation(
|
||||||
|
SignatureVersionHeader,
|
||||||
|
_setting.SignatureVersion.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildMd5Signature(string timestamp, string nonce)
|
||||||
|
{
|
||||||
|
var source = _setting.AppToken.Trim()
|
||||||
|
+ _setting.AppSecret.Trim()
|
||||||
|
+ timestamp
|
||||||
|
+ nonce;
|
||||||
|
return Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(source)))
|
||||||
|
.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildHmacSha256Signature(
|
||||||
|
string path,
|
||||||
|
HttpMethod method,
|
||||||
|
string timestamp,
|
||||||
|
string nonce,
|
||||||
|
Dictionary<string, string> parameters,
|
||||||
|
byte[] fileBytes,
|
||||||
|
string jsonBody)
|
||||||
|
{
|
||||||
|
var values = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new(AccessTokenHeader, _setting.AppToken.Trim()),
|
||||||
|
new(TimestampHeader, timestamp),
|
||||||
|
new(NonceHeader, nonce),
|
||||||
|
new(SignatureVersionHeader, SignatureHmacSha256)
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(jsonBody))
|
||||||
|
values.Add(new KeyValuePair<string, string>(
|
||||||
|
RequestBodySignatureKey,
|
||||||
|
jsonBody));
|
||||||
|
if (parameters != null)
|
||||||
|
{
|
||||||
|
values.AddRange(parameters.Where(item =>
|
||||||
|
!string.IsNullOrWhiteSpace(item.Key)
|
||||||
|
&& !string.IsNullOrWhiteSpace(item.Value)));
|
||||||
|
}
|
||||||
|
if (fileBytes != null)
|
||||||
|
{
|
||||||
|
values.Add(new KeyValuePair<string, string>(
|
||||||
|
"file",
|
||||||
|
Convert.ToHexString(MD5.HashData(fileBytes))
|
||||||
|
.ToLowerInvariant()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new StringBuilder()
|
||||||
|
.Append("requestMethod=")
|
||||||
|
.Append(method.Method)
|
||||||
|
.Append("&requestUrl=")
|
||||||
|
.Append(path);
|
||||||
|
foreach (var value in values
|
||||||
|
.OrderBy(item => item.Key, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
builder.Append('&')
|
||||||
|
.Append(value.Key.Trim())
|
||||||
|
.Append('=')
|
||||||
|
.Append(value.Value.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA256(
|
||||||
|
Encoding.UTF8.GetBytes(_setting.AppSecret.Trim()));
|
||||||
|
return Convert.ToHexString(
|
||||||
|
hmac.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())))
|
||||||
|
.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildUrl(
|
||||||
|
string path,
|
||||||
|
Dictionary<string, string> query = null)
|
||||||
|
{
|
||||||
|
var url = $"{_setting.ServerUrl.Trim().TrimEnd('/')}/{path.TrimStart('/')}";
|
||||||
|
if (query == null || query.Count == 0)
|
||||||
|
return url;
|
||||||
|
var queryString = string.Join("&", query.Select(item =>
|
||||||
|
Uri.EscapeDataString(item.Key)
|
||||||
|
+ "="
|
||||||
|
+ Uri.EscapeDataString(item.Value ?? string.Empty)));
|
||||||
|
return url + "?" + queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateSettings()
|
||||||
|
{
|
||||||
|
if (!_setting.Enabled)
|
||||||
|
throw Oops.Bah("契约锁电子签功能尚未启用");
|
||||||
|
if (string.IsNullOrWhiteSpace(_setting.ServerUrl))
|
||||||
|
throw Oops.Bah("请配置契约锁 ServerUrl");
|
||||||
|
if (string.IsNullOrWhiteSpace(_setting.AppToken))
|
||||||
|
throw Oops.Bah("请配置契约锁 AppToken");
|
||||||
|
if (string.IsNullOrWhiteSpace(_setting.AppSecret))
|
||||||
|
throw Oops.Bah("请配置契约锁 AppSecret");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadCode(JsonElement element)
|
||||||
|
{
|
||||||
|
if (element.ValueKind == JsonValueKind.Number
|
||||||
|
&& element.TryGetInt32(out var numericCode))
|
||||||
|
{
|
||||||
|
return numericCode;
|
||||||
|
}
|
||||||
|
return int.TryParse(element.ToString(), out var textCode)
|
||||||
|
? textCode
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetString(
|
||||||
|
JsonElement element,
|
||||||
|
string propertyName,
|
||||||
|
out string value)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
if (element.ValueKind != JsonValueKind.Object
|
||||||
|
|| !element.TryGetProperty(propertyName, out var property)
|
||||||
|
|| property.ValueKind == JsonValueKind.Null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = property.ValueKind == JsonValueKind.String
|
||||||
|
? property.GetString()
|
||||||
|
: property.ToString();
|
||||||
|
return !string.IsNullOrWhiteSpace(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeJson(
|
||||||
|
HttpResponseMessage response,
|
||||||
|
byte[] bytes)
|
||||||
|
{
|
||||||
|
var contentType = response.Content.Headers.ContentType?.MediaType;
|
||||||
|
if (!string.IsNullOrWhiteSpace(contentType)
|
||||||
|
&& contentType.Contains("json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = bytes.FirstOrDefault(item =>
|
||||||
|
!char.IsWhiteSpace((char)item));
|
||||||
|
return first == '{' || first == '[';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ThrowProviderError(
|
||||||
|
byte[] bytes,
|
||||||
|
global::System.Net.HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(text);
|
||||||
|
var root = document.RootElement;
|
||||||
|
var hasMessage = TryGetString(
|
||||||
|
root,
|
||||||
|
"message",
|
||||||
|
out var providerMessage)
|
||||||
|
|| TryGetString(
|
||||||
|
root,
|
||||||
|
"responseMessage",
|
||||||
|
out providerMessage);
|
||||||
|
var message = hasMessage ? providerMessage : text;
|
||||||
|
throw Oops.Bah($"契约锁下载失败:{message}");
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"契约锁下载失败:HTTP {(int)statusCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NullIfBlank(string value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造兼容契约锁长整型 Id 和字符串 Id 两种返回形式的 JSON 配置。
|
||||||
|
/// </summary>
|
||||||
|
private static JsonSerializerOptions CreateJsonOptions()
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
options.Converters.Add(new FlexibleStringJsonConverter());
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将第三方响应中的字符串或数字统一读取为字符串,避免长整型 Id 精度丢失。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class FlexibleStringJsonConverter : JsonConverter<string>
|
||||||
|
{
|
||||||
|
public override string Read(
|
||||||
|
ref Utf8JsonReader reader,
|
||||||
|
Type typeToConvert,
|
||||||
|
JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.String => reader.GetString(),
|
||||||
|
JsonTokenType.Number => GetRawNumber(ref reader),
|
||||||
|
JsonTokenType.True => bool.TrueString,
|
||||||
|
JsonTokenType.False => bool.FalseString,
|
||||||
|
_ => reader.GetString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(
|
||||||
|
Utf8JsonWriter writer,
|
||||||
|
string value,
|
||||||
|
JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRawNumber(ref Utf8JsonReader reader)
|
||||||
|
{
|
||||||
|
if (reader.TryGetInt64(out var integer))
|
||||||
|
return integer.ToString(CultureInfo.InvariantCulture);
|
||||||
|
return reader.GetDecimal().ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建契约锁合同草稿所需参数。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoCreateDraftInput
|
||||||
|
{
|
||||||
|
/// <summary>本系统幂等业务编号。</summary>
|
||||||
|
public string BizId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同流程标题。</summary>
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署企业法定名称。</summary>
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工姓名。</summary>
|
||||||
|
public string EmployeeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工手机号。</summary>
|
||||||
|
public string EmployeeMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁业务分类 Id。</summary>
|
||||||
|
public string CategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁企业印章 Id。</summary>
|
||||||
|
public string SealId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁企业成员姓名。</summary>
|
||||||
|
public string CreatorName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁企业成员手机号。</summary>
|
||||||
|
public string CreatorMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁服务器回调地址。</summary>
|
||||||
|
public string CallbackUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上传契约锁合同文档所需参数。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoAddDocumentInput
|
||||||
|
{
|
||||||
|
/// <summary>契约锁合同流程 Id。</summary>
|
||||||
|
public string ContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本系统幂等业务编号。</summary>
|
||||||
|
public string BizId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文档标题。</summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文档排序。</summary>
|
||||||
|
public int DocumentSort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>上传文件名。</summary>
|
||||||
|
public string FileName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>DOCX 文件内容。</summary>
|
||||||
|
public byte[] FileBytes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁签章位置。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
/// <summary>企业签署动作 Id。</summary>
|
||||||
|
public string ActionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>个人签署方 Id。</summary>
|
||||||
|
public string SignatoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签章类型。</summary>
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁文档 Id。</summary>
|
||||||
|
public string DocumentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>企业印章 Id。</summary>
|
||||||
|
public string SealId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署位置关键字。</summary>
|
||||||
|
public string Keyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>同名关键字的序号,从 1 开始。</summary>
|
||||||
|
public int KeywordIndex { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发起契约锁合同流程所需参数。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoSendContractInput
|
||||||
|
{
|
||||||
|
/// <summary>契约锁合同流程 Id。</summary>
|
||||||
|
public string ContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本系统幂等业务编号。</summary>
|
||||||
|
public string BizId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁合同发起企业名称。</summary>
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>所有文档的签名和盖章位置。</summary>
|
||||||
|
public List<QiyuesuoStamperInput> Stampers { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取契约锁员工签署页所需参数。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoSignPageInput
|
||||||
|
{
|
||||||
|
/// <summary>契约锁合同流程 Id。</summary>
|
||||||
|
public string ContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>员工手机号。</summary>
|
||||||
|
public string EmployeeMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署完成后的前端返回地址。</summary>
|
||||||
|
public string CallbackPage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁合同返回对象。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoContractResult
|
||||||
|
{
|
||||||
|
/// <summary>契约锁合同流程 Id。</summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>兼容部分接口返回的合同流程 Id 字段名。</summary>
|
||||||
|
public string ContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本系统幂等业务编号。</summary>
|
||||||
|
public string BizId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>流程标题。</summary>
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁合同状态。</summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署方列表。</summary>
|
||||||
|
public List<QiyuesuoSignatoryResult> Signatories { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>合同文档列表。</summary>
|
||||||
|
public List<QiyuesuoDocumentResult> Documents { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁签署方返回对象。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoSignatoryResult
|
||||||
|
{
|
||||||
|
/// <summary>签署方 Id。</summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>兼容部分接口返回的签署方 Id 字段名。</summary>
|
||||||
|
public string SignatoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署方类型。</summary>
|
||||||
|
public string TenantType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署方名称。</summary>
|
||||||
|
public string TenantName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署方状态。</summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署顺序。</summary>
|
||||||
|
public int? SerialNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>企业签署动作。</summary>
|
||||||
|
public List<QiyuesuoActionResult> Actions { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁企业签署动作返回对象。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoActionResult
|
||||||
|
{
|
||||||
|
/// <summary>签署动作 Id。</summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>兼容部分接口返回的签署动作 Id 字段名。</summary>
|
||||||
|
public string ActionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署动作类型。</summary>
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署动作状态。</summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>动作执行顺序。</summary>
|
||||||
|
public int? SerialNo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁合同文档返回对象。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoDocumentResult
|
||||||
|
{
|
||||||
|
/// <summary>契约锁文档 Id。</summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>兼容部分接口返回的文档 Id 字段名。</summary>
|
||||||
|
public string DocumentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文档标题。</summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文档页数。</summary>
|
||||||
|
public int? PageCount { get; set; }
|
||||||
|
}
|
||||||
@@ -56,6 +56,17 @@ public class OnboardingEmployeeDetailOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public List<ObTaskContract> Contracts { get; set; } = new List<ObTaskContract>();
|
public List<ObTaskContract> Contracts { get; set; } = new List<ObTaskContract>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工入职时上传的身份证、学历证明、体检报告等资料。
|
||||||
|
/// </summary>
|
||||||
|
public List<OnboardingTaskMaterialDetailOutput> Materials { get; set; }
|
||||||
|
= new List<OnboardingTaskMaterialDetailOutput>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁签署文件与签署日志组成的完整归档 ZIP 文件 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? ElectronicSignArchiveFileId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 提交与审核记录列表。
|
/// 提交与审核记录列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
|
|||||||
: await _db.Queryable<ObTaskContract>()
|
: await _db.Queryable<ObTaskContract>()
|
||||||
.Where(item => item.TaskId == task.Id)
|
.Where(item => item.TaskId == task.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
var materials = await GetEmployeeMaterials(task?.Id);
|
||||||
|
var electronicSignProcess = task == null
|
||||||
|
? null
|
||||||
|
: await _db.Queryable<ObElectronicSignProcess>()
|
||||||
|
.Where(item => item.TaskId == task.Id && item.ArchiveFileId.HasValue)
|
||||||
|
.OrderByDescending(item => item.Id)
|
||||||
|
.FirstAsync();
|
||||||
var audits = task == null
|
var audits = task == null
|
||||||
? new List<ObTaskAudit>()
|
? new List<ObTaskAudit>()
|
||||||
: await _db.Queryable<ObTaskAudit>()
|
: await _db.Queryable<ObTaskAudit>()
|
||||||
@@ -80,10 +87,60 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
|
|||||||
Task = task,
|
Task = task,
|
||||||
EmployeeInfo = employeeInfo,
|
EmployeeInfo = employeeInfo,
|
||||||
Contracts = contracts,
|
Contracts = contracts,
|
||||||
|
Materials = materials,
|
||||||
|
ElectronicSignArchiveFileId = electronicSignProcess?.ArchiveFileId,
|
||||||
Audits = audits
|
Audits = audits
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询员工来源任务中上传的全部材料,并补充原始文件名、后缀和文件大小。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<OnboardingTaskMaterialDetailOutput>> GetEmployeeMaterials(long? taskId)
|
||||||
|
{
|
||||||
|
if (!taskId.HasValue)
|
||||||
|
return new List<OnboardingTaskMaterialDetailOutput>();
|
||||||
|
|
||||||
|
var materials = await _db.Queryable<ObTaskMaterial>()
|
||||||
|
.Where(item => item.TaskId == taskId.Value)
|
||||||
|
.ToListAsync();
|
||||||
|
if (materials.Count == 0)
|
||||||
|
return new List<OnboardingTaskMaterialDetailOutput>();
|
||||||
|
|
||||||
|
var fileIds = materials
|
||||||
|
.Select(item => item.FileId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var files = await _db.Queryable<SysFile>()
|
||||||
|
.Where(item => fileIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var fileMap = files.ToDictionary(item => item.Id);
|
||||||
|
|
||||||
|
var requirements = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||||
|
.Where(item => item.TaskId == taskId.Value)
|
||||||
|
.ToListAsync();
|
||||||
|
var sortMap = requirements
|
||||||
|
.GroupBy(item => item.MaterialTypeId)
|
||||||
|
.ToDictionary(group => group.Key, group => group.Min(item => item.SortCode));
|
||||||
|
|
||||||
|
return materials
|
||||||
|
.Select(material =>
|
||||||
|
{
|
||||||
|
var output = material.Adapt<OnboardingTaskMaterialDetailOutput>();
|
||||||
|
if (fileMap.TryGetValue(material.FileId, out var file))
|
||||||
|
{
|
||||||
|
output.FileName = file.Name;
|
||||||
|
output.FileSuffix = file.Suffix;
|
||||||
|
output.FileSizeInfo = file.SizeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
})
|
||||||
|
.OrderBy(item => sortMap.GetValueOrDefault(item.MaterialTypeId, int.MaxValue))
|
||||||
|
.ThenBy(item => item.UploadedAt)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 批量补充员工所属公司、部门和岗位名称,避免列表逐行查询数据库。
|
/// 批量补充员工所属公司、部门和岗位名称,避免列表逐行查询数据库。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+4
-1
@@ -299,7 +299,10 @@ public class OnboardingTemplateService : IOnboardingTemplateService
|
|||||||
|
|
||||||
if (input.Status == "enable")
|
if (input.Status == "enable")
|
||||||
{
|
{
|
||||||
if (contractTemplates.Any(item => item.Status != "enable"))
|
if (contractTemplates.Any(item => !string.Equals(
|
||||||
|
item.Status,
|
||||||
|
"enable",
|
||||||
|
global::System.StringComparison.OrdinalIgnoreCase)))
|
||||||
throw Oops.Bah("启用入职模板前,请先启用全部合同模板");
|
throw Oops.Bah("启用入职模板前,请先启用全部合同模板");
|
||||||
if (versions.Any(item => item.VersionStatus != ContractTemplateVersionStatus.Enabled))
|
if (versions.Any(item => item.VersionStatus != ContractTemplateVersionStatus.Enabled))
|
||||||
throw Oops.Bah("启用入职模板前,请先启用全部合同版本");
|
throw Oops.Bah("启用入职模板前,请先启用全部合同版本");
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ public class OnboardingTaskService : IOnboardingTaskService
|
|||||||
if (contractTemplates.Count != templateIds.Count
|
if (contractTemplates.Count != templateIds.Count
|
||||||
|| contractTemplates.Any(item => item.CompanyId != input.CompanyId
|
|| contractTemplates.Any(item => item.CompanyId != input.CompanyId
|
||||||
|| item.DepartmentId != input.DepartmentId
|
|| item.DepartmentId != input.DepartmentId
|
||||||
|| item.Status != "enable"))
|
|| !string.Equals(
|
||||||
|
item.Status,
|
||||||
|
"enable",
|
||||||
|
global::System.StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
throw Oops.Bah("入职模板包含未启用或不属于当前公司、部门的合同模板");
|
throw Oops.Bah("入职模板包含未启用或不属于当前公司、部门的合同模板");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,4 +134,9 @@ public class OnboardingTaskMaterialDetailOutput : ObTaskMaterial
|
|||||||
/// 材料文件后缀,用于判断是否支持图片预览。
|
/// 材料文件后缀,用于判断是否支持图片预览。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string FileSuffix { get; set; }
|
public string FileSuffix { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化后的文件大小,例如 1.5MB。
|
||||||
|
/// </summary>
|
||||||
|
public string FileSizeInfo { get; set; }
|
||||||
}
|
}
|
||||||
@@ -124,6 +124,7 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
|
|||||||
{
|
{
|
||||||
output.FileName = file.Name;
|
output.FileName = file.Name;
|
||||||
output.FileSuffix = file.Suffix;
|
output.FileSuffix = file.Suffix;
|
||||||
|
output.FileSizeInfo = file.SizeInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
|
|||||||
@@ -115,10 +115,26 @@ public class FileService : DbRepository<SysFile>, IFileService
|
|||||||
IFormFile file = new FormFile(stream, 0, content.Length, "file", fileName)
|
IFormFile file = new FormFile(stream, 0, content.Length, "file", fileName)
|
||||||
{
|
{
|
||||||
Headers = new HeaderDictionary(),
|
Headers = new HeaderDictionary(),
|
||||||
ContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
ContentType = ResolveGeneratedContentType(fileName)
|
||||||
};
|
};
|
||||||
return await StorageFile(engine, file);
|
return await StorageFile(engine, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据归档文件扩展名设置 OSS 元数据,避免 PDF、ZIP 被错误标记为 DOCX。
|
||||||
|
/// </summary>
|
||||||
|
private static string ResolveGeneratedContentType(string fileName)
|
||||||
|
{
|
||||||
|
return Path.GetExtension(fileName).ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
".pdf" => "application/pdf",
|
||||||
|
".zip" => "application/zip",
|
||||||
|
".png" => "image/png",
|
||||||
|
".jpg" or ".jpeg" => "image/jpeg",
|
||||||
|
_ => "application/octet-stream"
|
||||||
|
};
|
||||||
|
}
|
||||||
#region 方法
|
#region 方法
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+16
-1
@@ -19,7 +19,7 @@ public class OnboardingContractDocumentController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建入职任务前,按人事当前填写的参数下载 DOCX 合同预览。
|
/// 创建入职任务前,按人事当前填写的参数下载指定 DOCX 合同预览。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("preview-before-create")]
|
[HttpPost("preview-before-create")]
|
||||||
[DisplayName("创建入职任务前预览合同")]
|
[DisplayName("创建入职任务前预览合同")]
|
||||||
@@ -35,6 +35,21 @@ public class OnboardingContractDocumentController
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建入职任务前,将全部 DOCX 合同预览打包下载。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("preview-all-before-create")]
|
||||||
|
[DisplayName("创建入职任务前预览全部合同")]
|
||||||
|
public async Task<FileContentResult> PreviewAllBeforeCreate(
|
||||||
|
[FromBody] OnboardingContractPreviewInput input)
|
||||||
|
{
|
||||||
|
var preview = await _service.PreviewAllBeforeCreate(input);
|
||||||
|
return new FileContentResult(preview.FileBytes, "application/zip")
|
||||||
|
{
|
||||||
|
FileDownloadName = preview.FileName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成入职合同文件
|
/// 生成入职合同文件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Web.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 接收契约锁服务器状态通知;第一版按平台要求使用明文回调。
|
||||||
|
/// </summary>
|
||||||
|
[ApiDescriptionSettings("Application", Tag = "入职电子签回调")]
|
||||||
|
[Route("open/onboarding/qiyuesuo")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class OnboardingElectronicSignCallbackController
|
||||||
|
{
|
||||||
|
private readonly IOnboardingElectronicSignService _service;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
|
public OnboardingElectronicSignCallbackController(
|
||||||
|
IOnboardingElectronicSignService service,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 接收契约锁回调并返回平台可识别的非统一响应。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("callback")]
|
||||||
|
[NonUnify]
|
||||||
|
public async Task<IActionResult> Callback()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = _httpContextAccessor.HttpContext?.Request
|
||||||
|
?? throw new InvalidOperationException("当前请求上下文不存在");
|
||||||
|
var payloadJson = await ReadPayload(request);
|
||||||
|
await _service.HandleCallback(payloadJson);
|
||||||
|
return new OkObjectResult(new
|
||||||
|
{
|
||||||
|
code = 0,
|
||||||
|
message = "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (global::System.Exception exception)
|
||||||
|
{
|
||||||
|
// 契约锁按 HTTP 状态码判断回调是否成功;返回 500 后平台会继续重试。
|
||||||
|
return new ObjectResult(new
|
||||||
|
{
|
||||||
|
code = -1,
|
||||||
|
message = exception.Message
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status500InternalServerError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁明文回调使用表单格式;同时兼容测试工具直接发送 JSON。
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<string> ReadPayload(HttpRequest request)
|
||||||
|
{
|
||||||
|
if (request.HasFormContentType)
|
||||||
|
{
|
||||||
|
var form = await request.ReadFormAsync();
|
||||||
|
var values = form
|
||||||
|
.OrderBy(item => item.Key, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(
|
||||||
|
item => item.Key,
|
||||||
|
item => item.Value.ToString());
|
||||||
|
return global::System.Text.Json.JsonSerializer.Serialize(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = new StreamReader(
|
||||||
|
request.Body,
|
||||||
|
Encoding.UTF8,
|
||||||
|
true,
|
||||||
|
leaveOpen: true);
|
||||||
|
return await reader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Web.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供入职任务电子签管理接口。
|
||||||
|
/// </summary>
|
||||||
|
[ApiDescriptionSettings("Application", Tag = "入职电子签约")]
|
||||||
|
[Route("biz/onboarding/electronic-sign")]
|
||||||
|
[RolePermission]
|
||||||
|
public class OnboardingElectronicSignController
|
||||||
|
{
|
||||||
|
private readonly IOnboardingElectronicSignService _service;
|
||||||
|
|
||||||
|
public OnboardingElectronicSignController(
|
||||||
|
IOnboardingElectronicSignService service)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询入职任务的电子签状态和合同文件。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("detail")]
|
||||||
|
[DisplayName("查看入职电子签详情")]
|
||||||
|
public async Task<OnboardingElectronicSignDetailOutput> Detail(
|
||||||
|
[FromQuery] BaseIdInput input)
|
||||||
|
{
|
||||||
|
return await _service.Detail(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动发起或继续上次失败的契约锁流程。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("initiate")]
|
||||||
|
[DisplayName("发起入职电子签")]
|
||||||
|
public async Task<OnboardingElectronicSignDetailOutput> Initiate(
|
||||||
|
[FromBody] OnboardingElectronicSignTaskInput input)
|
||||||
|
{
|
||||||
|
return await _service.Initiate(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保留原流程记录并创建一次全新的契约锁签约流程。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("reinitiate")]
|
||||||
|
[DisplayName("重新发起入职电子签")]
|
||||||
|
public async Task<OnboardingElectronicSignDetailOutput> Reinitiate(
|
||||||
|
[FromBody] OnboardingElectronicSignReinitiateInput input)
|
||||||
|
{
|
||||||
|
return await _service.Reinitiate(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主动同步契约锁状态,完成后自动归档。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("sync")]
|
||||||
|
[DisplayName("同步入职电子签状态")]
|
||||||
|
public async Task<OnboardingElectronicSignDetailOutput> Sync(
|
||||||
|
[FromBody] OnboardingElectronicSignTaskInput input)
|
||||||
|
{
|
||||||
|
return await _service.Sync(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取仍然有效的员工签署地址,过期则重新生成。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("get-link")]
|
||||||
|
[DisplayName("获取员工电子签链接")]
|
||||||
|
public async Task<OnboardingElectronicSignDetailOutput> GetSignLink(
|
||||||
|
[FromBody] OnboardingElectronicSignTaskInput input)
|
||||||
|
{
|
||||||
|
return await _service.GetSignLink(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 催办当前待签员工。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("notice")]
|
||||||
|
[DisplayName("催办员工电子签")]
|
||||||
|
public async Task Notice(
|
||||||
|
[FromBody] OnboardingElectronicSignTaskInput input)
|
||||||
|
{
|
||||||
|
await _service.Notice(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 作废尚未结束的电子签流程。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("invalid")]
|
||||||
|
[DisplayName("作废入职电子签")]
|
||||||
|
public async Task Invalid(
|
||||||
|
[FromBody] OnboardingElectronicSignInvalidInput input)
|
||||||
|
{
|
||||||
|
await _service.Invalid(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
-- OnboardX 契约锁电子签流程表
|
||||||
|
-- 适用数据库:MySQL 8.0+
|
||||||
|
-- 说明:
|
||||||
|
-- 1. 一条电子签流程可以包含同一入职任务下的多份合同。
|
||||||
|
-- 2. ProviderContractId、ProviderDocumentId 一律使用 VARCHAR,避免第三方 19 位 Id 在前端精度丢失。
|
||||||
|
-- 3. 回调事件通过 PayloadHash 幂等处理,可安全接收契约锁重复通知。
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ob_electronic_sign_process` (
|
||||||
|
`Id` BIGINT NOT NULL COMMENT '主键',
|
||||||
|
`TaskId` BIGINT NOT NULL COMMENT '入职任务Id',
|
||||||
|
`EmployeeId` BIGINT NOT NULL COMMENT '正式员工Id',
|
||||||
|
`Provider` VARCHAR(30) NOT NULL DEFAULT 'qiyuesuo' COMMENT '电子签服务商',
|
||||||
|
`BizId` VARCHAR(100) NOT NULL COMMENT '本系统幂等业务编号',
|
||||||
|
`IsCurrent` TINYINT(1) NULL DEFAULT 1 COMMENT '是否为任务当前电子签流程,历史流程为空',
|
||||||
|
`ProviderContractId` VARCHAR(100) NULL COMMENT '契约锁合同流程Id',
|
||||||
|
`CategoryId` VARCHAR(100) NOT NULL COMMENT '契约锁业务分类Id快照',
|
||||||
|
`SealId` VARCHAR(100) NOT NULL COMMENT '契约锁印章Id快照',
|
||||||
|
`Subject` VARCHAR(300) NOT NULL COMMENT '电子签流程标题',
|
||||||
|
`EmployeeName` VARCHAR(100) NOT NULL COMMENT '员工姓名快照',
|
||||||
|
`EmployeeMobile` VARCHAR(30) NOT NULL COMMENT '员工手机号快照',
|
||||||
|
`EmployeeIdCardNumber` VARCHAR(30) NULL COMMENT '员工身份证号码快照',
|
||||||
|
`CreatorName` VARCHAR(100) NOT NULL COMMENT '创建人姓名快照',
|
||||||
|
`CreatorMobile` VARCHAR(30) NOT NULL COMMENT '创建人手机号快照',
|
||||||
|
`SignStatus` VARCHAR(40) NOT NULL COMMENT '本系统电子签状态',
|
||||||
|
`ProviderStatus` VARCHAR(60) NULL COMMENT '契约锁原始合同状态',
|
||||||
|
`SignUrl` LONGTEXT NULL COMMENT '员工签署页面地址',
|
||||||
|
`SignUrlExpireAt` DATETIME(3) NULL COMMENT '签署页面地址到期时间',
|
||||||
|
`StartedAt` DATETIME(3) NOT NULL COMMENT '管理员发起时间',
|
||||||
|
`SentAt` DATETIME(3) NULL COMMENT '合同发送时间',
|
||||||
|
`CompletedAt` DATETIME(3) NULL COMMENT '全部签署完成时间',
|
||||||
|
`ArchivedAt` DATETIME(3) NULL COMMENT '归档完成时间',
|
||||||
|
`ArchiveFileId` BIGINT NULL COMMENT '完整归档ZIP文件Id',
|
||||||
|
`LastSyncedAt` DATETIME(3) NULL COMMENT '最后主动同步时间',
|
||||||
|
`LastCallbackAt` DATETIME(3) NULL COMMENT '最后回调时间',
|
||||||
|
`LastError` LONGTEXT NULL COMMENT '最近一次失败原因',
|
||||||
|
`RetryCount` INT NOT NULL DEFAULT 0 COMMENT '失败重试次数',
|
||||||
|
`ReissueReason` VARCHAR(500) NULL COMMENT '重新发起原因',
|
||||||
|
`ReissuedAt` DATETIME(3) NULL COMMENT '被新流程替代时间',
|
||||||
|
`ReissuedByProcessId` BIGINT NULL COMMENT '替代当前流程的新流程Id',
|
||||||
|
`ExtJson` LONGTEXT NULL COMMENT '扩展信息',
|
||||||
|
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '启用状态',
|
||||||
|
`CreateTime` DATETIME(3) NULL COMMENT '创建时间',
|
||||||
|
`UpdateTime` DATETIME(3) NULL COMMENT '更新时间',
|
||||||
|
`CreateUserId` BIGINT NULL COMMENT '创建者Id',
|
||||||
|
`UpdateUserId` BIGINT NULL COMMENT '修改者Id',
|
||||||
|
`CreateUser` VARCHAR(200) NULL COMMENT '创建人',
|
||||||
|
`UpdateUser` VARCHAR(200) NULL COMMENT '更新人',
|
||||||
|
`IsDelete` TINYINT(1) NULL DEFAULT 0 COMMENT '软删除',
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
UNIQUE KEY `uk_ob_esign_process_biz` (`BizId`),
|
||||||
|
UNIQUE KEY `uk_ob_esign_process_current` (`TaskId`, `IsCurrent`),
|
||||||
|
UNIQUE KEY `uk_ob_esign_process_provider_contract` (`ProviderContractId`),
|
||||||
|
KEY `idx_ob_esign_process_task` (`TaskId`, `StartedAt`),
|
||||||
|
KEY `idx_ob_esign_process_status` (`SignStatus`, `UpdateTime`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='入职电子签流程';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ob_electronic_sign_document` (
|
||||||
|
`Id` BIGINT NOT NULL COMMENT '主键',
|
||||||
|
`SignProcessId` BIGINT NOT NULL COMMENT '电子签流程Id',
|
||||||
|
`TaskContractId` BIGINT NOT NULL COMMENT '任务合同Id',
|
||||||
|
`ProviderDocumentId` VARCHAR(100) NOT NULL COMMENT '契约锁文档Id',
|
||||||
|
`DocumentName` VARCHAR(300) NOT NULL COMMENT '文档名称快照',
|
||||||
|
`DocumentSort` INT NOT NULL DEFAULT 1 COMMENT '文档顺序',
|
||||||
|
`OriginalDocxFileId` BIGINT NOT NULL COMMENT '原始DOCX文件Id',
|
||||||
|
`SignedPdfFileId` BIGINT NULL COMMENT '签署完成PDF文件Id',
|
||||||
|
`EmployeeSignKeyword` VARCHAR(200) NOT NULL COMMENT '员工签名关键字快照',
|
||||||
|
`CompanySealKeyword` VARCHAR(200) NOT NULL COMMENT '企业盖章关键字快照',
|
||||||
|
`EmployeeActionId` VARCHAR(100) NULL COMMENT '员工签署动作Id',
|
||||||
|
`CompanyActionId` VARCHAR(100) NULL COMMENT '企业盖章动作Id',
|
||||||
|
`ExtJson` LONGTEXT NULL COMMENT '扩展信息',
|
||||||
|
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '启用状态',
|
||||||
|
`CreateTime` DATETIME(3) NULL COMMENT '创建时间',
|
||||||
|
`UpdateTime` DATETIME(3) NULL COMMENT '更新时间',
|
||||||
|
`CreateUserId` BIGINT NULL COMMENT '创建者Id',
|
||||||
|
`UpdateUserId` BIGINT NULL COMMENT '修改者Id',
|
||||||
|
`CreateUser` VARCHAR(200) NULL COMMENT '创建人',
|
||||||
|
`UpdateUser` VARCHAR(200) NULL COMMENT '更新人',
|
||||||
|
`IsDelete` TINYINT(1) NULL DEFAULT 0 COMMENT '软删除',
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
UNIQUE KEY `uk_ob_esign_document_task_contract` (`SignProcessId`, `TaskContractId`),
|
||||||
|
UNIQUE KEY `uk_ob_esign_document_provider` (`ProviderDocumentId`),
|
||||||
|
KEY `idx_ob_esign_document_process_sort` (`SignProcessId`, `DocumentSort`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='入职电子签文档';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ob_electronic_sign_event` (
|
||||||
|
`Id` BIGINT NOT NULL COMMENT '主键',
|
||||||
|
`SignProcessId` BIGINT NULL COMMENT '电子签流程Id',
|
||||||
|
`PayloadHash` VARCHAR(64) NOT NULL COMMENT '回调内容SHA-256',
|
||||||
|
`EventSource` VARCHAR(30) NOT NULL COMMENT '事件来源',
|
||||||
|
`EventType` VARCHAR(100) NULL COMMENT '契约锁事件类型',
|
||||||
|
`ProviderStatus` VARCHAR(60) NULL COMMENT '契约锁状态',
|
||||||
|
`PayloadJson` LONGTEXT NULL COMMENT '事件原始内容',
|
||||||
|
`ProcessResult` VARCHAR(500) NULL COMMENT '处理结果',
|
||||||
|
`ReceivedAt` DATETIME(3) NOT NULL COMMENT '接收时间',
|
||||||
|
`ProcessedAt` DATETIME(3) NULL COMMENT '处理完成时间',
|
||||||
|
`ExtJson` LONGTEXT NULL COMMENT '扩展信息',
|
||||||
|
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '启用状态',
|
||||||
|
`CreateTime` DATETIME(3) NULL COMMENT '创建时间',
|
||||||
|
`UpdateTime` DATETIME(3) NULL COMMENT '更新时间',
|
||||||
|
`CreateUserId` BIGINT NULL COMMENT '创建者Id',
|
||||||
|
`UpdateUserId` BIGINT NULL COMMENT '修改者Id',
|
||||||
|
`CreateUser` VARCHAR(200) NULL COMMENT '创建人',
|
||||||
|
`UpdateUser` VARCHAR(200) NULL COMMENT '更新人',
|
||||||
|
`IsDelete` TINYINT(1) NULL DEFAULT 0 COMMENT '软删除',
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
UNIQUE KEY `uk_ob_esign_event_payload_hash` (`PayloadHash`),
|
||||||
|
KEY `idx_ob_esign_event_process` (`SignProcessId`, `ReceivedAt`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='入职电子签事件';
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- 增加入职电子签“作废后重新发起”能力。
|
||||||
|
-- 已执行 20260903_qiyuesuo_electronic_sign.sql 的数据库需要单独执行本文件。
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `ob_electronic_sign_process`
|
||||||
|
ADD COLUMN `IsCurrent` TINYINT(1) NULL DEFAULT NULL
|
||||||
|
COMMENT '是否为任务当前电子签流程,历史流程为空'
|
||||||
|
AFTER `BizId`;
|
||||||
|
|
||||||
|
ALTER TABLE `ob_electronic_sign_process`
|
||||||
|
ADD COLUMN `ReissueReason` VARCHAR(500) NULL
|
||||||
|
COMMENT '重新发起原因'
|
||||||
|
AFTER `RetryCount`,
|
||||||
|
ADD COLUMN `ReissuedAt` DATETIME(3) NULL
|
||||||
|
COMMENT '被新流程替代时间'
|
||||||
|
AFTER `ReissueReason`,
|
||||||
|
ADD COLUMN `ReissuedByProcessId` BIGINT NULL
|
||||||
|
COMMENT '替代当前流程的新流程Id'
|
||||||
|
AFTER `ReissuedAt`;
|
||||||
|
|
||||||
|
-- 雪花 Id 与创建时间同序,每个任务只把最新一条流程标记为当前流程。
|
||||||
|
UPDATE `ob_electronic_sign_process` process
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT `TaskId`, MAX(`Id`) AS `LatestProcessId`
|
||||||
|
FROM `ob_electronic_sign_process`
|
||||||
|
GROUP BY `TaskId`
|
||||||
|
) latest ON latest.`LatestProcessId` = process.`Id`
|
||||||
|
SET process.`IsCurrent` = 1;
|
||||||
|
|
||||||
|
ALTER TABLE `ob_electronic_sign_process`
|
||||||
|
ADD UNIQUE KEY `uk_ob_esign_process_current` (`TaskId`, `IsCurrent`);
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- 补充电子签署人实名快照。
|
||||||
|
-- 已执行 20260903_qiyuesuo_electronic_sign.sql 的数据库需要单独执行本文件。
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `ob_electronic_sign_process`
|
||||||
|
ADD COLUMN `EmployeeIdCardNumber` VARCHAR(30) NULL
|
||||||
|
COMMENT '员工身份证号码快照'
|
||||||
|
AFTER `EmployeeMobile`;
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OnboardX 基础业务假数据初始化脚本(MySQL 8.0+)
|
||||||
|
--
|
||||||
|
-- 使用:
|
||||||
|
-- 1. 先执行 20260828_reset_onboarding_business_data_v2.sql;
|
||||||
|
-- 2. 再执行本脚本;
|
||||||
|
-- 3. 发起任务时选择:
|
||||||
|
-- 重庆示例科技有限公司 / 技术部 / 后端开发工程师 /
|
||||||
|
-- 技术岗位标准入职模板(测试)。
|
||||||
|
--
|
||||||
|
-- 特点:
|
||||||
|
-- - 不删除任何数据;
|
||||||
|
-- - 不覆盖保留的 ob_variable_definition 和 ob_material_type;
|
||||||
|
-- - 只生成基础配置,不生成任务、员工或电子签约流程;
|
||||||
|
-- - 固定业务 Id + ON DUPLICATE KEY UPDATE,可重复执行。
|
||||||
|
--
|
||||||
|
-- 合同源文件默认自动选择 sys_file 中最近的 DOCX,并优先排除 OB20 开头的
|
||||||
|
-- 已生成任务合同。若自动选择不对,把 NULL 改为正确的源 DOCX 文件 Id。
|
||||||
|
-- 例如:SET @ob_seed_source_docx_file_id := 842701790859333;
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
USE `OnboardX`;
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET @ob_seed_source_docx_file_id := NULL;
|
||||||
|
|
||||||
|
SET @ob_seed_source_docx_file_id := COALESCE(
|
||||||
|
@ob_seed_source_docx_file_id,
|
||||||
|
(
|
||||||
|
SELECT file.`Id`
|
||||||
|
FROM `sys_file` file
|
||||||
|
WHERE LOWER(TRIM(LEADING '.' FROM COALESCE(file.`Suffix`, ''))) = 'docx'
|
||||||
|
AND COALESCE(file.`IsDelete`, 0) = 0
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN UPPER(COALESCE(file.`Name`, '')) LIKE 'OB20%' THEN 1 ELSE 0 END,
|
||||||
|
file.`CreateTime` DESC,
|
||||||
|
file.`Id` DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS `ob_assert_seed_prerequisites`;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE `ob_assert_seed_prerequisites`()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM `ob_variable_definition`
|
||||||
|
WHERE COALESCE(`IsDelete`, 0) = 0
|
||||||
|
AND LOWER(COALESCE(`Status`, 'enable')) = 'enable'
|
||||||
|
) THEN
|
||||||
|
SIGNAL SQLSTATE '45000'
|
||||||
|
SET MESSAGE_TEXT = '初始化失败:ob_variable_definition 没有启用数据';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM `ob_material_type`
|
||||||
|
WHERE COALESCE(`IsDelete`, 0) = 0
|
||||||
|
AND LOWER(COALESCE(`Status`, 'enable')) = 'enable'
|
||||||
|
) THEN
|
||||||
|
SIGNAL SQLSTATE '45000'
|
||||||
|
SET MESSAGE_TEXT = '初始化失败:ob_material_type 没有启用数据';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF @ob_seed_source_docx_file_id IS NULL OR NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_file`
|
||||||
|
WHERE `Id` = @ob_seed_source_docx_file_id
|
||||||
|
AND LOWER(TRIM(LEADING '.' FROM COALESCE(`Suffix`, ''))) = 'docx'
|
||||||
|
AND COALESCE(`IsDelete`, 0) = 0
|
||||||
|
) THEN
|
||||||
|
SIGNAL SQLSTATE '45000'
|
||||||
|
SET MESSAGE_TEXT = '初始化失败:没有可用的合同源 DOCX,请在脚本顶部指定文件 Id';
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL `ob_assert_seed_prerequisites`();
|
||||||
|
DROP PROCEDURE IF EXISTS `ob_assert_seed_prerequisites`;
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
-- 1. 公司
|
||||||
|
INSERT INTO `ob_company`
|
||||||
|
(`Id`, `CompanyCode`, `CompanyName`, `LegalName`, `UnifiedSocialCreditCode`,
|
||||||
|
`LegalRepresentative`, `RegisteredAddress`, `ContactPhone`, `Remark`,
|
||||||
|
`Status`, `CreateTime`, `UpdateTime`, `CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000001, 'DEMO-COMPANY-001', '重庆示例科技有限公司', '重庆示例科技有限公司',
|
||||||
|
'91500000MA5UDEMO01', '张示例', '重庆市渝北区示例大道 100 号', '023-88886666',
|
||||||
|
'测试公司,可在后台修改', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`CompanyCode` = VALUES(`CompanyCode`),
|
||||||
|
`CompanyName` = VALUES(`CompanyName`),
|
||||||
|
`LegalName` = VALUES(`LegalName`),
|
||||||
|
`UnifiedSocialCreditCode` = VALUES(`UnifiedSocialCreditCode`),
|
||||||
|
`LegalRepresentative` = VALUES(`LegalRepresentative`),
|
||||||
|
`RegisteredAddress` = VALUES(`RegisteredAddress`),
|
||||||
|
`ContactPhone` = VALUES(`ContactPhone`),
|
||||||
|
`Remark` = VALUES(`Remark`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
-- 2. 部门
|
||||||
|
INSERT INTO `ob_department`
|
||||||
|
(`Id`, `CompanyId`, `DepartmentCode`, `DepartmentName`, `SortCode`, `Remark`,
|
||||||
|
`Status`, `CreateTime`, `UpdateTime`, `CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000101, 8800000000000001, 'DEMO-DEPT-HR', '人事部', 10,
|
||||||
|
'负责招聘、入职和员工关系', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000102, 8800000000000001, 'DEMO-DEPT-TECH', '技术部', 20,
|
||||||
|
'负责产品研发和质量保障', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000103, 8800000000000001, 'DEMO-DEPT-OPERATION', '运营部', 30,
|
||||||
|
'负责产品运营和客户服务', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`CompanyId` = VALUES(`CompanyId`),
|
||||||
|
`DepartmentCode` = VALUES(`DepartmentCode`),
|
||||||
|
`DepartmentName` = VALUES(`DepartmentName`),
|
||||||
|
`SortCode` = VALUES(`SortCode`),
|
||||||
|
`Remark` = VALUES(`Remark`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
-- 3. 岗位及默认人事合同参数
|
||||||
|
INSERT INTO `ob_position`
|
||||||
|
(`Id`, `CompanyId`, `DepartmentId`, `PositionCode`, `PositionName`,
|
||||||
|
`DefaultContractYears`, `DefaultProbationMonths`, `DefaultParamsJson`, `Remark`,
|
||||||
|
`Status`, `CreateTime`, `UpdateTime`, `CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000201, 8800000000000001, 8800000000000101,
|
||||||
|
'DEMO-HR-SPECIALIST', '人事专员', 3, 3,
|
||||||
|
JSON_OBJECT('hr.BaseSalary', '4500', 'hr.PositionSalary', '1000',
|
||||||
|
'hr.PerformanceAllowance', '500', 'hr.AttendanceBonus', '200',
|
||||||
|
'hr.RegularSalary', '6200', 'hr.WorkCity', '重庆',
|
||||||
|
'hr.PositionName', '人事专员', 'hr.ContractTerm', '3', 'hr.ProbationTerm', '3'),
|
||||||
|
'测试岗位', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000202, 8800000000000001, 8800000000000102,
|
||||||
|
'DEMO-BACKEND-ENGINEER', '后端开发工程师', 3, 3,
|
||||||
|
JSON_OBJECT('hr.BaseSalary', '6500', 'hr.PositionSalary', '2500',
|
||||||
|
'hr.PerformanceAllowance', '1000', 'hr.AttendanceBonus', '200',
|
||||||
|
'hr.RegularSalary', '10200', 'hr.WorkCity', '重庆',
|
||||||
|
'hr.PositionName', '后端开发工程师', 'hr.ContractTerm', '3', 'hr.ProbationTerm', '3'),
|
||||||
|
'完整流程推荐使用此岗位', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000203, 8800000000000001, 8800000000000102,
|
||||||
|
'DEMO-FRONTEND-ENGINEER', '前端开发工程师', 3, 3,
|
||||||
|
JSON_OBJECT('hr.BaseSalary', '6200', 'hr.PositionSalary', '2300',
|
||||||
|
'hr.PerformanceAllowance', '1000', 'hr.AttendanceBonus', '200',
|
||||||
|
'hr.RegularSalary', '9700', 'hr.WorkCity', '重庆',
|
||||||
|
'hr.PositionName', '前端开发工程师', 'hr.ContractTerm', '3', 'hr.ProbationTerm', '3'),
|
||||||
|
'测试岗位', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000204, 8800000000000001, 8800000000000102,
|
||||||
|
'DEMO-QA-ENGINEER', '测试工程师', 3, 3,
|
||||||
|
JSON_OBJECT('hr.BaseSalary', '5500', 'hr.PositionSalary', '1800',
|
||||||
|
'hr.PerformanceAllowance', '800', 'hr.AttendanceBonus', '200',
|
||||||
|
'hr.RegularSalary', '8300', 'hr.WorkCity', '重庆',
|
||||||
|
'hr.PositionName', '测试工程师', 'hr.ContractTerm', '3', 'hr.ProbationTerm', '3'),
|
||||||
|
'测试岗位', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000205, 8800000000000001, 8800000000000103,
|
||||||
|
'DEMO-OPERATION-SPECIALIST', '运营专员', 3, 3,
|
||||||
|
JSON_OBJECT('hr.BaseSalary', '4500', 'hr.PositionSalary', '1200',
|
||||||
|
'hr.PerformanceAllowance', '800', 'hr.AttendanceBonus', '200',
|
||||||
|
'hr.RegularSalary', '6700', 'hr.WorkCity', '重庆',
|
||||||
|
'hr.PositionName', '运营专员', 'hr.ContractTerm', '3', 'hr.ProbationTerm', '3'),
|
||||||
|
'测试岗位', 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`CompanyId` = VALUES(`CompanyId`),
|
||||||
|
`DepartmentId` = VALUES(`DepartmentId`),
|
||||||
|
`PositionCode` = VALUES(`PositionCode`),
|
||||||
|
`PositionName` = VALUES(`PositionName`),
|
||||||
|
`DefaultContractYears` = VALUES(`DefaultContractYears`),
|
||||||
|
`DefaultProbationMonths` = VALUES(`DefaultProbationMonths`),
|
||||||
|
`DefaultParamsJson` = VALUES(`DefaultParamsJson`),
|
||||||
|
`Remark` = VALUES(`Remark`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
-- 4. 技术部合同模板及适用岗位
|
||||||
|
INSERT INTO `ob_contract_template`
|
||||||
|
(`Id`, `CompanyId`, `DepartmentId`, `TemplateCode`, `TemplateName`, `ContractType`,
|
||||||
|
`Description`, `Status`, `CreateTime`, `UpdateTime`, `CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000301, 8800000000000001, 8800000000000102,
|
||||||
|
'DEMO-LABOR-CONTRACT-001', '技术岗位劳动合同(测试)', '劳动合同',
|
||||||
|
'测试合同模板,源 DOCX 由脚本顶部变量决定',
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`CompanyId` = VALUES(`CompanyId`),
|
||||||
|
`DepartmentId` = VALUES(`DepartmentId`),
|
||||||
|
`TemplateCode` = VALUES(`TemplateCode`),
|
||||||
|
`TemplateName` = VALUES(`TemplateName`),
|
||||||
|
`ContractType` = VALUES(`ContractType`),
|
||||||
|
`Description` = VALUES(`Description`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
INSERT INTO `ob_contract_template_position`
|
||||||
|
(`Id`, `ContractTemplateId`, `PositionId`, `Status`, `CreateTime`, `UpdateTime`,
|
||||||
|
`CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000401, 8800000000000301, 8800000000000202,
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000402, 8800000000000301, 8800000000000203,
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000403, 8800000000000301, 8800000000000204,
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`ContractTemplateId` = VALUES(`ContractTemplateId`),
|
||||||
|
`PositionId` = VALUES(`PositionId`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
-- 5. 已启用合同版本
|
||||||
|
INSERT INTO `ob_contract_template_version`
|
||||||
|
(`Id`, `TemplateId`, `VersionNo`, `SourceDocxFileId`, `FileHash`,
|
||||||
|
`EffectiveDate`, `ExpiryDate`, `VersionStatus`, `Remark`,
|
||||||
|
`Status`, `CreateTime`, `UpdateTime`, `CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000311, 8800000000000301, '1.0', @ob_seed_source_docx_file_id,
|
||||||
|
CONCAT('seed-file-', @ob_seed_source_docx_file_id),
|
||||||
|
CURDATE(), DATE_ADD(CURDATE(), INTERVAL 10 YEAR), 'enabled', '测试合同版本',
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`TemplateId` = VALUES(`TemplateId`),
|
||||||
|
`VersionNo` = VALUES(`VersionNo`),
|
||||||
|
`SourceDocxFileId` = VALUES(`SourceDocxFileId`),
|
||||||
|
`FileHash` = VALUES(`FileHash`),
|
||||||
|
`EffectiveDate` = VALUES(`EffectiveDate`),
|
||||||
|
`ExpiryDate` = VALUES(`ExpiryDate`),
|
||||||
|
`VersionStatus` = 'enabled',
|
||||||
|
`Remark` = VALUES(`Remark`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
-- SQL 不能解压 DOCX 识别占位符,因此复制当前启用的四类变量字典。
|
||||||
|
-- DOCX 未使用的变量不会改变文件;这样能避免实际占位符初始化时遗漏。
|
||||||
|
INSERT INTO `ob_template_variable`
|
||||||
|
(`Id`, `TemplateVersionId`, `VariableCode`, `VariableName`, `VariableType`, `DataType`,
|
||||||
|
`EmployeeField`, `DefaultValue`, `Required`, `HrEditable`, `SortCode`, `Remark`,
|
||||||
|
`Status`, `CreateTime`, `UpdateTime`, `CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
SELECT
|
||||||
|
8000000000000000 + definition.`Id`,
|
||||||
|
8800000000000311,
|
||||||
|
definition.`VariableCode`,
|
||||||
|
definition.`VariableName`,
|
||||||
|
CASE LOWER(definition.`VariableCategory`)
|
||||||
|
WHEN 'employee' THEN 'employee'
|
||||||
|
WHEN 'hr' THEN 'contract'
|
||||||
|
WHEN 'company' THEN 'company'
|
||||||
|
WHEN 'system' THEN 'system'
|
||||||
|
END,
|
||||||
|
COALESCE(NULLIF(definition.`DataType`, ''), 'string'),
|
||||||
|
CASE
|
||||||
|
WHEN LOWER(definition.`VariableCategory`) IN ('employee', 'company')
|
||||||
|
THEN SUBSTRING_INDEX(definition.`VariableCode`, '.', -1)
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
definition.`DefaultValue`,
|
||||||
|
COALESCE(definition.`Required`, 0),
|
||||||
|
CASE WHEN LOWER(definition.`VariableCategory`) = 'hr' THEN 1 ELSE 0 END,
|
||||||
|
COALESCE(definition.`SortCode`, 100),
|
||||||
|
definition.`Remark`,
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0
|
||||||
|
FROM `ob_variable_definition` definition
|
||||||
|
WHERE COALESCE(definition.`IsDelete`, 0) = 0
|
||||||
|
AND LOWER(COALESCE(definition.`Status`, 'enable')) = 'enable'
|
||||||
|
AND LOWER(definition.`VariableCategory`) IN ('employee', 'hr', 'company', 'system')
|
||||||
|
AND definition.`Id` BETWEEN 1 AND 1007199254740991
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`VariableName` = VALUES(`VariableName`),
|
||||||
|
`VariableType` = VALUES(`VariableType`),
|
||||||
|
`DataType` = VALUES(`DataType`),
|
||||||
|
`EmployeeField` = VALUES(`EmployeeField`),
|
||||||
|
`DefaultValue` = VALUES(`DefaultValue`),
|
||||||
|
`Required` = VALUES(`Required`),
|
||||||
|
`HrEditable` = VALUES(`HrEditable`),
|
||||||
|
`SortCode` = VALUES(`SortCode`),
|
||||||
|
`Remark` = VALUES(`Remark`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
-- 6. 可直接发起任务的入职模板
|
||||||
|
INSERT INTO `ob_onboarding_template`
|
||||||
|
(`Id`, `TemplateCode`, `TemplateName`, `CompanyId`, `DepartmentId`, `Description`,
|
||||||
|
`Status`, `CreateTime`, `UpdateTime`, `CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000501, 'DEMO-ONBOARDING-TECH', '技术岗位标准入职模板(测试)',
|
||||||
|
8800000000000001, 8800000000000102,
|
||||||
|
'适用于后端、前端和测试工程师,可用于完整入职流程测试',
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`TemplateCode` = VALUES(`TemplateCode`),
|
||||||
|
`TemplateName` = VALUES(`TemplateName`),
|
||||||
|
`CompanyId` = VALUES(`CompanyId`),
|
||||||
|
`DepartmentId` = VALUES(`DepartmentId`),
|
||||||
|
`Description` = VALUES(`Description`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
INSERT INTO `ob_onboarding_template_position`
|
||||||
|
(`Id`, `OnboardingTemplateId`, `PositionId`, `Status`, `CreateTime`, `UpdateTime`,
|
||||||
|
`CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000601, 8800000000000501, 8800000000000202,
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000602, 8800000000000501, 8800000000000203,
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0),
|
||||||
|
(8800000000000603, 8800000000000501, 8800000000000204,
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`OnboardingTemplateId` = VALUES(`OnboardingTemplateId`),
|
||||||
|
`PositionId` = VALUES(`PositionId`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
INSERT INTO `ob_onboarding_template_contract`
|
||||||
|
(`Id`, `OnboardingTemplateId`, `ContractTemplateId`, `ContractTemplateVersionId`,
|
||||||
|
`Required`, `PrintCopies`, `SortCode`, `Status`, `CreateTime`, `UpdateTime`,
|
||||||
|
`CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
VALUES
|
||||||
|
(8800000000000701, 8800000000000501, 8800000000000301, 8800000000000311,
|
||||||
|
1, 1, 10, 'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`OnboardingTemplateId` = VALUES(`OnboardingTemplateId`),
|
||||||
|
`ContractTemplateId` = VALUES(`ContractTemplateId`),
|
||||||
|
`ContractTemplateVersionId` = VALUES(`ContractTemplateVersionId`),
|
||||||
|
`Required` = 1,
|
||||||
|
`PrintCopies` = 1,
|
||||||
|
`SortCode` = 10,
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
-- 所有启用资料类型都会显示;一寸白底证件照必传,其余选传。
|
||||||
|
INSERT INTO `ob_onboarding_template_material`
|
||||||
|
(`Id`, `OnboardingTemplateId`, `MaterialTypeId`, `Required`, `SortCode`,
|
||||||
|
`Status`, `CreateTime`, `UpdateTime`, `CreateUser`, `UpdateUser`, `IsDelete`)
|
||||||
|
SELECT
|
||||||
|
7000000000000000 + material.`Id`,
|
||||||
|
8800000000000501,
|
||||||
|
material.`Id`,
|
||||||
|
CASE
|
||||||
|
WHEN material.`MaterialName` LIKE '%一寸白底%'
|
||||||
|
OR LOWER(material.`MaterialCode`) IN ('photo', 'id_photo', 'head_photo') THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY material.`Id`) * 10,
|
||||||
|
'ENABLE', NOW(3), NOW(3), 'seed-script', 'seed-script', 0
|
||||||
|
FROM `ob_material_type` material
|
||||||
|
WHERE COALESCE(material.`IsDelete`, 0) = 0
|
||||||
|
AND LOWER(COALESCE(material.`Status`, 'enable')) = 'enable'
|
||||||
|
AND material.`Id` BETWEEN 1 AND 2007199254740991
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`Required` = VALUES(`Required`),
|
||||||
|
`SortCode` = VALUES(`SortCode`),
|
||||||
|
`Status` = 'ENABLE',
|
||||||
|
`UpdateTime` = NOW(3),
|
||||||
|
`UpdateUser` = 'seed-script',
|
||||||
|
`IsDelete` = 0;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- 执行结果:先核对自动选中的源 DOCX,再核对初始化数量。
|
||||||
|
SELECT
|
||||||
|
'SUCCESS' AS `Result`,
|
||||||
|
@ob_seed_source_docx_file_id AS `SourceDocxFileId`,
|
||||||
|
(SELECT `Name` FROM `sys_file` WHERE `Id` = @ob_seed_source_docx_file_id LIMIT 1) AS `SourceDocxFileName`,
|
||||||
|
8800000000000001 AS `CompanyId`,
|
||||||
|
8800000000000102 AS `DepartmentId`,
|
||||||
|
8800000000000202 AS `RecommendedPositionId`,
|
||||||
|
8800000000000501 AS `OnboardingTemplateId`;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM `ob_company`
|
||||||
|
WHERE `Id` = 8800000000000001 AND COALESCE(`IsDelete`, 0) = 0) AS `CompanyCount`,
|
||||||
|
(SELECT COUNT(*) FROM `ob_department`
|
||||||
|
WHERE `CompanyId` = 8800000000000001 AND COALESCE(`IsDelete`, 0) = 0) AS `DepartmentCount`,
|
||||||
|
(SELECT COUNT(*) FROM `ob_position`
|
||||||
|
WHERE `CompanyId` = 8800000000000001 AND COALESCE(`IsDelete`, 0) = 0) AS `PositionCount`,
|
||||||
|
(SELECT COUNT(*) FROM `ob_template_variable`
|
||||||
|
WHERE `TemplateVersionId` = 8800000000000311 AND COALESCE(`IsDelete`, 0) = 0) AS `ContractVariableCount`,
|
||||||
|
(SELECT COUNT(*) FROM `ob_onboarding_template_material`
|
||||||
|
WHERE `OnboardingTemplateId` = 8800000000000501 AND COALESCE(`IsDelete`, 0) = 0) AS `MaterialCount`;
|
||||||
Reference in New Issue
Block a user