Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0803d9b295 | ||
|
|
83854ce901 | ||
|
|
0bb8263431 | ||
|
|
14cfd4146e | ||
|
|
3eae5f8e5f | ||
|
|
ff7e9c1496 | ||
|
|
c42d82d100 | ||
|
|
fa48e73b84 | ||
|
|
ab0248d73d | ||
|
|
bd20ceac4e | ||
|
|
aaec170c4f | ||
|
|
9de05b44b8 | ||
|
|
e9894538e8 | ||
|
|
20ea1bc70c | ||
|
|
43a498162e | ||
|
|
f004aec10f | ||
|
|
20409635d6 | ||
|
|
1d925bd10b | ||
|
|
378bf7fe51 | ||
|
|
571db6fc55 | ||
|
|
f858b04701 | ||
|
|
1fd557a19e | ||
|
|
95b14b198a | ||
|
|
c015bc356c | ||
|
|
da777327f5 | ||
|
|
384eaff444 | ||
|
|
6151872b6b | ||
|
|
1d42c52d53 | ||
|
|
b5ae0a44c7 | ||
|
|
c925c8ba32 | ||
|
|
f5b40e5659 | ||
|
|
58804be611 | ||
|
|
80940c60a5 | ||
|
|
ae8167a566 | ||
|
|
658869b3a7 | ||
|
|
9741481a62 | ||
|
|
db3d58bc99 | ||
|
|
9ae5e0bc54 | ||
|
|
be20a31332 | ||
|
|
38801533e9 | ||
|
|
085bf5e2ab | ||
|
|
4679bd6c13 | ||
|
|
0931e1f343 | ||
|
|
22a7df5bec | ||
|
|
b8b2d7e3fa |
@@ -8,6 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DocumentationFile>$(MSBuildProjectName).xml</DocumentationFile>
|
<!-- 由 SDK 按目标框架输出 XML,避免 net6/net7/net8 并行编译时共同写入项目根目录的同名文件。 -->
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -6,8 +6,30 @@
|
|||||||
"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": "",
|
||||||
|
"CreatorName": "袁林",
|
||||||
|
"CreatorMobile": "15123135384",
|
||||||
|
"CallbackUrl": "https://undiffusively-hemielytral-myrtle.ngrok-free.dev/api/open/onboarding/qiyuesuo/callback",
|
||||||
|
"SignPageCallbackUrl": "",
|
||||||
|
"SignUrlExpireMinutes": 10,
|
||||||
|
"EmployeeSignKeyword": "乙方(劳动者)签字:",
|
||||||
|
"EmployeeSignKeywordIndex": 1,
|
||||||
|
"CompanySealKeyword": "甲方(盖章):",
|
||||||
|
"CompanySealKeywordIndex": 1,
|
||||||
|
"CompanySealOffsetX": 0,
|
||||||
|
"CompanySealOffsetY": -0.05,
|
||||||
|
"EnableAcrossPageSeal": true,
|
||||||
|
"AcrossPageSealType": "ALL_DOCUMENT",
|
||||||
|
"TimeoutSeconds": 90
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,28 @@
|
|||||||
//系统层设置
|
//系统层设置
|
||||||
"ApplicationSettings": {
|
"ApplicationSettings": {
|
||||||
"InitTable": false, //是否初始化表结构
|
"InitTable": false, //是否初始化表结构
|
||||||
"InitSeedData": false, //是否初始化种子数据0
|
"InitSeedData": false //是否初始化种子数据
|
||||||
}
|
},
|
||||||
|
// 契约锁正式环境配置。上线前填写正式 AppToken/AppSecret,并把 ServerUrl 切换为 .com。
|
||||||
|
"QiyuesuoSetting": {
|
||||||
|
"Enabled": true,
|
||||||
|
"ServerUrl": "https://openapi.qiyuesuo.com",
|
||||||
|
"AppToken": "mOCjDn0gMR",
|
||||||
|
"AppSecret": "5lPmSSRd7n8IhO3BvtWZJkBP1QNC59",
|
||||||
|
"SignatureVersion": "",
|
||||||
|
"CreatorName": "李珍",
|
||||||
|
"CreatorMobile": "13629784055",
|
||||||
|
"CallbackUrl": "https://onboardx-api.cnzhyx.cn/api/open/onboarding/qiyuesuo/callback",
|
||||||
|
"SignPageCallbackUrl": "",
|
||||||
|
"SignUrlExpireMinutes": 10,
|
||||||
|
"EmployeeSignKeyword": "乙方(劳动者)签字:",
|
||||||
|
"EmployeeSignKeywordIndex": 1,
|
||||||
|
"CompanySealKeyword": "甲方(盖章):",
|
||||||
|
"CompanySealKeywordIndex": 1,
|
||||||
|
"CompanySealOffsetX": 0,
|
||||||
|
"CompanySealOffsetY": -0.05,
|
||||||
|
"EnableAcrossPageSeal": true,
|
||||||
|
"AcrossPageSealType": "ALL_DOCUMENT",
|
||||||
|
"TimeoutSeconds": 90
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,16 +10,60 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务公司档案,保存合同甲方及入职归属公司信息。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_company", TableDescription = "入职业务公司")]
|
[SugarTable("ob_company", TableDescription = "入职业务公司")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObCompany : BaseEntity
|
public class ObCompany : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 公司业务编码。
|
||||||
|
/// </summary>
|
||||||
public virtual string CompanyCode { get; set; }
|
public virtual string CompanyCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公司显示名称。
|
||||||
|
/// </summary>
|
||||||
public virtual string CompanyName { get; set; }
|
public virtual string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同中使用的甲方完整法定名称。
|
||||||
|
/// </summary>
|
||||||
public virtual string LegalName { get; set; }
|
public virtual string LegalName { get; set; }
|
||||||
public string UnifiedSocialCreditCode { get; set; }
|
|
||||||
public string LegalRepresentative { get; set; }
|
/// <summary>
|
||||||
public string RegisteredAddress { get; set; }
|
/// 统一社会信用代码。
|
||||||
public string ContactPhone { get; set; }
|
/// </summary>
|
||||||
public string Remark { get; set; }
|
public virtual string UnifiedSocialCreditCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 法定代表人姓名。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string LegalRepresentative { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工商注册地址。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string RegisteredAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公司联系电话。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string ContactPhone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁默认业务分类 Id;每个签约公司独立配置。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string QiyuesuoCategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁企业印章 Id;每个签约公司独立配置。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string QiyuesuoSealId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,40 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同适用范围,将已启用的合同版本绑定到公司或具体岗位。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_contract_scope", TableDescription = "公司岗位合同适用范围")]
|
[SugarTable("ob_contract_scope", TableDescription = "公司岗位合同适用范围")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObContractScope : BaseEntity
|
public class ObContractScope : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
public long CompanyId { get; set; }
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属岗位 Id;为空时表示适用于公司下全部岗位。
|
||||||
|
/// </summary>
|
||||||
public long? PositionId { get; set; }
|
public long? PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板主档案 Id。
|
||||||
|
/// </summary>
|
||||||
public long TemplateId { get; set; }
|
public long TemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板版本 Id。
|
||||||
|
/// </summary>
|
||||||
public long TemplateVersionId { get; set; }
|
public long TemplateVersionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该适用范围下的合同是否必签。
|
||||||
|
/// </summary>
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示排序码。
|
||||||
|
/// </summary>
|
||||||
public int? SortCode { get; set; }
|
public int? SortCode { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,66 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板主档案,用于区分不同合同类型及模板用途。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_contract_template", TableDescription = "合同模板主档案")]
|
[SugarTable("ob_contract_template", TableDescription = "合同模板主档案")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObContractTemplate : BaseEntity
|
public class ObContractTemplate : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public long? CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板所属入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板业务编码。
|
||||||
|
/// </summary>
|
||||||
public string TemplateCode { get; set; }
|
public string TemplateCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板名称。
|
||||||
|
/// </summary>
|
||||||
public string TemplateName { get; set; }
|
public string TemplateName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同类型。
|
||||||
|
/// </summary>
|
||||||
public string ContractType { get; set; }
|
public string ContractType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板用途说明。
|
||||||
|
/// </summary>
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司名称,仅用于接口展示。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门名称,仅用于接口展示。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string DepartmentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位 Id 列表,仅用于接口展示和编辑回显。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public List<long> PositionIds { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位名称列表,仅用于接口展示。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public List<string> PositionNames { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板适用岗位关系,用于限制合同模板可被哪些岗位的入职模板选择。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_contract_template_position", TableDescription = "合同模板适用岗位")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObContractTemplatePosition : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板主档案 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long ContractTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long PositionId { get; set; }
|
||||||
|
}
|
||||||
@@ -10,16 +10,56 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板版本,保存不可变的 DOCX 源文件及版本有效期。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_contract_template_version", TableDescription = "合同模板版本")]
|
[SugarTable("ob_contract_template_version", TableDescription = "合同模板版本")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObContractTemplateVersion : BaseEntity
|
public class ObContractTemplateVersion : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板主档案 Id。
|
||||||
|
/// </summary>
|
||||||
public long TemplateId { get; set; }
|
public long TemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板版本号。
|
||||||
|
/// </summary>
|
||||||
public string VersionNo { get; set; }
|
public string VersionNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板 DOCX 源文件 Id。
|
||||||
|
/// </summary>
|
||||||
public long SourceDocxFileId { get; set; }
|
public long SourceDocxFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 源文件内容哈希,用于校验文件是否变化。
|
||||||
|
/// </summary>
|
||||||
public string FileHash { get; set; }
|
public string FileHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 版本生效日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? EffectiveDate { get; set; }
|
public DateTime? EffectiveDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 版本失效日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ExpiryDate { get; set; }
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板版本状态。
|
||||||
|
/// </summary>
|
||||||
public string VersionStatus { get; set; } = ContractTemplateVersionStatus.Draft;
|
public string VersionStatus { get; set; } = ContractTemplateVersionStatus.Draft;
|
||||||
|
|
||||||
|
/// <summary>本版本是否需要公司盖章;历史空值沿用需要盖章。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本版本是否需要骑缝章;历史空值沿用系统骑缝章配置。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
public string Remark { get; set; }
|
public string Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务部门档案,属于某家入职公司。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_department", TableDescription = "入职业务部门")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObDepartment : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 部门业务编码。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string DepartmentCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 部门名称。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string DepartmentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序码。
|
||||||
|
/// </summary>
|
||||||
|
public int SortCode { get; set; } = 99;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司名称 (展示用)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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>发起本次签约时冻结的公司盖章要求。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>发起本次签约时冻结的骑缝章要求。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { 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; }
|
||||||
|
}
|
||||||
@@ -10,37 +10,188 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
[SugarTable("ob_employee", TableDescription = "正式员工档案")]
|
/// <summary>
|
||||||
|
/// 审核通过后生成的员工档案,是入职流程的最终业务数据。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_employee", TableDescription = "员工档案")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObEmployee : BaseEntity
|
public class ObEmployee : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 员工编号。
|
||||||
|
/// </summary>
|
||||||
public string EmployeeNo { get; set; }
|
public string EmployeeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 内部人员编号。
|
||||||
|
/// </summary>
|
||||||
public string InternalNo { get; set; }
|
public string InternalNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工姓名。
|
||||||
|
/// </summary>
|
||||||
public string EmployeeName { get; set; }
|
public string EmployeeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
public long CompanyId { get; set; }
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司名称,仅用于接口展示,不映射数据库字段。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门名称。
|
||||||
|
/// </summary>
|
||||||
public string DepartmentName { get; set; }
|
public string DepartmentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务岗位 Id。
|
||||||
|
/// </summary>
|
||||||
public long PositionId { get; set; }
|
public long PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用工类型:full_time 正式用工,part_time 兼职用工。
|
||||||
|
/// </summary>
|
||||||
|
public string EmploymentType { get; set; } = EmployeeEmploymentType.FullTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转正状态:probation 试用期、regular 已转正、not_applicable 不适用。
|
||||||
|
/// </summary>
|
||||||
|
public string RegularizationStatus { get; set; } = EmployeeRegularizationStatus.Probation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 社保状态:unknown 未维护、not_purchased 未购买、purchased 已购买、stopped 已停保、not_applicable 不适用。
|
||||||
|
/// </summary>
|
||||||
|
public string SocialInsuranceStatus { get; set; } = EmployeeSocialInsuranceStatus.Unknown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属岗位名称,仅用于接口展示,不映射数据库字段。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string PositionName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 性别。
|
||||||
|
/// </summary>
|
||||||
public string Gender { get; set; }
|
public string Gender { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号码。
|
||||||
|
/// </summary>
|
||||||
public string Phone { get; set; }
|
public string Phone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 身份证号码。
|
||||||
|
/// </summary>
|
||||||
public string IdCardNumber { get; set; }
|
public string IdCardNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 毕业院校。
|
||||||
|
/// </summary>
|
||||||
public string College { get; set; }
|
public string College { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最高学历。
|
||||||
|
/// </summary>
|
||||||
public string Education { get; set; }
|
public string Education { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所学专业。
|
||||||
|
/// </summary>
|
||||||
public string Major { get; set; }
|
public string Major { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 出生日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? BirthDate { get; set; }
|
public DateTime? BirthDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 户籍所在地。
|
||||||
|
/// </summary>
|
||||||
public string RegisteredResidence { get; set; }
|
public string RegisteredResidence { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 民族。
|
||||||
|
/// </summary>
|
||||||
public string Nation { get; set; }
|
public string Nation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 现居住或联系地址。
|
||||||
|
/// </summary>
|
||||||
public string ContactAddress { get; set; }
|
public string ContactAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? EntryDate { get; set; }
|
public DateTime? EntryDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同开始日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ContractStartDate { get; set; }
|
public DateTime? ContractStartDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同结束日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ContractEndDate { get; set; }
|
public DateTime? ContractEndDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 试用期开始日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ProbationStartDate { get; set; }
|
public DateTime? ProbationStartDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 试用期结束日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ProbationEndDate { get; set; }
|
public DateTime? ProbationEndDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转正日期。
|
||||||
|
/// </summary>
|
||||||
public DateTime? RegularizationDate { get; set; }
|
public DateTime? RegularizationDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 社保缴纳公司。
|
||||||
|
/// </summary>
|
||||||
public string SocialInsuranceCompany { get; set; }
|
public string SocialInsuranceCompany { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同性质。
|
||||||
|
/// </summary>
|
||||||
public string ContractNature { get; set; }
|
public string ContractNature { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工资银行卡号。
|
||||||
|
/// </summary>
|
||||||
public string BankCardNumber { get; set; }
|
public string BankCardNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 银行卡开户名。
|
||||||
|
/// </summary>
|
||||||
public string BankAccountName { get; set; }
|
public string BankAccountName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开户银行名称。
|
||||||
|
/// </summary>
|
||||||
public string BankName { get; set; }
|
public string BankName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
public string Remark { get; set; }
|
public string Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过时冻结的动态字段信息 JSON。
|
||||||
|
/// </summary>
|
||||||
public string ExtraInfoJson { get; set; }
|
public string ExtraInfoJson { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工人事材料附件,可关联转正记录及后续扩展的人事业务。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_employee_attachment", TableDescription = "员工人事材料附件")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObEmployeeAttachment : BaseEntity
|
||||||
|
{
|
||||||
|
public long EmployeeId { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public long? ChangeRecordId { get; set; }
|
||||||
|
|
||||||
|
public string BusinessType { get; set; }
|
||||||
|
|
||||||
|
public long FileId { get; set; }
|
||||||
|
|
||||||
|
public string FileName { get; set; }
|
||||||
|
|
||||||
|
public DateTime UploadedAt { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string FileSuffix { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string FileSizeInfo { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工人事信息变更记录,保留转正及后续扩展业务的前后快照。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_employee_change_record", TableDescription = "员工人事信息变更记录")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObEmployeeChangeRecord : BaseEntity
|
||||||
|
{
|
||||||
|
public long EmployeeId { get; set; }
|
||||||
|
|
||||||
|
public string ChangeType { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public DateTime? EffectiveDate { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string BeforeJson { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string AfterJson { get; set; }
|
||||||
|
|
||||||
|
public string Remark { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public List<ObEmployeeAttachment> Attachments { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正式员工的一份合同在审核建档时冻结的业务快照。
|
||||||
|
/// 常用日期和薪资使用独立列,全部标准及自定义变量同时保存在 VariablesJson 中。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_employee_contract_snapshot", TableDescription = "员工合同快照")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObEmployeeContractSnapshot : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>正式员工 Id。</summary>
|
||||||
|
public long EmployeeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>来源入职任务 Id。</summary>
|
||||||
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>来源任务合同 Id,同一任务合同只允许生成一份员工快照。</summary>
|
||||||
|
public long TaskContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同模板 Id。</summary>
|
||||||
|
public long TemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同模板版本 Id。</summary>
|
||||||
|
public long TemplateVersionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同名称快照。</summary>
|
||||||
|
public string ContractName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同类型快照。</summary>
|
||||||
|
public string ContractType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同开始日期。</summary>
|
||||||
|
public DateTime? ContractStartDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同结束日期。</summary>
|
||||||
|
public DateTime? ContractEndDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同期限原始文本。</summary>
|
||||||
|
public string ContractTerm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>试用期开始日期。</summary>
|
||||||
|
public DateTime? ProbationStartDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>试用期结束日期。</summary>
|
||||||
|
public DateTime? ProbationEndDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>试用期期限原始文本。</summary>
|
||||||
|
public string ProbationTerm { get; set; }
|
||||||
|
|
||||||
|
/// <summary>明确使用 hr.ProbationSalary 保存的试用薪资;不从其他工资项目推算。</summary>
|
||||||
|
public decimal? ProbationSalary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>基本工资。</summary>
|
||||||
|
public decimal? BaseSalary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>岗位工资。</summary>
|
||||||
|
public decimal? PositionSalary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>绩效津贴。</summary>
|
||||||
|
public decimal? PerformanceAllowance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>全勤奖。</summary>
|
||||||
|
public decimal? AttendanceBonus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>转正后薪资。</summary>
|
||||||
|
public decimal? RegularSalary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>工作地点或城市。</summary>
|
||||||
|
public string WorkCity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同中的岗位名称。</summary>
|
||||||
|
public string PositionName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>合同中的员工工号。</summary>
|
||||||
|
public string EmployeeNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 以 VariableCode 为键保存该合同当时的全部变量元数据和值。
|
||||||
|
/// 导出时通过稳定编码判断变量是否存在,不依赖可修改的中文名称。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = "json", IsNullable = false)]
|
||||||
|
public string VariablesJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>快照结构版本。</summary>
|
||||||
|
public int SnapshotVersion { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>快照生成时间。</summary>
|
||||||
|
public DateTime SnapshotAt { get; set; }
|
||||||
|
}
|
||||||
@@ -10,14 +10,40 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态入职字段定义,用于扩展固定人员基础信息之外的填写项。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_form_field", TableDescription = "动态入职填写字段")]
|
[SugarTable("ob_form_field", TableDescription = "动态入职填写字段")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObFormField : BaseEntity
|
public class ObFormField : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段业务编码。
|
||||||
|
/// </summary>
|
||||||
public string FieldCode { get; set; }
|
public string FieldCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段显示名称。
|
||||||
|
/// </summary>
|
||||||
public string FieldName { get; set; }
|
public string FieldName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量数据类型。
|
||||||
|
/// </summary>
|
||||||
public string DataType { get; set; }
|
public string DataType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 选择型字段或变量的可选项 JSON。
|
||||||
|
/// </summary>
|
||||||
public string OptionsJson { get; set; }
|
public string OptionsJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字段校验规则。
|
||||||
|
/// </summary>
|
||||||
public string ValidationRule { get; set; }
|
public string ValidationRule { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工端输入提示。
|
||||||
|
/// </summary>
|
||||||
public string Placeholder { get; set; }
|
public string Placeholder { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,35 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段填写要求,配置公司或岗位需要展示及校验的字段。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_form_requirement", TableDescription = "公司岗位动态填写项要求")]
|
[SugarTable("ob_form_requirement", TableDescription = "公司岗位动态填写项要求")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObFormRequirement : BaseEntity
|
public class ObFormRequirement : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
public long CompanyId { get; set; }
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属岗位 Id;为空时表示适用于公司下全部岗位。
|
||||||
|
/// </summary>
|
||||||
public long? PositionId { get; set; }
|
public long? PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段定义 Id。
|
||||||
|
/// </summary>
|
||||||
public long FieldId { get; set; }
|
public long FieldId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人是否必须填写该动态字段。
|
||||||
|
/// </summary>
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示排序码。
|
||||||
|
/// </summary>
|
||||||
public int? SortCode { get; set; }
|
public int? SortCode { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,35 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职材料要求,配置公司或岗位需要候选人上传的材料。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_material_requirement", TableDescription = "公司岗位入职资料要求")]
|
[SugarTable("ob_material_requirement", TableDescription = "公司岗位入职资料要求")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObMaterialRequirement : BaseEntity
|
public class ObMaterialRequirement : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
public long CompanyId { get; set; }
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属岗位 Id;为空时表示适用于公司下全部岗位。
|
||||||
|
/// </summary>
|
||||||
public long? PositionId { get; set; }
|
public long? PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料类型 Id。
|
||||||
|
/// </summary>
|
||||||
public long MaterialTypeId { get; set; }
|
public long MaterialTypeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人是否必须上传该类材料。
|
||||||
|
/// </summary>
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示排序码。
|
||||||
|
/// </summary>
|
||||||
public int? SortCode { get; set; }
|
public int? SortCode { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,35 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职材料类型,定义允许上传的文件类型、数量和大小。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_material_type", TableDescription = "入职上传资料类型")]
|
[SugarTable("ob_material_type", TableDescription = "入职上传资料类型")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObMaterialType : BaseEntity
|
public class ObMaterialType : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 材料类型业务编码。
|
||||||
|
/// </summary>
|
||||||
public string MaterialCode { get; set; }
|
public string MaterialCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料显示名称。
|
||||||
|
/// </summary>
|
||||||
public string MaterialName { get; set; }
|
public string MaterialName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 允许上传的文件扩展名,多个值使用逗号分隔。
|
||||||
|
/// </summary>
|
||||||
public string AllowedExtensions { get; set; }
|
public string AllowedExtensions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个任务允许上传的最大文件数量。
|
||||||
|
/// </summary>
|
||||||
public int MaxFileCount { get; set; }
|
public int MaxFileCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个文件最大大小,单位为 MB。
|
||||||
|
/// </summary>
|
||||||
public int? MaxFileSizeMb { get; set; }
|
public int? MaxFileSizeMb { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,114 @@
|
|||||||
// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
|
namespace SimpleAdmin.Application;
|
||||||
//
|
|
||||||
// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
|
|
||||||
// 1.请不要删除和修改根目录下的LICENSE文件。
|
|
||||||
// 2.请不要删除和修改SimpleAdmin源码头部的版权声明。
|
|
||||||
// 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin
|
|
||||||
// 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。
|
|
||||||
// 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为。
|
|
||||||
// 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关。
|
|
||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新员工入职任务,承载候选人填写、审核、转正式员工和归档全过程。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_onboarding_task", TableDescription = "新员工入职任务")]
|
[SugarTable("ob_onboarding_task", TableDescription = "新员工入职任务")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObOnboardingTask : BaseEntity
|
public class ObOnboardingTask : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务编号。
|
||||||
|
/// </summary>
|
||||||
public string TaskNo { get; set; }
|
public string TaskNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过后生成的正式员工 Id。
|
||||||
|
/// </summary>
|
||||||
public long? EmployeeId { get; set; }
|
public long? EmployeeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
public long CompanyId { get; set; }
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务所属岗位 Id。
|
||||||
|
/// </summary>
|
||||||
public long PositionId { get; set; }
|
public long PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用工类型:full_time 正式用工,part_time 兼职用工。
|
||||||
|
/// </summary>
|
||||||
|
public string EmploymentType { get; set; } = EmployeeEmploymentType.FullTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建任务时锁定的入职模板 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long OnboardingTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建任务时指定的审核人用户 Id。
|
||||||
|
/// </summary>
|
||||||
public long? AssignedReviewerUserId { get; set; }
|
public long? AssignedReviewerUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务当前状态。
|
||||||
|
/// </summary>
|
||||||
public string TaskStatus { get; set; } = OnboardingTaskStatus.Draft;
|
public string TaskStatus { get; set; } = OnboardingTaskStatus.Draft;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工填写链接业务过期时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? LinkExpireAt { get; set; }
|
public DateTime? LinkExpireAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人累计提交轮次。
|
||||||
|
/// </summary>
|
||||||
public int SubmitCount { get; set; }
|
public int SubmitCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人最近一次提交时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? LastSubmittedAt { get; set; }
|
public DateTime? LastSubmittedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核或流程操作发生时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? AuditedAt { get; set; }
|
public DateTime? AuditedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务归档时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ArchivedAt { get; set; }
|
public DateTime? ArchivedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最近一次审核退回原因。
|
||||||
|
/// </summary>
|
||||||
public string RejectReason { get; set; }
|
public string RejectReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
public string Remark { get; set; }
|
public string Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公司名称 (展示用)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 部门名称 (展示用)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string DepartmentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位名称 (展示用)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string PositionName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板名称 (展示用)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string OnboardingTemplateName { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板,统一组织公司、岗位、合同、补充字段和上传材料。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_onboarding_template", TableDescription = "入职模板")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObOnboardingTemplate : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板业务编码。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板显示名称。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板用途和适用范围说明。
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板需要使用的合同文件及锁定版本。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_onboarding_template_contract", TableDescription = "入职模板合同配置")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObOnboardingTemplateContract : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long OnboardingTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板主档 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long ContractTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 锁定的合同模板版本 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long ContractTemplateVersionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工是否必须查看并确认该合同。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同打印份数。
|
||||||
|
/// </summary>
|
||||||
|
public int PrintCopies { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示和生成顺序。
|
||||||
|
/// </summary>
|
||||||
|
public int? SortCode { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板需要员工填写的动态补充字段。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_onboarding_template_form_field", TableDescription = "入职模板补充字段")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObOnboardingTemplateFormField : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long OnboardingTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态补充字段 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long FieldId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工是否必须填写。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机端显示顺序。
|
||||||
|
/// </summary>
|
||||||
|
public int? SortCode { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板需要员工上传的材料配置。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_onboarding_template_material", TableDescription = "入职模板上传材料")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObOnboardingTemplateMaterial : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long OnboardingTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上传材料类型 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long MaterialTypeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工是否必须上传。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机端显示顺序。
|
||||||
|
/// </summary>
|
||||||
|
public int? SortCode { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板适用岗位关系。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_onboarding_template_position", TableDescription = "入职模板适用岗位")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObOnboardingTemplatePosition : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long OnboardingTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long PositionId { get; set; }
|
||||||
|
}
|
||||||
@@ -10,16 +10,63 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务岗位档案,保存岗位所属部门、默认合同期限、试用期及业务参数。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_position", TableDescription = "入职业务岗位")]
|
[SugarTable("ob_position", TableDescription = "入职业务岗位")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObPosition : BaseEntity
|
public class ObPosition : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
public long CompanyId { get; set; }
|
public long CompanyId { get; set; }
|
||||||
public virtual string PositionCode { get; set; }
|
|
||||||
public virtual string PositionName { get; set; }
|
|
||||||
public decimal? DefaultContractYears { get; set; }
|
|
||||||
public int? DefaultProbationMonths { get; set; }
|
|
||||||
public string DefaultParamsJson { get; set; }
|
|
||||||
public string Remark { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位业务编码。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string PositionCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位名称。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string PositionName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认合同期限,单位为年。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DefaultContractYears { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认试用期,单位为月。
|
||||||
|
/// </summary>
|
||||||
|
public int? DefaultProbationMonths { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位默认合同参数 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public string DefaultParamsJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司名称 (展示用)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门名称 (展示用)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = true)]
|
||||||
|
public string DepartmentName { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,15 +10,45 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务提交与审核记录,按轮次保留意见及数据快照。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_task_audit", TableDescription = "入职任务提交与审核记录")]
|
[SugarTable("ob_task_audit", TableDescription = "入职任务提交与审核记录")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTaskAudit : BaseEntity
|
public class ObTaskAudit : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
public long TaskId { get; set; }
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核轮次,与任务提交次数对应。
|
||||||
|
/// </summary>
|
||||||
public int AuditRound { get; set; }
|
public int AuditRound { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核或流程动作。
|
||||||
|
/// </summary>
|
||||||
public string AuditAction { get; set; }
|
public string AuditAction { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核操作人用户 Id;员工提交时可为空。
|
||||||
|
/// </summary>
|
||||||
public long? AuditorUserId { get; set; }
|
public long? AuditorUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核意见或流程说明。
|
||||||
|
/// </summary>
|
||||||
public string AuditOpinion { get; set; }
|
public string AuditOpinion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本轮提交或审核时的数据快照 JSON。
|
||||||
|
/// </summary>
|
||||||
public string SnapshotJson { get; set; }
|
public string SnapshotJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核或流程操作发生时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime AuditedAt { get; set; }
|
public DateTime AuditedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,22 +10,91 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务合同实例,锁定模板版本并记录生成、阅读和签署状态。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_task_contract", TableDescription = "入职任务合同明细")]
|
[SugarTable("ob_task_contract", TableDescription = "入职任务合同明细")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTaskContract : BaseEntity
|
public class ObTaskContract : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
public long TaskId { get; set; }
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板主档案 Id。
|
||||||
|
/// </summary>
|
||||||
public long TemplateId { get; set; }
|
public long TemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板版本 Id。
|
||||||
|
/// </summary>
|
||||||
public long TemplateVersionId { get; set; }
|
public long TemplateVersionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务创建时冻结的合同名称。
|
||||||
|
/// </summary>
|
||||||
public string ContractName { get; set; }
|
public string ContractName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同类型。
|
||||||
|
/// </summary>
|
||||||
public string ContractType { get; set; }
|
public string ContractType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否为候选人必须阅读确认的合同。
|
||||||
|
/// </summary>
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务创建时冻结的合同打印份数。
|
||||||
|
/// </summary>
|
||||||
|
public int PrintCopies { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>任务创建时冻结的公司盖章要求;历史空值沿用需要盖章。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>任务创建时冻结的骑缝章要求;历史空值沿用系统配置。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务合同当前状态。
|
||||||
|
/// </summary>
|
||||||
public string ContractStatus { get; set; } = TaskContractStatus.PendingGenerate;
|
public string ContractStatus { get; set; } = TaskContractStatus.PendingGenerate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成后的 DOCX 文件 Id。
|
||||||
|
/// </summary>
|
||||||
public long? GeneratedDocxFileId { get; set; }
|
public long? GeneratedDocxFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 归档或打印使用的 PDF 文件 Id。
|
||||||
|
/// </summary>
|
||||||
public long? GeneratedPdfFileId { get; set; }
|
public long? GeneratedPdfFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同文件生成时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? GeneratedAt { get; set; }
|
public DateTime? GeneratedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人首次阅读时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ReadAt { get; set; }
|
public DateTime? ReadAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人确认阅读时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ConfirmedAt { get; set; }
|
public DateTime? ConfirmedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 线下签署完成时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? OfflineSignedAt { get; set; }
|
public DateTime? OfflineSignedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同打印时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? PrintedAt { get; set; }
|
public DateTime? PrintedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,55 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务合同变量最终值快照,合同生成后不再读取模板实时值。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_task_contract_variable", TableDescription = "任务合同变量最终值快照")]
|
[SugarTable("ob_task_contract_variable", TableDescription = "任务合同变量最终值快照")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTaskContractVariable : BaseEntity
|
public class ObTaskContractVariable : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属任务合同 Id。
|
||||||
|
/// </summary>
|
||||||
public long TaskContractId { get; set; }
|
public long TaskContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DOCX 中使用的变量编码。
|
||||||
|
/// </summary>
|
||||||
public string VariableCode { get; set; }
|
public string VariableCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量显示名称。
|
||||||
|
/// </summary>
|
||||||
public string VariableName { get; set; }
|
public string VariableName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量来源类型,例如员工、公司、合同或系统。
|
||||||
|
/// </summary>
|
||||||
public string VariableType { get; set; }
|
public string VariableType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量数据类型。
|
||||||
|
/// </summary>
|
||||||
public string DataType { get; set; }
|
public string DataType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量在任务合同中的展示顺序,创建任务时从模板变量复制并固定。
|
||||||
|
/// </summary>
|
||||||
|
public int SortCode { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同生成时使用的最终变量值。
|
||||||
|
/// </summary>
|
||||||
public string VariableValue { get; set; }
|
public string VariableValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最终值来源类型。
|
||||||
|
/// </summary>
|
||||||
public string SourceType { get; set; }
|
public string SourceType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否由人事手工覆盖自动值。
|
||||||
|
/// </summary>
|
||||||
public bool IsManualOverride { get; set; }
|
public bool IsManualOverride { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,35 +10,200 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
[SugarTable("ob_task_employee_info", TableDescription = "候选人提交的固定基础信息")]
|
/// <summary>
|
||||||
|
/// 候选人在入职任务中提交的《入职登记表》标准员工档案信息。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_task_employee_info", TableDescription = "候选人填写的入职登记表")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTaskEmployeeInfo : BaseEntity
|
public class ObTaskEmployeeInfo : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
public long TaskId { get; set; }
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工编号(选填/人事分配)。
|
||||||
|
/// </summary>
|
||||||
public string EmployeeNo { get; set; }
|
public string EmployeeNo { get; set; }
|
||||||
public string InternalNo { get; set; }
|
|
||||||
|
// ==================== 1. 基本个人信息 ====================
|
||||||
|
/// <summary>
|
||||||
|
/// 员工姓名。
|
||||||
|
/// </summary>
|
||||||
public string EmployeeName { get; set; }
|
public string EmployeeName { get; set; }
|
||||||
public string DepartmentName { get; set; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// 性别。
|
||||||
|
/// </summary>
|
||||||
public string Gender { get; set; }
|
public string Gender { get; set; }
|
||||||
public string Phone { get; set; }
|
|
||||||
public string IdCardNumber { get; set; }
|
/// <summary>
|
||||||
public string College { get; set; }
|
/// 民族。
|
||||||
public string Education { get; set; }
|
/// </summary>
|
||||||
public string Major { get; set; }
|
|
||||||
public DateTime? BirthDate { get; set; }
|
|
||||||
public string RegisteredResidence { get; set; }
|
|
||||||
public string Nation { get; set; }
|
public string Nation { get; set; }
|
||||||
public string ContactAddress { get; set; }
|
|
||||||
public DateTime? EntryDate { get; set; }
|
/// <summary>
|
||||||
public DateTime? ContractStartDate { get; set; }
|
/// 出生年月日。
|
||||||
public DateTime? ContractEndDate { get; set; }
|
/// </summary>
|
||||||
public DateTime? ProbationStartDate { get; set; }
|
public string Birthday { get; set; }
|
||||||
public DateTime? ProbationEndDate { get; set; }
|
|
||||||
public DateTime? RegularizationDate { get; set; }
|
/// <summary>
|
||||||
public string SocialInsuranceCompany { get; set; }
|
/// 户籍所在地。
|
||||||
public string ContractNature { get; set; }
|
/// </summary>
|
||||||
|
public string RegisteredResidence { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 身高 (cm)。
|
||||||
|
/// </summary>
|
||||||
|
public string Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 体重 (kg)。
|
||||||
|
/// </summary>
|
||||||
|
public string Weight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 健康状况。
|
||||||
|
/// </summary>
|
||||||
|
public string HealthCondition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 婚育情况。
|
||||||
|
/// </summary>
|
||||||
|
public string MaritalStatus { get; set; }
|
||||||
|
|
||||||
|
// ==================== 2. 学历与资质 ====================
|
||||||
|
/// <summary>
|
||||||
|
/// 最高学历。
|
||||||
|
/// </summary>
|
||||||
|
public string Education { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所学专业。
|
||||||
|
/// </summary>
|
||||||
|
public string Major { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 毕业院校。
|
||||||
|
/// </summary>
|
||||||
|
public string GraduatedSchool { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 职业资格。
|
||||||
|
/// </summary>
|
||||||
|
public string VocationalQualification { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 资格等级。
|
||||||
|
/// </summary>
|
||||||
|
public string QualificationLevel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有何技术特长。
|
||||||
|
/// </summary>
|
||||||
|
public string TechnicalSpecialty { get; set; }
|
||||||
|
|
||||||
|
// ==================== 3. 联系方式与薪酬账户 ====================
|
||||||
|
/// <summary>
|
||||||
|
/// 联系电话/手机号。
|
||||||
|
/// </summary>
|
||||||
|
public string Phone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 电子邮箱。
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 身份证号码。
|
||||||
|
/// </summary>
|
||||||
|
public string IdCardNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 暂住地址 / 现居住地址。
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 房东联系方式。
|
||||||
|
/// </summary>
|
||||||
|
public string LandlordContact { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工资银行卡号。
|
||||||
|
/// </summary>
|
||||||
public string BankCardNumber { get; set; }
|
public string BankCardNumber { get; set; }
|
||||||
public string BankAccountName { get; set; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开户银行名称。
|
||||||
|
/// </summary>
|
||||||
public string BankName { get; set; }
|
public string BankName { get; set; }
|
||||||
public string Remark { get; set; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// 银行卡开户人姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string BankAccountName { get; set; }
|
||||||
|
|
||||||
|
// ==================== 4. 紧急联系人 ====================
|
||||||
|
/// <summary>
|
||||||
|
/// 紧急联系人姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string EmergencyContact { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 紧急联系人电话。
|
||||||
|
/// </summary>
|
||||||
|
public string EmergencyPhone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 紧急联系人联系地址。
|
||||||
|
/// </summary>
|
||||||
|
public string EmergencyAddress { get; set; }
|
||||||
|
|
||||||
|
// ==================== 5. 动态多条经历(JSON 存储) ====================
|
||||||
|
/// <summary>
|
||||||
|
/// 教育学习经历 JSON 列表: [ { timeRange, location, content, result } ]
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string EducationHistoryJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主要工作经历 JSON 列表: [ { timeRange, companyName, salary, position, leaveReason } ]
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string WorkHistoryJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 家庭成员 JSON 列表: [ { name, relationship, age, company, phone } ]
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string FamilyMembersJson { get; set; }
|
||||||
|
|
||||||
|
// ==================== 6. 入职情况与说明 ====================
|
||||||
|
/// <summary>
|
||||||
|
/// 入职部门。
|
||||||
|
/// </summary>
|
||||||
|
public string OnboardingDepartment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职时间。
|
||||||
|
/// </summary>
|
||||||
|
public string OnboardingDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职岗位。
|
||||||
|
/// </summary>
|
||||||
|
public string OnboardingPosition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 其他说明。
|
||||||
|
/// </summary>
|
||||||
|
public string OtherRemarks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工补充自定义变量键值 JSON: { "VariableName": "Value" }
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||||
|
public string CustomVariablesJson { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务创建时冻结的动态补充字段要求。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_task_form_requirement", TableDescription = "任务补充字段快照")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObTaskFormRequirement : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原动态字段 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long FieldId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的字段业务编码。
|
||||||
|
/// </summary>
|
||||||
|
public string FieldCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的字段显示名称。
|
||||||
|
/// </summary>
|
||||||
|
public string FieldName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的数据类型。
|
||||||
|
/// </summary>
|
||||||
|
public string DataType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的选项 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public string OptionsJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否必须填写。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机端显示顺序。
|
||||||
|
/// </summary>
|
||||||
|
public int? SortCode { get; set; }
|
||||||
|
}
|
||||||
@@ -10,14 +10,40 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人动态字段填写值,保留字段快照及提交轮次。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_task_form_value", TableDescription = "候选人动态填写信息")]
|
[SugarTable("ob_task_form_value", TableDescription = "候选人动态填写信息")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTaskFormValue : BaseEntity
|
public class ObTaskFormValue : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
public long TaskId { get; set; }
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段定义 Id。
|
||||||
|
/// </summary>
|
||||||
public long FieldId { get; set; }
|
public long FieldId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段业务编码。
|
||||||
|
/// </summary>
|
||||||
public string FieldCode { get; set; }
|
public string FieldCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段显示名称。
|
||||||
|
/// </summary>
|
||||||
public string FieldName { get; set; }
|
public string FieldName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人填写的字段值。
|
||||||
|
/// </summary>
|
||||||
public string FieldValue { get; set; }
|
public string FieldValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该数据所属的提交轮次。
|
||||||
|
/// </summary>
|
||||||
public int SubmitRound { get; set; }
|
public int SubmitRound { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,51 @@
|
|||||||
// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
|
namespace SimpleAdmin.Application;
|
||||||
//
|
|
||||||
// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
|
|
||||||
// 1.请不要删除和修改根目录下的LICENSE文件。
|
|
||||||
// 2.请不要删除和修改SimpleAdmin源码头部的版权声明。
|
|
||||||
// 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin
|
|
||||||
// 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。
|
|
||||||
// 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为。
|
|
||||||
// 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关。
|
|
||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新员工专属填写链接。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_task_link", TableDescription = "新员工专属填写链接")]
|
[SugarTable("ob_task_link", TableDescription = "新员工专属填写链接")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTaskLink : BaseEntity
|
public class ObTaskLink : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
public long TaskId { get; set; }
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 明文 Token,仅供受保护的管理端链接接口内部读取。
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
[global::System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
public string PlainToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 访问 Token 的 SHA-256 哈希。
|
||||||
|
/// </summary>
|
||||||
public string TokenHash { get; set; }
|
public string TokenHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 填写链接当前状态。
|
||||||
|
/// </summary>
|
||||||
public string LinkStatus { get; set; } = OnboardingLinkStatus.Active;
|
public string LinkStatus { get; set; } = OnboardingLinkStatus.Active;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 链接过期时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime ExpiresAt { get; set; }
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 链接最近访问时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? LastAccessedAt { get; set; }
|
public DateTime? LastAccessedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 链接累计访问次数。
|
||||||
|
/// </summary>
|
||||||
public int AccessCount { get; set; }
|
public int AccessCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 链接撤销时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? RevokedAt { get; set; }
|
public DateTime? RevokedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,50 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人上传的入职材料,记录文件、提交轮次及审核状态。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_task_material", TableDescription = "候选人上传资料")]
|
[SugarTable("ob_task_material", TableDescription = "候选人上传资料")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTaskMaterial : BaseEntity
|
public class ObTaskMaterial : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
public long TaskId { get; set; }
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料类型 Id。
|
||||||
|
/// </summary>
|
||||||
public long MaterialTypeId { get; set; }
|
public long MaterialTypeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料显示名称。
|
||||||
|
/// </summary>
|
||||||
public string MaterialName { get; set; }
|
public string MaterialName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 系统文件 Id。
|
||||||
|
/// </summary>
|
||||||
public long FileId { get; set; }
|
public long FileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该数据所属的提交轮次。
|
||||||
|
/// </summary>
|
||||||
public int SubmitRound { get; set; }
|
public int SubmitRound { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料上传时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime UploadedAt { get; set; }
|
public DateTime UploadedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料审核状态。
|
||||||
|
/// </summary>
|
||||||
public string ReviewStatus { get; set; } = TaskMaterialReviewStatus.Pending;
|
public string ReviewStatus { get; set; } = TaskMaterialReviewStatus.Pending;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料审核意见。
|
||||||
|
/// </summary>
|
||||||
public string ReviewRemark { get; set; }
|
public string ReviewRemark { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务创建时冻结的上传材料要求。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_task_material_requirement", TableDescription = "任务上传材料快照")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObTaskMaterialRequirement : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原材料类型 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long MaterialTypeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的材料编码。
|
||||||
|
/// </summary>
|
||||||
|
public string MaterialCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的材料名称。
|
||||||
|
/// </summary>
|
||||||
|
public string MaterialName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的允许扩展名。
|
||||||
|
/// </summary>
|
||||||
|
public string AllowedExtensions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的最大文件数量。
|
||||||
|
/// </summary>
|
||||||
|
public int MaxFileCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结的单文件大小上限,单位 MB。
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxFileSizeMb { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否必须上传。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机端显示顺序。
|
||||||
|
/// </summary>
|
||||||
|
public int? SortCode { get; set; }
|
||||||
|
}
|
||||||
@@ -10,16 +10,50 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务操作日志,用于追踪关键状态变化及操作人。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_task_operation_log", TableDescription = "入职任务操作日志")]
|
[SugarTable("ob_task_operation_log", TableDescription = "入职任务操作日志")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTaskOperationLog : BaseEntity
|
public class ObTaskOperationLog : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
public long TaskId { get; set; }
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作人类型,例如员工、人事或系统。
|
||||||
|
/// </summary>
|
||||||
public string OperatorType { get; set; }
|
public string OperatorType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 后台操作人用户 Id。
|
||||||
|
/// </summary>
|
||||||
public long? OperatorUserId { get; set; }
|
public long? OperatorUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作类型。
|
||||||
|
/// </summary>
|
||||||
public string OperationType { get; set; }
|
public string OperationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作说明。
|
||||||
|
/// </summary>
|
||||||
public string OperationContent { get; set; }
|
public string OperationContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次变更的数据 JSON。
|
||||||
|
/// </summary>
|
||||||
public string ChangeDataJson { get; set; }
|
public string ChangeDataJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作来源 IP 地址。
|
||||||
|
/// </summary>
|
||||||
public string IpAddress { get; set; }
|
public string IpAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作发生时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime OperatedAt { get; set; }
|
public DateTime OperatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,21 +10,75 @@
|
|||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板变量定义,描述 DOCX 占位符、来源类型及填写规则。
|
||||||
|
/// </summary>
|
||||||
[SugarTable("ob_template_variable", TableDescription = "合同模板变量定义")]
|
[SugarTable("ob_template_variable", TableDescription = "合同模板变量定义")]
|
||||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
public class ObTemplateVariable : BaseEntity
|
public class ObTemplateVariable : BaseEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板版本 Id。
|
||||||
|
/// </summary>
|
||||||
public long TemplateVersionId { get; set; }
|
public long TemplateVersionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DOCX 中使用的变量编码。
|
||||||
|
/// </summary>
|
||||||
public string VariableCode { get; set; }
|
public string VariableCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量显示名称。
|
||||||
|
/// </summary>
|
||||||
public string VariableName { get; set; }
|
public string VariableName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量来源类型,例如员工、公司、合同或系统。
|
||||||
|
/// </summary>
|
||||||
public string VariableType { get; set; }
|
public string VariableType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量数据类型。
|
||||||
|
/// </summary>
|
||||||
public string DataType { get; set; }
|
public string DataType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量关联的候选人基础信息字段。
|
||||||
|
/// </summary>
|
||||||
public string EmployeeField { get; set; }
|
public string EmployeeField { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量默认值。
|
||||||
|
/// </summary>
|
||||||
public string DefaultValue { get; set; }
|
public string DefaultValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 选择型字段或变量的可选项 JSON。
|
||||||
|
/// </summary>
|
||||||
public string OptionsJson { get; set; }
|
public string OptionsJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量格式化规则。
|
||||||
|
/// </summary>
|
||||||
public string FormatRule { get; set; }
|
public string FormatRule { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否必填、必传或必签。
|
||||||
|
/// </summary>
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事是否可在创建任务后手工填写或调整。
|
||||||
|
/// </summary>
|
||||||
public bool HrEditable { get; set; }
|
public bool HrEditable { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示排序码。
|
||||||
|
/// </summary>
|
||||||
public int? SortCode { get; set; }
|
public int? SortCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
public string Remark { get; set; }
|
public string Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局合同变量字典定义表。
|
||||||
|
/// 包含标准入职登记表变量、人事合同变量及企业自定义变量。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("ob_variable_definition", TableDescription = "合同变量字典定义")]
|
||||||
|
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||||
|
public class ObVariableDefinition : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 变量全编码,如 employee.EmployeeName, hr.BaseSalary, company.CompanyName, system.TaskNo。
|
||||||
|
/// </summary>
|
||||||
|
public string VariableCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量中文显示名称,如“员工姓名”、“基本工资”。
|
||||||
|
/// </summary>
|
||||||
|
public string VariableName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量作用域分类:employee(员工变量), hr(人事参数), company(公司变量), system(系统变量)。
|
||||||
|
/// </summary>
|
||||||
|
public string VariableCategory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据类型:string, number, date, multiline。
|
||||||
|
/// </summary>
|
||||||
|
public string DataType { get; set; } = "string";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认值或占位提示。
|
||||||
|
/// </summary>
|
||||||
|
public string DefaultValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否为入职登记表内置字段(内置字段不可删,且第1步自动收集)。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsBuiltIn { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否必填。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序码。
|
||||||
|
/// </summary>
|
||||||
|
public int SortCode { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变量说明/备注。
|
||||||
|
/// </summary>
|
||||||
|
public string Remark { get; set; }
|
||||||
|
}
|
||||||
@@ -15,13 +15,44 @@ namespace SimpleAdmin.Application;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class OnboardingTaskStatus
|
public static class OnboardingTaskStatus
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 草稿,任务尚未生成员工填写链接。
|
||||||
|
/// </summary>
|
||||||
public const string Draft = "draft";
|
public const string Draft = "draft";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待填写,候选人可以通过有效链接编辑资料。
|
||||||
|
/// </summary>
|
||||||
public const string PendingFill = "pending_fill";
|
public const string PendingFill = "pending_fill";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核中,候选人已提交且资料暂时锁定。
|
||||||
|
/// </summary>
|
||||||
public const string Reviewing = "reviewing";
|
public const string Reviewing = "reviewing";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已退回,候选人可根据意见补充后再次提交。
|
||||||
|
/// </summary>
|
||||||
public const string Rejected = "rejected";
|
public const string Rejected = "rejected";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已通过,系统已经生成员工档案。
|
||||||
|
/// </summary>
|
||||||
public const string Approved = "approved";
|
public const string Approved = "approved";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已归档,任务流程结束且历史数据只读。
|
||||||
|
/// </summary>
|
||||||
public const string Archived = "archived";
|
public const string Archived = "archived";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已过期,任务或链接超过有效期。
|
||||||
|
/// </summary>
|
||||||
public const string Expired = "expired";
|
public const string Expired = "expired";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已撤销,任务或链接被人事主动终止。
|
||||||
|
/// </summary>
|
||||||
public const string Revoked = "revoked";
|
public const string Revoked = "revoked";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +61,24 @@ public static class OnboardingTaskStatus
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class OnboardingLinkStatus
|
public static class OnboardingLinkStatus
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 有效,链接可以正常访问。
|
||||||
|
/// </summary>
|
||||||
public const string Active = "active";
|
public const string Active = "active";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已过期,任务或链接超过有效期。
|
||||||
|
/// </summary>
|
||||||
public const string Expired = "expired";
|
public const string Expired = "expired";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已撤销,任务或链接被人事主动终止。
|
||||||
|
/// </summary>
|
||||||
public const string Revoked = "revoked";
|
public const string Revoked = "revoked";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已替换,旧链接已由新链接取代。
|
||||||
|
/// </summary>
|
||||||
public const string Replaced = "replaced";
|
public const string Replaced = "replaced";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +87,19 @@ public static class OnboardingLinkStatus
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ContractTemplateVersionStatus
|
public static class ContractTemplateVersionStatus
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 草稿,任务尚未生成员工填写链接。
|
||||||
|
/// </summary>
|
||||||
public const string Draft = "draft";
|
public const string Draft = "draft";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已启用,可用于创建新的入职任务。
|
||||||
|
/// </summary>
|
||||||
public const string Enabled = "enabled";
|
public const string Enabled = "enabled";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已停用,不再用于新任务。
|
||||||
|
/// </summary>
|
||||||
public const string Disabled = "disabled";
|
public const string Disabled = "disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,21 +108,185 @@ public static class ContractTemplateVersionStatus
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class TaskContractStatus
|
public static class TaskContractStatus
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 待生成,任务合同尚未生成文件。
|
||||||
|
/// </summary>
|
||||||
public const string PendingGenerate = "pending_generate";
|
public const string PendingGenerate = "pending_generate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待确认,合同已生成并等待候选人阅读确认。
|
||||||
|
/// </summary>
|
||||||
public const string PendingConfirm = "pending_confirm";
|
public const string PendingConfirm = "pending_confirm";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已确认,候选人已完成阅读确认。
|
||||||
|
/// </summary>
|
||||||
public const string Confirmed = "confirmed";
|
public const string Confirmed = "confirmed";
|
||||||
|
|
||||||
|
/// <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>
|
||||||
public const string Archived = "archived";
|
public const string Archived = "archived";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工用工类型。
|
||||||
|
/// </summary>
|
||||||
|
public static class EmployeeEmploymentType
|
||||||
|
{
|
||||||
|
public const string FullTime = "full_time";
|
||||||
|
public const string PartTime = "part_time";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工转正状态,仅由后台人事操作改变,不根据合同日期自动推算。
|
||||||
|
/// </summary>
|
||||||
|
public static class EmployeeRegularizationStatus
|
||||||
|
{
|
||||||
|
public const string Probation = "probation";
|
||||||
|
public const string Regular = "regular";
|
||||||
|
public const string NotApplicable = "not_applicable";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工社保状态。
|
||||||
|
/// </summary>
|
||||||
|
public static class EmployeeSocialInsuranceStatus
|
||||||
|
{
|
||||||
|
public const string Unknown = "unknown";
|
||||||
|
public const string NotPurchased = "not_purchased";
|
||||||
|
public const string Purchased = "purchased";
|
||||||
|
public const string Stopped = "stopped";
|
||||||
|
public const string NotApplicable = "not_applicable";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工人事变更类型。
|
||||||
|
/// </summary>
|
||||||
|
public static class EmployeeChangeType
|
||||||
|
{
|
||||||
|
public const string HrInfoUpdate = "hr_info_update";
|
||||||
|
public const string Regularization = "regularization";
|
||||||
|
public const string RegularizationRevoke = "regularization_revoke";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工附件业务类型。
|
||||||
|
/// </summary>
|
||||||
|
public static class EmployeeAttachmentBusinessType
|
||||||
|
{
|
||||||
|
public const string Regularization = "regularization";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
public static class ContractVariableType
|
public static class ContractVariableType
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 导入模板时尚未确认来源的变量。
|
||||||
|
/// </summary>
|
||||||
|
public const string Unresolved = "unresolved";
|
||||||
|
/// <summary>
|
||||||
|
/// 值来源于候选人或正式员工信息。
|
||||||
|
/// </summary>
|
||||||
public const string Employee = "employee";
|
public const string Employee = "employee";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 值来源于公司档案。
|
||||||
|
/// </summary>
|
||||||
public const string Company = "company";
|
public const string Company = "company";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 值来源于合同业务参数。
|
||||||
|
/// </summary>
|
||||||
public const string Contract = "contract";
|
public const string Contract = "contract";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 值由系统自动生成。
|
||||||
|
/// </summary>
|
||||||
public const string System = "system";
|
public const string System = "system";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +295,24 @@ public static class ContractVariableType
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ContractVariableSourceType
|
public static class ContractVariableSourceType
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 值来源于候选人或正式员工信息。
|
||||||
|
/// </summary>
|
||||||
public const string Employee = "employee";
|
public const string Employee = "employee";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 值来源于公司档案。
|
||||||
|
/// </summary>
|
||||||
public const string Company = "company";
|
public const string Company = "company";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 值由人事手工录入。
|
||||||
|
/// </summary>
|
||||||
public const string HrInput = "hr_input";
|
public const string HrInput = "hr_input";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 值由系统自动生成。
|
||||||
|
/// </summary>
|
||||||
public const string System = "system";
|
public const string System = "system";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +321,19 @@ public static class ContractVariableSourceType
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class TaskMaterialReviewStatus
|
public static class TaskMaterialReviewStatus
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 待审核。
|
||||||
|
/// </summary>
|
||||||
public const string Pending = "pending";
|
public const string Pending = "pending";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过。
|
||||||
|
/// </summary>
|
||||||
public const string Passed = "passed";
|
public const string Passed = "passed";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 需要补充或重新上传。
|
||||||
|
/// </summary>
|
||||||
public const string NeedSupplement = "need_supplement";
|
public const string NeedSupplement = "need_supplement";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +342,28 @@ public static class TaskMaterialReviewStatus
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class OnboardingAuditAction
|
public static class OnboardingAuditAction
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 候选人提交入职资料。
|
||||||
|
/// </summary>
|
||||||
public const string Submit = "submit";
|
public const string Submit = "submit";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事审核通过。
|
||||||
|
/// </summary>
|
||||||
public const string Approve = "approve";
|
public const string Approve = "approve";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事退回补充。
|
||||||
|
/// </summary>
|
||||||
public const string Reject = "reject";
|
public const string Reject = "reject";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事撤销任务。
|
||||||
|
/// </summary>
|
||||||
public const string Revoke = "revoke";
|
public const string Revoke = "revoke";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事归档任务。
|
||||||
|
/// </summary>
|
||||||
public const string Archive = "archive";
|
public const string Archive = "archive";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
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>
|
||||||
|
/// 契约锁企业成员中的合同创建人姓名。
|
||||||
|
/// </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>
|
||||||
|
/// 企业盖章相对关键字的横向偏移量,范围 (-1, 1),正数向右、负数向左。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CompanySealOffsetX { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 企业盖章相对关键字的纵向偏移量,范围 (-1, 1),正数向上、负数向下。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CompanySealOffsetY { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 历史版本未单独设置开关时,是否在企业盖章时加盖骑缝章;新版本显式开关优先。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAcrossPageSeal { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 骑缝章方式:ALL_DOCUMENT(所选文档合并)或 SINGLE_DOCUMENT(所选文档各自独立)。
|
||||||
|
/// </summary>
|
||||||
|
public string AcrossPageSealType { get; set; } = "ALL_DOCUMENT";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单次 HTTP 请求超时秒数。
|
||||||
|
/// </summary>
|
||||||
|
public int TimeoutSeconds { get; set; } = 90;
|
||||||
|
}
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职审核、退回、撤销或归档操作参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingAuditActionInput
|
public class OnboardingAuditActionInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "任务Id不能为空")]
|
[IdNotNull(ErrorMessage = "任务Id不能为空")]
|
||||||
public long TaskId { get; set; }
|
public long TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核意见或流程操作说明。
|
||||||
|
/// </summary>
|
||||||
public string AuditOpinion { get; set; }
|
public string AuditOpinion { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义入职审核业务能力。
|
||||||
|
/// </summary>
|
||||||
public interface IOnboardingAuditService : ITransient
|
public interface IOnboardingAuditService : ITransient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过入职任务,并在同一事务中创建正式员工档案。
|
||||||
|
/// </summary>
|
||||||
Task Approve(OnboardingAuditActionInput input);
|
Task Approve(OnboardingAuditActionInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 退回入职任务,保存审核意见并允许候选人补充资料。
|
||||||
|
/// </summary>
|
||||||
Task Reject(OnboardingAuditActionInput input);
|
Task Reject(OnboardingAuditActionInput input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,50 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现入职任务审核与退回业务能力。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingAuditService : IOnboardingAuditService
|
public class OnboardingAuditService : IOnboardingAuditService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarClient _db;
|
private readonly SqlSugarScope _db;
|
||||||
private readonly IConfigService _config;
|
|
||||||
private readonly IOnboardingContractSnapshotService _snapshotService;
|
private readonly IOnboardingContractSnapshotService _snapshotService;
|
||||||
|
private readonly IOnboardingEmployeeContractSnapshotService _employeeContractSnapshotService;
|
||||||
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
public OnboardingAuditService(
|
public OnboardingAuditService(
|
||||||
ISqlSugarClient db,
|
IOnboardingContractSnapshotService snapshotService,
|
||||||
IConfigService config,
|
IOnboardingEmployeeContractSnapshotService employeeContractSnapshotService,
|
||||||
IOnboardingContractSnapshotService snapshotService)
|
IOnboardingDataPermissionService dataPermission)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = DbContext.DB;
|
||||||
_config = config;
|
|
||||||
_snapshotService = snapshotService;
|
_snapshotService = snapshotService;
|
||||||
|
_employeeContractSnapshotService = employeeContractSnapshotService;
|
||||||
|
_dataPermission = dataPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过入职任务,并在同一事务中创建正式员工档案。
|
||||||
|
/// </summary>
|
||||||
public async Task Approve(OnboardingAuditActionInput input)
|
public async Task Approve(OnboardingAuditActionInput input)
|
||||||
|
{
|
||||||
|
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => ApproveCore(input), protectIdentity: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApproveCore(OnboardingAuditActionInput input)
|
||||||
{
|
{
|
||||||
var task = await GetAuditableTask(input.TaskId);
|
var task = await GetAuditableTask(input.TaskId);
|
||||||
var employeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
var employeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||||
.FirstAsync(item => item.TaskId == task.Id)
|
.FirstAsync(item => item.TaskId == task.Id)
|
||||||
?? throw Oops.Bah("候选人资料不存在");
|
?? throw Oops.Bah("候选人资料不存在");
|
||||||
|
|
||||||
var hasMaterialNeedSupplement = await _db.Queryable<ObTaskMaterial>()
|
var hasUnapprovedMaterial = await _db.Queryable<ObTaskMaterial>()
|
||||||
.AnyAsync(item => item.TaskId == task.Id
|
.AnyAsync(item => item.TaskId == task.Id
|
||||||
&& item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
|
&& item.ReviewStatus != TaskMaterialReviewStatus.Passed);
|
||||||
if (hasMaterialNeedSupplement)
|
if (hasUnapprovedMaterial)
|
||||||
{
|
{
|
||||||
throw Oops.Bah("仍有资料需要补充,不能审核通过");
|
throw Oops.Bah("仍有资料未审核通过,不能审核通过入职任务");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 审核通过是创建正式员工的唯一入口,因此必须在入库前拦截身份证号或手机号重复。
|
||||||
var hasExistingEmployee = await _db.Queryable<ObEmployee>()
|
var hasExistingEmployee = await _db.Queryable<ObEmployee>()
|
||||||
.AnyAsync(item => item.IdCardNumber == employeeInfo.IdCardNumber || item.Phone == employeeInfo.Phone);
|
.AnyAsync(item => item.IdCardNumber == employeeInfo.IdCardNumber || item.Phone == employeeInfo.Phone);
|
||||||
if (hasExistingEmployee)
|
if (hasExistingEmployee)
|
||||||
@@ -38,25 +52,105 @@ public class OnboardingAuditService : IOnboardingAuditService
|
|||||||
throw Oops.Bah("该身份证号或手机号已存在正式员工");
|
throw Oops.Bah("该身份证号或手机号已存在正式员工");
|
||||||
}
|
}
|
||||||
|
|
||||||
var extraValues = await _db.Queryable<ObTaskFormValue>()
|
var contractStartDate = await ResolveContractStartDate(task.Id);
|
||||||
.Where(item => item.TaskId == task.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
var employee = employeeInfo.Adapt<ObEmployee>();
|
var employee = employeeInfo.Adapt<ObEmployee>();
|
||||||
|
if (string.IsNullOrWhiteSpace(employee.EmployeeNo))
|
||||||
|
{
|
||||||
|
var taskContractIds = await _db.Queryable<ObTaskContract>()
|
||||||
|
.Where(item => item.TaskId == task.Id)
|
||||||
|
.Select(item => item.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (taskContractIds.Count > 0)
|
||||||
|
{
|
||||||
|
employee.EmployeeNo = await _db.Queryable<ObTaskContractVariable>()
|
||||||
|
.Where(item => taskContractIds.Contains(item.TaskContractId) && item.VariableCode == "hr.EmployeeNo" && item.VariableValue != null && item.VariableValue != "")
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.Select(item => item.VariableValue)
|
||||||
|
.FirstAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
employee.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
employee.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||||
employee.CompanyId = task.CompanyId;
|
employee.CompanyId = task.CompanyId;
|
||||||
|
employee.DepartmentId = task.DepartmentId;
|
||||||
employee.PositionId = task.PositionId;
|
employee.PositionId = task.PositionId;
|
||||||
employee.ExtraInfoJson = global::System.Text.Json.JsonSerializer.Serialize(extraValues);
|
employee.EmploymentType = string.IsNullOrWhiteSpace(task.EmploymentType)
|
||||||
|
? EmployeeEmploymentType.FullTime
|
||||||
|
: task.EmploymentType;
|
||||||
|
employee.RegularizationStatus = employee.EmploymentType == EmployeeEmploymentType.PartTime
|
||||||
|
? EmployeeRegularizationStatus.NotApplicable
|
||||||
|
: EmployeeRegularizationStatus.Probation;
|
||||||
|
employee.SocialInsuranceStatus = EmployeeSocialInsuranceStatus.Unknown;
|
||||||
|
if (task.DepartmentId.HasValue)
|
||||||
|
{
|
||||||
|
var dept = await _db.Queryable<ObDepartment>().InSingleAsync(task.DepartmentId.Value);
|
||||||
|
employee.DepartmentName = dept?.DepartmentName ?? employeeInfo.OnboardingDepartment;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
employee.DepartmentName = employeeInfo.OnboardingDepartment;
|
||||||
|
}
|
||||||
|
employee.College = employeeInfo.GraduatedSchool;
|
||||||
|
employee.BirthDate = ParseDate(employeeInfo.Birthday);
|
||||||
|
employee.ContactAddress = employeeInfo.CurrentAddress;
|
||||||
|
employee.EntryDate = contractStartDate;
|
||||||
|
employee.ContractStartDate = contractStartDate;
|
||||||
|
employee.Remark = employeeInfo.OtherRemarks;
|
||||||
|
employee.ExtraInfoJson = global::System.Text.Json.JsonSerializer.Serialize(employeeInfo);
|
||||||
|
employee.Status = CommonStatusConst.ENABLE;
|
||||||
|
employee.CreateTime = DateTime.Now;
|
||||||
|
// 员工档案归属于原任务创建人,而不是执行审核的人。
|
||||||
|
employee.CreateUserId = task.CreateUserId;
|
||||||
|
employee.CreateUser = task.CreateUser;
|
||||||
|
employee.UpdateTime = null;
|
||||||
|
employee.UpdateUserId = null;
|
||||||
|
employee.UpdateUser = null;
|
||||||
|
employee.IsDelete = false;
|
||||||
|
|
||||||
|
// 员工档案、任务状态和审核记录必须同时成功,避免出现“已通过但没有员工档案”的中间状态。
|
||||||
await _db.Ado.BeginTranAsync();
|
await _db.Ado.BeginTranAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var updatedRows = await _db.Updateable<ObOnboardingTask>()
|
||||||
|
.SetColumns(item => new ObOnboardingTask
|
||||||
|
{
|
||||||
|
EmployeeId = employee.Id,
|
||||||
|
TaskStatus = OnboardingTaskStatus.Approved,
|
||||||
|
AuditedAt = DateTime.Now
|
||||||
|
})
|
||||||
|
.Where(item => item.Id == task.Id
|
||||||
|
&& item.TaskStatus == OnboardingTaskStatus.Reviewing
|
||||||
|
&& item.EmployeeId == null)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
if (updatedRows != 1)
|
||||||
|
throw Oops.Bah("该入职任务已被其他审核人处理,请刷新后查看");
|
||||||
|
|
||||||
await _snapshotService.Build(task.Id, employeeInfo);
|
await _snapshotService.Build(task.Id, employeeInfo);
|
||||||
await _db.Insertable(employee).ExecuteCommandAsync();
|
await _db.Insertable(employee).ExecuteCommandAsync();
|
||||||
|
|
||||||
|
// 全局插入审计会把 CreateUserId 设置为当前审核人。管理员代审核他人任务时,
|
||||||
|
// 员工档案仍应归属于原任务创建人,否则普通人事将无法继续维护自己发起的员工。
|
||||||
|
if (task.CreateUserId.HasValue && task.CreateUserId != UserManager.UserId)
|
||||||
|
{
|
||||||
|
await _db.Updateable<ObEmployee>()
|
||||||
|
.SetColumns(item => item.CreateUserId == task.CreateUserId)
|
||||||
|
.SetColumns(item => item.CreateUser == task.CreateUser)
|
||||||
|
.Where(item => item.Id == employee.Id)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
var ownerPersisted = await _db.Queryable<ObEmployee>()
|
||||||
|
.AnyAsync(item => item.Id == employee.Id
|
||||||
|
&& item.CreateUserId == task.CreateUserId);
|
||||||
|
if (!ownerPersisted)
|
||||||
|
throw Oops.Bah("员工档案创建人归属保存失败,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _employeeContractSnapshotService.Build(employee.Id, task.Id);
|
||||||
|
|
||||||
task.EmployeeId = employee.Id;
|
task.EmployeeId = employee.Id;
|
||||||
task.TaskStatus = OnboardingTaskStatus.Approved;
|
task.TaskStatus = OnboardingTaskStatus.Approved;
|
||||||
task.AuditedAt = DateTime.Now;
|
task.AuditedAt = DateTime.Now;
|
||||||
await _db.Updateable(task).ExecuteCommandAsync();
|
|
||||||
await WriteAudit(task, OnboardingAuditAction.Approve, input.AuditOpinion);
|
await WriteAudit(task, OnboardingAuditAction.Approve, input.AuditOpinion);
|
||||||
await _db.Ado.CommitTranAsync();
|
await _db.Ado.CommitTranAsync();
|
||||||
}
|
}
|
||||||
@@ -67,7 +161,15 @@ public class OnboardingAuditService : IOnboardingAuditService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 退回入职任务,保存审核意见并允许候选人补充资料。
|
||||||
|
/// </summary>
|
||||||
public async Task Reject(OnboardingAuditActionInput input)
|
public async Task Reject(OnboardingAuditActionInput input)
|
||||||
|
{
|
||||||
|
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => RejectCore(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RejectCore(OnboardingAuditActionInput input)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(input.AuditOpinion))
|
if (string.IsNullOrWhiteSpace(input.AuditOpinion))
|
||||||
{
|
{
|
||||||
@@ -75,10 +177,34 @@ public class OnboardingAuditService : IOnboardingAuditService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var task = await GetAuditableTask(input.TaskId);
|
var task = await GetAuditableTask(input.TaskId);
|
||||||
task.TaskStatus = OnboardingTaskStatus.Rejected;
|
await _db.Ado.BeginTranAsync();
|
||||||
task.RejectReason = input.AuditOpinion;
|
try
|
||||||
await _db.Updateable(task).ExecuteCommandAsync();
|
{
|
||||||
await WriteAudit(task, OnboardingAuditAction.Reject, input.AuditOpinion);
|
var auditedAt = DateTime.Now;
|
||||||
|
var updatedRows = await _db.Updateable<ObOnboardingTask>()
|
||||||
|
.SetColumns(item => new ObOnboardingTask
|
||||||
|
{
|
||||||
|
TaskStatus = OnboardingTaskStatus.Rejected,
|
||||||
|
AuditedAt = auditedAt,
|
||||||
|
RejectReason = input.AuditOpinion
|
||||||
|
})
|
||||||
|
.Where(item => item.Id == task.Id
|
||||||
|
&& item.TaskStatus == OnboardingTaskStatus.Reviewing)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
if (updatedRows != 1)
|
||||||
|
throw Oops.Bah("该入职任务已被其他审核人处理,请刷新后查看");
|
||||||
|
|
||||||
|
task.TaskStatus = OnboardingTaskStatus.Rejected;
|
||||||
|
task.AuditedAt = auditedAt;
|
||||||
|
task.RejectReason = input.AuditOpinion;
|
||||||
|
await WriteAudit(task, OnboardingAuditAction.Reject, input.AuditOpinion);
|
||||||
|
await _db.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _db.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ObOnboardingTask> GetAuditableTask(long taskId)
|
private async Task<ObOnboardingTask> GetAuditableTask(long taskId)
|
||||||
@@ -90,29 +216,66 @@ public class OnboardingAuditService : IOnboardingAuditService
|
|||||||
throw Oops.Bah("当前任务不在审核中");
|
throw Oops.Bah("当前任务不在审核中");
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = await _config.GetByConfigKey(CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.reviewer.userIds")
|
await _dataPermission.EnsureCanManage(
|
||||||
?? throw Oops.Bah("请配置入职审核人");
|
task.CreateUserId,
|
||||||
using var configJson = global::System.Text.Json.JsonDocument.Parse(config.ConfigValue);
|
OnboardingPermissionCodes.TaskManageAll,
|
||||||
var reviewerIds = new List<long>();
|
"入职任务");
|
||||||
if (configJson.RootElement.TryGetProperty("all", out var allReviewers))
|
|
||||||
{
|
|
||||||
reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configJson.RootElement.TryGetProperty("company", out var companyReviewers)
|
|
||||||
&& companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers))
|
|
||||||
{
|
|
||||||
reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reviewerIds.Contains(UserManager.UserId))
|
|
||||||
{
|
|
||||||
throw Oops.Bah("您不是该入职任务的审核人");
|
|
||||||
}
|
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从任务签署材料参数中读取唯一的合同开始日期,作为正式员工入职日期。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<DateTime> ResolveContractStartDate(long taskId)
|
||||||
|
{
|
||||||
|
var taskContractIds = await _db.Queryable<ObTaskContract>()
|
||||||
|
.Where(item => item.TaskId == taskId)
|
||||||
|
.Select(item => item.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
if (taskContractIds.Count == 0)
|
||||||
|
throw Oops.Bah("入职任务没有签署材料,无法确定入职日期");
|
||||||
|
|
||||||
|
var startDateValues = await _db.Queryable<ObTaskContractVariable>()
|
||||||
|
.Where(item => taskContractIds.Contains(item.TaskContractId)
|
||||||
|
&& item.VariableCode == "hr.ContractStartDate")
|
||||||
|
.Select(item => item.VariableValue)
|
||||||
|
.ToListAsync();
|
||||||
|
var normalizedValues = startDateValues
|
||||||
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||||
|
.Select(item => item.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
if (normalizedValues.Count == 0)
|
||||||
|
throw Oops.Bah("签署材料参数“合同开始日期”尚未填写,无法生成员工档案");
|
||||||
|
|
||||||
|
var parsedDates = new List<DateTime>();
|
||||||
|
foreach (var value in normalizedValues)
|
||||||
|
{
|
||||||
|
if (!DateTime.TryParse(value, out var parsedDate))
|
||||||
|
throw Oops.Bah("签署材料参数“合同开始日期”格式不正确");
|
||||||
|
|
||||||
|
parsedDates.Add(parsedDate.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
var distinctDates = parsedDates.Distinct().ToList();
|
||||||
|
if (distinctDates.Count > 1)
|
||||||
|
throw Oops.Bah("多个签署材料的合同开始日期不一致,请统一后再审核通过");
|
||||||
|
|
||||||
|
return distinctDates[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将登记表中的日期文本转换为正式员工档案日期。
|
||||||
|
/// </summary>
|
||||||
|
private static DateTime? ParseDate(string value)
|
||||||
|
{
|
||||||
|
return DateTime.TryParse(value, out var result)
|
||||||
|
? result.Date
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
private Task WriteAudit(ObOnboardingTask task, string action, string opinion)
|
private Task WriteAudit(ObOnboardingTask task, string action, string opinion)
|
||||||
{
|
{
|
||||||
return _db.Insertable(new ObTaskAudit
|
return _db.Insertable(new ObTaskAudit
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>版本盖章配置局部更新,避免覆盖另一个开关、版本状态或历史任务快照。</summary>
|
||||||
|
internal static class ContractTemplateVersionSealConfiguration
|
||||||
|
{
|
||||||
|
internal static IUpdateable<ObContractTemplateVersion> BuildUpdate(
|
||||||
|
ISqlSugarClient db,
|
||||||
|
OnboardingContractVersionSealInput input)
|
||||||
|
{
|
||||||
|
if (input.Id <= 0)
|
||||||
|
throw Oops.Bah("版本Id不能为空");
|
||||||
|
if (!input.CompanySealEnabled.HasValue && !input.AcrossPageSealEnabled.HasValue)
|
||||||
|
throw Oops.Bah("请指定需要修改的盖章开关");
|
||||||
|
|
||||||
|
var update = db.Updateable<ObContractTemplateVersion>()
|
||||||
|
.Where(item => item.Id == input.Id);
|
||||||
|
if (input.CompanySealEnabled.HasValue)
|
||||||
|
update = update.SetColumns(item => item.CompanySealEnabled == input.CompanySealEnabled);
|
||||||
|
if (input.AcrossPageSealEnabled.HasValue)
|
||||||
|
update = update.SetColumns(item => item.AcrossPageSealEnabled == input.AcrossPageSealEnabled);
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签署材料模板版本的时间有效性规则。
|
||||||
|
/// </summary>
|
||||||
|
internal static class ContractTemplateVersionValidity
|
||||||
|
{
|
||||||
|
internal static void ValidateRange(DateTime? effectiveAt, DateTime? expiresAt)
|
||||||
|
{
|
||||||
|
if (effectiveAt.HasValue && expiresAt.HasValue && expiresAt.Value <= effectiveAt.Value)
|
||||||
|
throw Oops.Bah("失效时间必须晚于生效时间");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsAvailable(ObContractTemplateVersion version, DateTime now)
|
||||||
|
{
|
||||||
|
return version != null
|
||||||
|
&& version.VersionStatus == ContractTemplateVersionStatus.Enabled
|
||||||
|
&& (!version.EffectiveDate.HasValue || version.EffectiveDate.Value <= now)
|
||||||
|
&& (!version.ExpiryDate.HasValue || version.ExpiryDate.Value > now);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务数据操作权限校验。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingDataPermissionService : ITransient
|
||||||
|
{
|
||||||
|
Task<bool> CanManage(long? createUserId, string manageAllPermissionCode);
|
||||||
|
|
||||||
|
Task EnsureCanManage(long? createUserId, string manageAllPermissionCode, string dataName);
|
||||||
|
|
||||||
|
Task EnsureCanManage(IEnumerable<long?> createUserIds, string manageAllPermissionCode, string dataName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一校验入职业务中的可编辑编码,避免非法字符或超长内容落到数据库后才报 500。
|
||||||
|
/// </summary>
|
||||||
|
internal static class OnboardingCodeValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 规范化并校验业务编码。编码必须以字母开头,可包含字母、数字、点、下划线和短横线。
|
||||||
|
/// </summary>
|
||||||
|
public static string Normalize(string code, string fieldName, int maxLength)
|
||||||
|
{
|
||||||
|
var normalized = code?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
throw Oops.Bah($"{fieldName}不能为空");
|
||||||
|
|
||||||
|
if (normalized.Length > maxLength)
|
||||||
|
throw Oops.Bah($"{fieldName}长度不能超过 {maxLength} 个字符");
|
||||||
|
|
||||||
|
if (!global::System.Text.RegularExpressions.Regex.IsMatch(
|
||||||
|
normalized,
|
||||||
|
@"^[A-Za-z][A-Za-z0-9._-]*$"))
|
||||||
|
{
|
||||||
|
throw Oops.Bah($"{fieldName}必须以字母开头,只能包含字母、数字、点、下划线和短横线");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
using System.Data.Common;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所有修改同一任务的入口共用 MySQL 命名锁,适用于同一主库上的多个后端实例。
|
||||||
|
/// 锁使用独立、非池化连接,不在第三方请求期间持有业务数据库事务。
|
||||||
|
/// </summary>
|
||||||
|
internal static class OnboardingConcurrency
|
||||||
|
{
|
||||||
|
public static Task RunTask(SqlSugarScope db, long taskId, Func<Task> action,
|
||||||
|
bool transactional = false, bool protectIdentity = false)
|
||||||
|
=> RunTask(db, taskId, async () => { await action(); return true; },
|
||||||
|
transactional, protectIdentity);
|
||||||
|
|
||||||
|
public static Task<T> RunTask<T>(SqlSugarScope db, long taskId, Func<Task<T>> action,
|
||||||
|
bool transactional = false, bool protectIdentity = false)
|
||||||
|
=> Run(db, $"task:{taskId}", () => protectIdentity
|
||||||
|
? RunPerson(db, taskId, () => InTransaction(db, action, transactional))
|
||||||
|
: InTransaction(db, action, transactional));
|
||||||
|
|
||||||
|
public static Task RunEmployee(SqlSugarScope db, long employeeId, Func<Task> action)
|
||||||
|
=> Run(db, $"employee:{employeeId}", async () => { await action(); return true; });
|
||||||
|
|
||||||
|
public static Task RunPublic(SqlSugarScope db, string token, Func<Task> action,
|
||||||
|
bool transactional = false, bool protectIdentity = false)
|
||||||
|
=> RunPublic(db, token, async () => { await action(); return true; },
|
||||||
|
transactional, protectIdentity);
|
||||||
|
|
||||||
|
public static async Task<T> RunPublic<T>(SqlSugarScope db, string token, Func<Task<T>> action,
|
||||||
|
bool transactional = false, bool protectIdentity = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
throw Oops.Bah("入职链接 Token 不能为空");
|
||||||
|
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
|
||||||
|
var link = await db.Queryable<ObTaskLink>().FirstAsync(item => item.TokenHash == hash)
|
||||||
|
?? throw Oops.Bah("链接不存在");
|
||||||
|
// 此处只定位锁;被保护的方法必须在锁内重新检查链接、任务状态和归属。
|
||||||
|
return await RunTask(db, link.TaskId, action, transactional, protectIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task RunContract(SqlSugarScope db, long contractId, Func<Task> action,
|
||||||
|
bool transactional = false)
|
||||||
|
=> RunContract(db, contractId, async () => { await action(); return true; }, transactional);
|
||||||
|
|
||||||
|
public static async Task<T> RunContract<T>(SqlSugarScope db, long contractId, Func<Task<T>> action,
|
||||||
|
bool transactional = false)
|
||||||
|
{
|
||||||
|
var contract = await db.Queryable<ObTaskContract>().InSingleAsync(contractId)
|
||||||
|
?? throw Oops.Bah("任务合同不存在");
|
||||||
|
return await RunTask(db, contract.TaskId, action, transactional);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RunMaterial(SqlSugarScope db, long materialId, Func<Task> action)
|
||||||
|
{
|
||||||
|
var material = await db.Queryable<ObTaskMaterial>().InSingleAsync(materialId)
|
||||||
|
?? throw Oops.Bah("任务资料不存在");
|
||||||
|
await RunTask(db, material.TaskId, action, transactional: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<T> RunPerson<T>(SqlSugarScope db, long taskId, Func<Task<T>> action)
|
||||||
|
{
|
||||||
|
var info = await db.Queryable<ObTaskEmployeeInfo>().FirstAsync(item => item.TaskId == taskId)
|
||||||
|
?? throw Oops.Bah("请先完整填写基本信息");
|
||||||
|
// 统一顺序:任务 -> 身份证 -> 手机;跨任务提交/建档的查重与写入也串行。
|
||||||
|
return await Run(db, "identity:" + info.IdCardNumber?.Trim().ToUpperInvariant(),
|
||||||
|
() => Run(db, "phone:" + info.Phone?.Trim(), action));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<T> InTransaction<T>(SqlSugarScope db, Func<Task<T>> action, bool enabled)
|
||||||
|
{
|
||||||
|
if (!enabled || db.Ado.Transaction != null)
|
||||||
|
return await action();
|
||||||
|
await db.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await action();
|
||||||
|
await db.Ado.CommitTranAsync();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await db.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<T> Run<T>(SqlSugarScope db, string resource, Func<Task<T>> action)
|
||||||
|
{
|
||||||
|
if (db.CurrentConnectionConfig.DbType != global::SqlSugar.DbType.MySql)
|
||||||
|
throw Oops.Bah("入职并发保护需要 MySQL 数据库");
|
||||||
|
// 使用已选择的业务主库,不能在只读副本/不同主库上分别申请锁。
|
||||||
|
var database = db.Ado.Connection.Database;
|
||||||
|
var key = Convert.ToHexString(SHA256.HashData(
|
||||||
|
Encoding.UTF8.GetBytes($"onboardx:{database}:{resource}")));
|
||||||
|
return OnboardingOperationGate.Run(key, () => Acquire(db, key), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IAsyncDisposable> Acquire(SqlSugarScope db, string key)
|
||||||
|
{
|
||||||
|
var connection = (DbConnection)Activator.CreateInstance(db.Ado.Connection.GetType());
|
||||||
|
var builder = new DbConnectionStringBuilder
|
||||||
|
{
|
||||||
|
ConnectionString = db.CurrentConnectionConfig.ConnectionString
|
||||||
|
};
|
||||||
|
// 确保异常关闭时会真正关闭持锁会话,不把命名锁留在连接池里。
|
||||||
|
builder["Pooling"] = false;
|
||||||
|
connection.ConnectionString = builder.ConnectionString;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.OpenAsync();
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = "SELECT GET_LOCK(@lockName, 0)";
|
||||||
|
command.CommandTimeout = 5;
|
||||||
|
AddKey(command, key);
|
||||||
|
var result = await command.ExecuteScalarAsync();
|
||||||
|
if (result == null || result == DBNull.Value || Convert.ToInt32(result) != 1)
|
||||||
|
throw Oops.Bah("当前数据正在处理,请稍后刷新重试");
|
||||||
|
return new NamedLock(connection, key);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddKey(DbCommand command, string key)
|
||||||
|
{
|
||||||
|
var parameter = command.CreateParameter();
|
||||||
|
parameter.ParameterName = "@lockName";
|
||||||
|
parameter.Value = key;
|
||||||
|
command.Parameters.Add(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NamedLock : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly DbConnection _connection;
|
||||||
|
private readonly string _key;
|
||||||
|
|
||||||
|
public NamedLock(DbConnection connection, string key)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var command = _connection.CreateCommand();
|
||||||
|
command.CommandText = "SELECT RELEASE_LOCK(@lockName)";
|
||||||
|
command.CommandTimeout = 5;
|
||||||
|
AddKey(command, _key);
|
||||||
|
await command.ExecuteScalarAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 非池化物理连接关闭会释放会话锁;释放错误不覆盖业务原异常。
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务数据操作权限校验。
|
||||||
|
/// 普通人事可查看全部业务数据,但只能操作本人创建的数据;拥有模块“管理全部”按钮权限的人员可操作全部数据。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingDataPermissionService : IOnboardingDataPermissionService
|
||||||
|
{
|
||||||
|
private readonly ISysUserService _sysUserService;
|
||||||
|
|
||||||
|
public OnboardingDataPermissionService(ISysUserService sysUserService)
|
||||||
|
{
|
||||||
|
_sysUserService = sysUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanManage(long? createUserId, string manageAllPermissionCode)
|
||||||
|
{
|
||||||
|
if (UserManager.SuperAdmin)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (createUserId.HasValue && createUserId.Value == UserManager.UserId)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var buttonCodes = await _sysUserService.GetButtonCodeList(UserManager.UserId);
|
||||||
|
return buttonCodes.Contains(manageAllPermissionCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureCanManage(long? createUserId, string manageAllPermissionCode, string dataName)
|
||||||
|
{
|
||||||
|
if (!await CanManage(createUserId, manageAllPermissionCode))
|
||||||
|
throw Oops.Bah($"只能操作本人创建的{dataName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureCanManage(
|
||||||
|
IEnumerable<long?> createUserIds,
|
||||||
|
string manageAllPermissionCode,
|
||||||
|
string dataName)
|
||||||
|
{
|
||||||
|
var owners = createUserIds.Distinct().ToList();
|
||||||
|
if (owners.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (UserManager.SuperAdmin || owners.All(id => id.HasValue && id.Value == UserManager.UserId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var buttonCodes = await _sysUserService.GetButtonCodeList(UserManager.UserId);
|
||||||
|
if (!buttonCodes.Contains(manageAllPermissionCode))
|
||||||
|
throw Oops.Bah($"所选数据中包含他人创建的{dataName},不能操作");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使同一异步调用链可重入,独立请求仍须取得外部互斥锁。
|
||||||
|
/// 不在 Acquire 中设置 AsyncLocal,避免值不能流回调用方的问题。
|
||||||
|
/// </summary>
|
||||||
|
internal static class OnboardingOperationGate
|
||||||
|
{
|
||||||
|
private sealed record Frame(string Key, Frame Parent);
|
||||||
|
private static readonly AsyncLocal<Frame> Held = new();
|
||||||
|
|
||||||
|
internal static async Task<T> Run<T>(
|
||||||
|
string key,
|
||||||
|
Func<Task<IAsyncDisposable>> acquire,
|
||||||
|
Func<Task<T>> action)
|
||||||
|
{
|
||||||
|
for (var frame = Held.Value; frame != null; frame = frame.Parent)
|
||||||
|
if (frame.Key == key)
|
||||||
|
return await action();
|
||||||
|
|
||||||
|
await using var lease = await acquire();
|
||||||
|
var previous = Held.Value;
|
||||||
|
Held.Value = new Frame(key, previous);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await action();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Held.Value = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务按钮权限编码。
|
||||||
|
/// </summary>
|
||||||
|
public static class OnboardingPermissionCodes
|
||||||
|
{
|
||||||
|
public const string SetupManageAll = "onboardingSetupManageAll";
|
||||||
|
public const string EntryConfigManageAll = "onboardingEntryConfigManageAll";
|
||||||
|
public const string ContractManageAll = "onboardingContractManageAll";
|
||||||
|
public const string TaskManageAll = "onboardingTaskManageAll";
|
||||||
|
public const string EmployeeManageAll = "onboardingEmployeeManageAll";
|
||||||
|
}
|
||||||
@@ -1,25 +1,103 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务公司分页查询参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingCompanyPageInput : BasePageInput
|
public class OnboardingCompanyPageInput : BasePageInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 公司业务编码。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string CompanyCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公司名称。
|
||||||
|
/// </summary>
|
||||||
public virtual string CompanyName { get; set; }
|
public virtual string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态或状态筛选条件。
|
||||||
|
/// </summary>
|
||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增入职业务公司参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingCompanyAddInput : ObCompany
|
public class OnboardingCompanyAddInput : ObCompany
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 公司业务编码。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "公司编码不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "公司编码不能为空")]
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(50, ErrorMessage = "公司编码不能超过50个字符")]
|
||||||
public override string CompanyCode { get; set; }
|
public override string CompanyCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公司名称。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "公司名称不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "公司名称不能为空")]
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(50, ErrorMessage = "公司名称不能超过50个字符")]
|
||||||
public override string CompanyName { get; set; }
|
public override string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同甲方完整法定名称。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "合同甲方全称不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "合同甲方全称不能为空")]
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(100, ErrorMessage = "合同甲方全称不能超过100个字符")]
|
||||||
public override string LegalName { get; set; }
|
public override string LegalName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一社会信用代码。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(18, ErrorMessage = "统一社会信用代码不能超过18位")]
|
||||||
|
public override string UnifiedSocialCreditCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 法定代表人姓名。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(20, ErrorMessage = "法定代表人不能超过20个字符")]
|
||||||
|
public override string LegalRepresentative { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工商注册地址。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(150, ErrorMessage = "注册地址不能超过150个字符")]
|
||||||
|
public override string RegisteredAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 公司联系电话。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(20, ErrorMessage = "联系电话不能超过20个字符")]
|
||||||
|
public override string ContactPhone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁默认业务分类 Id;每个签约公司独立配置。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(19, ErrorMessage = "契约锁业务分类Id不能超过19位数字")]
|
||||||
|
public override string QiyuesuoCategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁企业印章 Id;每个签约公司独立配置。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(19, ErrorMessage = "契约锁印章Id不能超过19位数字")]
|
||||||
|
public override string QiyuesuoSealId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(300, ErrorMessage = "备注不能超过300个字符")]
|
||||||
|
public override string Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑入职业务公司参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingCompanyEditInput : OnboardingCompanyAddInput
|
public class OnboardingCompanyEditInput : OnboardingCompanyAddInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||||
public override long Id { get; set; }
|
public override long Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义入职业务公司业务能力。
|
||||||
|
/// </summary>
|
||||||
public interface IOnboardingCompanyService : ITransient
|
public interface IOnboardingCompanyService : ITransient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询入职业务公司。
|
||||||
|
/// </summary>
|
||||||
Task<SqlSugarPagedList<ObCompany>> Page(OnboardingCompanyPageInput input);
|
Task<SqlSugarPagedList<ObCompany>> Page(OnboardingCompanyPageInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 查询全部已启用的入职业务公司。
|
||||||
|
/// </summary>
|
||||||
Task<List<ObCompany>> ListEnabled();
|
Task<List<ObCompany>> ListEnabled();
|
||||||
|
/// <summary>
|
||||||
|
/// 查询公司详情。
|
||||||
|
/// </summary>
|
||||||
Task<ObCompany> Detail(BaseIdInput input);
|
Task<ObCompany> Detail(BaseIdInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 新增入职业务公司。
|
||||||
|
/// </summary>
|
||||||
Task Add(OnboardingCompanyAddInput input);
|
Task Add(OnboardingCompanyAddInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑入职业务公司。
|
||||||
|
/// </summary>
|
||||||
Task Edit(OnboardingCompanyEditInput input);
|
Task Edit(OnboardingCompanyEditInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被岗位、任务或员工引用的公司。
|
||||||
|
/// </summary>
|
||||||
Task Delete(BaseIdListInput input);
|
Task Delete(BaseIdListInput input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现入职业务公司业务能力。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingCompanyService
|
public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingCompanyService
|
||||||
{
|
{
|
||||||
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
|
public OnboardingCompanyService(IOnboardingDataPermissionService dataPermission)
|
||||||
|
{
|
||||||
|
_dataPermission = dataPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询入职业务公司。
|
||||||
|
/// </summary>
|
||||||
public async Task<SqlSugarPagedList<ObCompany>> Page(OnboardingCompanyPageInput input)
|
public async Task<SqlSugarPagedList<ObCompany>> Page(OnboardingCompanyPageInput input)
|
||||||
{
|
{
|
||||||
return await Context.Queryable<ObCompany>()
|
return await Context.Queryable<ObCompany>()
|
||||||
.WhereIF(
|
.WhereIF(!string.IsNullOrWhiteSpace(input.CompanyCode), it => it.CompanyCode.Contains(input.CompanyCode))
|
||||||
!string.IsNullOrWhiteSpace(input.CompanyName),
|
.WhereIF(!string.IsNullOrWhiteSpace(input.CompanyName), it => it.CompanyName.Contains(input.CompanyName))
|
||||||
it => it.CompanyName.Contains(input.CompanyName) || it.CompanyCode.Contains(input.CompanyName))
|
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
||||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), input.GetSafeOrderBy("Id", "Status", "CreateTime", "UpdateTime", "CreateUser", "CreateUserId", "CompanyCode", "CompanyName", "LegalName", "ContactPhone"))
|
||||||
.OrderBy(it => it.CreateTime)
|
.OrderBy(it => it.CreateTime)
|
||||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询全部已启用的入职业务公司。
|
||||||
|
/// </summary>
|
||||||
public Task<List<ObCompany>> ListEnabled()
|
public Task<List<ObCompany>> ListEnabled()
|
||||||
{
|
{
|
||||||
return Context.Queryable<ObCompany>()
|
return Context.Queryable<ObCompany>()
|
||||||
@@ -22,34 +37,77 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询公司详情。
|
||||||
|
/// </summary>
|
||||||
public async Task<ObCompany> Detail(BaseIdInput input)
|
public async Task<ObCompany> Detail(BaseIdInput input)
|
||||||
{
|
{
|
||||||
return await GetByIdAsync(input.Id) ?? throw Oops.Bah("公司不存在");
|
return await GetByIdAsync(input.Id) ?? throw Oops.Bah("公司不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增入职业务公司。
|
||||||
|
/// </summary>
|
||||||
public async Task Add(OnboardingCompanyAddInput input)
|
public async Task Add(OnboardingCompanyAddInput input)
|
||||||
{
|
{
|
||||||
|
NormalizeAndValidateQiyuesuoConfig(input);
|
||||||
await EnsureCodeUnique(input.CompanyCode, 0);
|
await EnsureCodeUnique(input.CompanyCode, 0);
|
||||||
await InsertAsync(input.Adapt<ObCompany>());
|
await InsertAsync(input.Adapt<ObCompany>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑入职业务公司。
|
||||||
|
/// </summary>
|
||||||
public async Task Edit(OnboardingCompanyEditInput input)
|
public async Task Edit(OnboardingCompanyEditInput input)
|
||||||
{
|
{
|
||||||
await Detail(new BaseIdInput { Id = input.Id });
|
NormalizeAndValidateQiyuesuoConfig(input);
|
||||||
|
var company = await Detail(new BaseIdInput { Id = input.Id });
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
company.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.SetupManageAll,
|
||||||
|
"公司");
|
||||||
await EnsureCodeUnique(input.CompanyCode, input.Id);
|
await EnsureCodeUnique(input.CompanyCode, input.Id);
|
||||||
await UpdateAsync(input.Adapt<ObCompany>());
|
await UpdateAsync(input.Adapt<ObCompany>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被岗位、任务或员工引用的公司。
|
||||||
|
/// </summary>
|
||||||
public async Task Delete(BaseIdListInput input)
|
public async Task Delete(BaseIdListInput input)
|
||||||
{
|
{
|
||||||
if (input.Ids.Count == 0)
|
if (input.Ids.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var companies = await Context.Queryable<ObCompany>()
|
||||||
|
.Where(item => input.Ids.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (companies.Count != input.Ids.Distinct().Count())
|
||||||
|
throw Oops.Bah("公司不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
companies.Select(item => item.CreateUserId),
|
||||||
|
OnboardingPermissionCodes.SetupManageAll,
|
||||||
|
"公司");
|
||||||
|
|
||||||
var hasPositions = await Context.Queryable<ObPosition>()
|
var hasPositions = await Context.Queryable<ObPosition>()
|
||||||
.AnyAsync(it => input.Ids.Contains(it.CompanyId));
|
.AnyAsync(it => input.Ids.Contains(it.CompanyId));
|
||||||
if (hasPositions)
|
if (hasPositions)
|
||||||
throw Oops.Bah("公司下仍有岗位,不能删除");
|
throw Oops.Bah("公司下仍有岗位,不能删除");
|
||||||
|
|
||||||
|
var hasDepartments = await Context.Queryable<ObDepartment>()
|
||||||
|
.AnyAsync(item => input.Ids.Contains(item.CompanyId));
|
||||||
|
if (hasDepartments)
|
||||||
|
throw Oops.Bah("公司下仍有部门,不能删除");
|
||||||
|
|
||||||
|
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplate>()
|
||||||
|
.AnyAsync(item => input.Ids.Contains(item.CompanyId));
|
||||||
|
if (hasOnboardingTemplates)
|
||||||
|
throw Oops.Bah("公司已被入职模板引用,不能删除");
|
||||||
|
|
||||||
|
var hasContractTemplates = await Context.Queryable<ObContractTemplate>()
|
||||||
|
.AnyAsync(item => item.CompanyId.HasValue && input.Ids.Contains(item.CompanyId.Value));
|
||||||
|
if (hasContractTemplates)
|
||||||
|
throw Oops.Bah("公司已被合同模板引用,不能删除");
|
||||||
|
|
||||||
var hasTasks = await Context.Queryable<ObOnboardingTask>()
|
var hasTasks = await Context.Queryable<ObOnboardingTask>()
|
||||||
.AnyAsync(it => input.Ids.Contains(it.CompanyId));
|
.AnyAsync(it => input.Ids.Contains(it.CompanyId));
|
||||||
if (hasTasks)
|
if (hasTasks)
|
||||||
@@ -63,6 +121,36 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
|
|||||||
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
|
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void NormalizeAndValidateQiyuesuoConfig(ObCompany company)
|
||||||
|
{
|
||||||
|
company.CompanyCode = OnboardingCodeValidator.Normalize(company.CompanyCode, "公司编码", 50);
|
||||||
|
company.CompanyName = company.CompanyName?.Trim();
|
||||||
|
company.LegalName = company.LegalName?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(company.CompanyName))
|
||||||
|
throw Oops.Bah("公司名称不能为空");
|
||||||
|
|
||||||
|
company.QiyuesuoCategoryId = company.QiyuesuoCategoryId?.Trim();
|
||||||
|
company.QiyuesuoSealId = company.QiyuesuoSealId?.Trim();
|
||||||
|
|
||||||
|
var hasCategoryId = !string.IsNullOrWhiteSpace(company.QiyuesuoCategoryId);
|
||||||
|
var hasSealId = !string.IsNullOrWhiteSpace(company.QiyuesuoSealId);
|
||||||
|
if (hasCategoryId != hasSealId)
|
||||||
|
throw Oops.Bah("契约锁默认业务分类 Id 和企业印章 Id 必须同时填写");
|
||||||
|
if (!hasCategoryId)
|
||||||
|
return;
|
||||||
|
if (!company.QiyuesuoCategoryId.All(char.IsDigit)
|
||||||
|
|| !company.QiyuesuoSealId.All(char.IsDigit))
|
||||||
|
{
|
||||||
|
throw Oops.Bah("契约锁默认业务分类 Id 和企业印章 Id 只能填写纯数字");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!long.TryParse(company.QiyuesuoCategoryId, out _)
|
||||||
|
|| !long.TryParse(company.QiyuesuoSealId, out _))
|
||||||
|
{
|
||||||
|
throw Oops.Bah("契约锁默认业务分类 Id 和企业印章 Id 超出有效数字范围(最大支持19位长整数)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task EnsureCodeUnique(string code, long id)
|
private async Task EnsureCodeUnique(string code, long id)
|
||||||
{
|
{
|
||||||
var exists = await Context.Queryable<ObCompany>()
|
var exists = await Context.Queryable<ObCompany>()
|
||||||
|
|||||||
+66
@@ -1,7 +1,73 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成任务合同文件参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingGenerateContractInput
|
public class OnboardingGenerateContractInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 任务合同 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "任务合同Id不能为空")]
|
[IdNotNull(ErrorMessage = "任务合同Id不能为空")]
|
||||||
public long TaskContractId { get; set; }
|
public long TaskContractId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事在创建入职任务前下载合同预览的参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractPreviewInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||||
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "部门Id不能为空")]
|
||||||
|
public long DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职岗位 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
|
||||||
|
public long PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前选择的入职模板 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "入职模板Id不能为空")]
|
||||||
|
public long OnboardingTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次需要预览的合同模板版本 Id;批量预览时无需传入。
|
||||||
|
/// </summary>
|
||||||
|
public long? ContractTemplateVersionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事当前在创建表单中填写的合同变量。
|
||||||
|
/// </summary>
|
||||||
|
public List<OnboardingInitialContractVariableInput> ContractVariables
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建任务前生成的临时 DOCX 预览文件。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractPreviewOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DOCX 文件内容,仅通过当前请求返回,不保存到文件表或 OSS。
|
||||||
|
/// </summary>
|
||||||
|
public byte[] FileBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 浏览器下载时使用的文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
+28
-1
@@ -1,2 +1,29 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
public interface IOnboardingContractDocumentService : ITransient { Task<long> Generate(OnboardingGenerateContractInput input); }
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义入职合同文件生成能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingContractDocumentService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建入职任务前,根据人事当前填写的参数生成临时 DOCX 预览。
|
||||||
|
/// </summary>
|
||||||
|
Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
|
||||||
|
OnboardingContractPreviewInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建入职任务前,按模板顺序生成全部合同预览并打包为 ZIP。
|
||||||
|
/// </summary>
|
||||||
|
Task<OnboardingContractPreviewOutput> PreviewAllBeforeCreate(
|
||||||
|
OnboardingContractPreviewInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据已审核通过的任务合同生成并保存 DOCX 文件。
|
||||||
|
/// </summary>
|
||||||
|
Task<long> Generate(OnboardingGenerateContractInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工公开入口已验证一次性任务链接后,生成该链接所属的合同文件。
|
||||||
|
/// </summary>
|
||||||
|
Task<long> GenerateForPublicAccess(long taskContractId, long taskId);
|
||||||
|
}
|
||||||
|
|||||||
+410
-50
@@ -1,26 +1,350 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现入职合同预览和归档 DOCX 文件生成能力。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingContractDocumentService : IOnboardingContractDocumentService
|
public class OnboardingContractDocumentService : IOnboardingContractDocumentService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarClient _db;
|
private readonly SqlSugarScope _db;
|
||||||
private readonly IFileService _fileService;
|
private readonly IFileService _fileService;
|
||||||
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
public OnboardingContractDocumentService(ISqlSugarClient db, IFileService fileService)
|
public OnboardingContractDocumentService(
|
||||||
|
IFileService fileService,
|
||||||
|
IOnboardingDataPermissionService dataPermission)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = DbContext.DB;
|
||||||
_fileService = fileService;
|
_fileService = fileService;
|
||||||
|
_dataPermission = dataPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在任务创建前生成指定合同的临时 DOCX,仅通过响应返回,不写文件表和 OSS。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
|
||||||
|
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>()
|
||||||
|
.FirstAsync(item => item.Id == input.CompanyId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE)
|
||||||
|
?? throw Oops.Bah("公司不存在或未启用");
|
||||||
|
|
||||||
|
var departmentExists = await _db.Queryable<ObDepartment>()
|
||||||
|
.AnyAsync(item => item.Id == input.DepartmentId
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE);
|
||||||
|
if (!departmentExists)
|
||||||
|
throw Oops.Bah("部门不存在、未启用或不属于该公司");
|
||||||
|
|
||||||
|
var positionExists = await _db.Queryable<ObPosition>()
|
||||||
|
.AnyAsync(item => item.Id == input.PositionId
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.DepartmentId == input.DepartmentId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE);
|
||||||
|
if (!positionExists)
|
||||||
|
throw Oops.Bah("岗位不存在、未启用或不属于该部门/公司");
|
||||||
|
|
||||||
|
var onboardingTemplate = await _db.Queryable<ObOnboardingTemplate>()
|
||||||
|
.FirstAsync(item => item.Id == input.OnboardingTemplateId
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.DepartmentId == input.DepartmentId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE)
|
||||||
|
?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司、部门");
|
||||||
|
|
||||||
|
var supportsPosition = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||||
|
.AnyAsync(item => item.OnboardingTemplateId == onboardingTemplate.Id
|
||||||
|
&& item.PositionId == input.PositionId);
|
||||||
|
if (!supportsPosition)
|
||||||
|
throw Oops.Bah("所选入职模板不适用于该岗位");
|
||||||
|
|
||||||
|
var contractConfigs = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||||
|
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.OrderBy(item => item.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
if (contractConfigs.Count == 0)
|
||||||
|
throw Oops.Bah("入职模板尚未配置合同文件");
|
||||||
|
|
||||||
|
var duplicateVersion = contractConfigs
|
||||||
|
.GroupBy(item => item.ContractTemplateVersionId)
|
||||||
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
|
if (duplicateVersion != null)
|
||||||
|
throw Oops.Bah("入职模板中存在重复的合同版本,请先调整模板配置");
|
||||||
|
|
||||||
|
var templateIds = contractConfigs
|
||||||
|
.Select(item => item.ContractTemplateId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
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 supportedTemplateCount = await _db.Queryable<ObContractTemplatePosition>()
|
||||||
|
.CountAsync(item => templateIds.Contains(item.ContractTemplateId)
|
||||||
|
&& item.PositionId == input.PositionId);
|
||||||
|
if (supportedTemplateCount != templateIds.Count)
|
||||||
|
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 now = DateTime.Now;
|
||||||
|
var versionsValid = versions.Count == versionIds.Count
|
||||||
|
&& contractConfigs.All(config => versions.Any(version =>
|
||||||
|
version.Id == config.ContractTemplateVersionId
|
||||||
|
&& version.TemplateId == config.ContractTemplateId
|
||||||
|
&& ContractTemplateVersionValidity.IsAvailable(version, now)));
|
||||||
|
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>()
|
||||||
|
.Where(item => item.TemplateVersionId == version.Id)
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.ToListAsync();
|
||||||
|
var duplicateDefinition = definitions
|
||||||
|
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
|
if (duplicateDefinition != null)
|
||||||
|
throw Oops.Bah($"合同变量“{duplicateDefinition.Key}”存在重复定义");
|
||||||
|
|
||||||
|
var inputs = (input.ContractVariables
|
||||||
|
?? new List<OnboardingInitialContractVariableInput>())
|
||||||
|
.Where(item => item.ContractTemplateVersionId == version.Id)
|
||||||
|
.ToList();
|
||||||
|
var editableDefinitions = definitions
|
||||||
|
.Where(IsHrEditableDefinition)
|
||||||
|
.ToList();
|
||||||
|
foreach (var submitted in inputs)
|
||||||
|
{
|
||||||
|
var definitionExists = editableDefinitions.Any(definition =>
|
||||||
|
string.Equals(
|
||||||
|
definition.VariableCode,
|
||||||
|
submitted.VariableCode,
|
||||||
|
StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (!definitionExists)
|
||||||
|
throw Oops.Bah($"合同变量“{submitted.VariableCode}”不存在或不允许人事填写");
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingRequired = editableDefinitions.FirstOrDefault(definition =>
|
||||||
|
{
|
||||||
|
if (!definition.Required)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var submitted = inputs.FirstOrDefault(item =>
|
||||||
|
string.Equals(
|
||||||
|
item.VariableCode,
|
||||||
|
definition.VariableCode,
|
||||||
|
StringComparison.OrdinalIgnoreCase));
|
||||||
|
return string.IsNullOrWhiteSpace(
|
||||||
|
submitted?.VariableValue ?? definition.DefaultValue);
|
||||||
|
});
|
||||||
|
if (missingRequired != null)
|
||||||
|
throw Oops.Bah($"请填写合同变量“{missingRequired.VariableName}”");
|
||||||
|
|
||||||
|
var previewTask = new ObOnboardingTask
|
||||||
|
{
|
||||||
|
TaskNo = "【发起后自动生成】",
|
||||||
|
CompanyId = input.CompanyId,
|
||||||
|
DepartmentId = input.DepartmentId,
|
||||||
|
PositionId = input.PositionId,
|
||||||
|
OnboardingTemplateId = input.OnboardingTemplateId
|
||||||
|
};
|
||||||
|
var emptyDynamicValues = new Dictionary<string, string>(
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
var values = definitions.ToDictionary(
|
||||||
|
definition => definition.VariableCode,
|
||||||
|
definition =>
|
||||||
|
{
|
||||||
|
var submitted = inputs.FirstOrDefault(item =>
|
||||||
|
string.Equals(
|
||||||
|
item.VariableCode,
|
||||||
|
definition.VariableCode,
|
||||||
|
StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (submitted != null)
|
||||||
|
return submitted.VariableValue ?? string.Empty;
|
||||||
|
|
||||||
|
return OnboardingContractSnapshotService.ResolveValue(
|
||||||
|
definition,
|
||||||
|
null,
|
||||||
|
emptyDynamicValues,
|
||||||
|
context.Company,
|
||||||
|
previewTask,
|
||||||
|
true)
|
||||||
|
?? string.Empty;
|
||||||
|
},
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var source = await _fileService.Download(
|
||||||
|
new BaseIdInput { Id = version.SourceDocxFileId })
|
||||||
|
?? throw Oops.Bah("无法读取合同源文件");
|
||||||
|
await using var sourceStream = source.FileStream;
|
||||||
|
using var sourceBytes = new MemoryStream();
|
||||||
|
await sourceStream.CopyToAsync(sourceBytes);
|
||||||
|
|
||||||
|
return new OnboardingContractPreviewOutput
|
||||||
|
{
|
||||||
|
FileBytes = DocxTemplateProcessor.Render(
|
||||||
|
sourceBytes.ToArray(),
|
||||||
|
values),
|
||||||
|
FileName = BuildPreviewFileName(contractTemplate.TemplateName, fileOrder)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
|
||||||
|
/// </summary>
|
||||||
public async Task<long> Generate(OnboardingGenerateContractInput input)
|
public async Task<long> Generate(OnboardingGenerateContractInput input)
|
||||||
|
{
|
||||||
|
return await OnboardingConcurrency.RunContract(_db, input.TaskContractId, () => GenerateCore(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<long> GenerateCore(OnboardingGenerateContractInput input)
|
||||||
{
|
{
|
||||||
var contract = await _db.Queryable<ObTaskContract>().InSingleAsync(input.TaskContractId)
|
var contract = await _db.Queryable<ObTaskContract>().InSingleAsync(input.TaskContractId)
|
||||||
?? throw Oops.Bah("任务合同不存在");
|
?? throw Oops.Bah("任务合同不存在");
|
||||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(contract.TaskId)
|
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(contract.TaskId)
|
||||||
?? throw Oops.Bah("入职任务不存在");
|
?? throw Oops.Bah("入职任务不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
task.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.TaskManageAll,
|
||||||
|
"入职任务");
|
||||||
|
|
||||||
if (task.TaskStatus != OnboardingTaskStatus.Approved)
|
return await GenerateCore(contract, task);
|
||||||
throw Oops.Bah("仅审核通过的任务可生成归档合同");
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<long> GenerateForPublicAccess(long taskContractId, long taskId)
|
||||||
|
{
|
||||||
|
return await OnboardingConcurrency.RunTask(_db, taskId, () => GenerateForPublicAccessCore(taskContractId, taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<long> GenerateForPublicAccessCore(long taskContractId, long taskId)
|
||||||
|
{
|
||||||
|
var contract = await _db.Queryable<ObTaskContract>()
|
||||||
|
.FirstAsync(item => item.Id == taskContractId && item.TaskId == taskId)
|
||||||
|
?? throw Oops.Bah("合同不存在");
|
||||||
|
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId)
|
||||||
|
?? throw Oops.Bah("入职任务不存在");
|
||||||
|
|
||||||
|
return await GenerateCore(contract, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行已经完成调用方权限或公开链接归属校验的合同生成逻辑。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<long> GenerateCore(ObTaskContract contract, ObOnboardingTask task)
|
||||||
|
{
|
||||||
|
var statusAllowsGeneration = task.TaskStatus == OnboardingTaskStatus.PendingFill
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Rejected
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Reviewing
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Approved
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Archived;
|
||||||
|
if (!statusAllowsGeneration)
|
||||||
|
throw Oops.Bah("当前任务状态不能生成合同");
|
||||||
if (contract.GeneratedDocxFileId.HasValue)
|
if (contract.GeneratedDocxFileId.HasValue)
|
||||||
return contract.GeneratedDocxFileId.Value;
|
return contract.GeneratedDocxFileId.Value;
|
||||||
|
|
||||||
@@ -28,70 +352,106 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
?? throw Oops.Bah("合同版本不存在");
|
?? throw Oops.Bah("合同版本不存在");
|
||||||
var source = await _db.Queryable<SysFile>().InSingleAsync(version.SourceDocxFileId)
|
var source = await _db.Queryable<SysFile>().InSingleAsync(version.SourceDocxFileId)
|
||||||
?? throw Oops.Bah("合同源文件不存在");
|
?? throw Oops.Bah("合同源文件不存在");
|
||||||
var variables = await _db.Queryable<ObTaskContractVariable>()
|
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||||
.Where(x => x.TaskContractId == contract.Id)
|
.Where(item => item.TemplateVersionId == version.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
var variables = await _db.Queryable<ObTaskContractVariable>()
|
||||||
|
.Where(item => item.TaskContractId == contract.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
var duplicateVariable = variables
|
||||||
|
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
|
if (duplicateVariable != null)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同变量快照“{duplicateVariable.Key}”存在重复数据,请联系管理员修复后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
var missing = definitions
|
||||||
|
.Where(item => item.Required)
|
||||||
|
.FirstOrDefault(definition => string.IsNullOrWhiteSpace(
|
||||||
|
variables.FirstOrDefault(variable => variable.VariableCode == definition.VariableCode)?.VariableValue));
|
||||||
|
if (missing != null)
|
||||||
|
throw Oops.Bah($"合同“{contract.ContractName}”的变量“{missing.VariableName}”尚未填写,暂时不能预览合同");
|
||||||
|
|
||||||
var sourceResult = await _fileService.Download(new BaseIdInput { Id = source.Id })
|
var sourceResult = await _fileService.Download(new BaseIdInput { Id = source.Id })
|
||||||
?? throw Oops.Bah("无法读取合同源文件");
|
?? throw Oops.Bah("无法读取合同源文件");
|
||||||
|
|
||||||
await using var sourceStream = sourceResult.FileStream;
|
await using var sourceStream = sourceResult.FileStream;
|
||||||
using var sourceBytes = new MemoryStream();
|
using var sourceBytes = new MemoryStream();
|
||||||
await sourceStream.CopyToAsync(sourceBytes);
|
await sourceStream.CopyToAsync(sourceBytes);
|
||||||
|
|
||||||
var rendered = ReplaceDocxText(
|
var rendered = DocxTemplateProcessor.Render(
|
||||||
sourceBytes.ToArray(),
|
sourceBytes.ToArray(),
|
||||||
variables.ToDictionary(x => x.VariableCode, x => x.VariableValue ?? string.Empty));
|
variables.ToDictionary(
|
||||||
var name = $"{task.TaskNo}_{contract.ContractName}.docx";
|
item => item.VariableCode,
|
||||||
var fileId = await _fileService.StoreGeneratedFile(source.Engine, name, rendered);
|
item => item.VariableValue ?? string.Empty,
|
||||||
|
StringComparer.OrdinalIgnoreCase));
|
||||||
|
var generatedAt = DateTime.Now;
|
||||||
|
var employeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||||
|
.FirstAsync(item => item.TaskId == task.Id);
|
||||||
|
var employeeName = string.IsNullOrWhiteSpace(employeeInfo?.EmployeeName)
|
||||||
|
? "新员工"
|
||||||
|
: employeeInfo.EmployeeName.Trim();
|
||||||
|
var docType = string.IsNullOrWhiteSpace(contract.ContractType)
|
||||||
|
? (string.IsNullOrWhiteSpace(contract.ContractName) ? "签署材料" : contract.ContractName.Trim())
|
||||||
|
: contract.ContractType.Trim();
|
||||||
|
var invalidChars = global::System.IO.Path.GetInvalidFileNameChars();
|
||||||
|
var safeEmployeeName = new string(employeeName.Select(character => invalidChars.Contains(character) ? '_' : character).ToArray());
|
||||||
|
var safeDocType = new string(docType.Select(character => invalidChars.Contains(character) ? '_' : character).ToArray());
|
||||||
|
// 按照规范:编号_员工姓名_文档类型.docx
|
||||||
|
var name = $"{task.TaskNo}_{safeEmployeeName}_{safeDocType}.docx";
|
||||||
|
var fileId = await _fileService.StoreGeneratedFile(
|
||||||
|
SysDictConst.FILE_ENGINE_ALIYUN_OSS,
|
||||||
|
name,
|
||||||
|
rendered);
|
||||||
|
|
||||||
contract.GeneratedDocxFileId = fileId;
|
contract.GeneratedDocxFileId = fileId;
|
||||||
contract.GeneratedAt = DateTime.Now;
|
contract.GeneratedAt = generatedAt;
|
||||||
|
contract.ContractStatus = task.TaskStatus == OnboardingTaskStatus.Approved
|
||||||
|
? TaskContractStatus.OfflinePendingSign
|
||||||
|
: TaskContractStatus.PendingConfirm;
|
||||||
await _db.Updateable(contract).ExecuteCommandAsync();
|
await _db.Updateable(contract).ExecuteCommandAsync();
|
||||||
|
|
||||||
return fileId;
|
return fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] ReplaceDocxText(byte[] docx, Dictionary<string, string> values)
|
/// <summary>
|
||||||
|
/// 判断变量是否允许人事在发起任务前填写。
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsHrEditableDefinition(ObTemplateVariable definition)
|
||||||
{
|
{
|
||||||
using var input = new MemoryStream(docx);
|
return definition.HrEditable
|
||||||
using var archive = new global::System.IO.Compression.ZipArchive(
|
&& (definition.VariableType == "hr"
|
||||||
input,
|
|| definition.VariableType == ContractVariableType.Contract
|
||||||
global::System.IO.Compression.ZipArchiveMode.Read);
|
|| definition.VariableType == ContractVariableType.Company
|
||||||
using var output = new MemoryStream();
|
|| definition.VariableCode.StartsWith(
|
||||||
using var target = new global::System.IO.Compression.ZipArchive(
|
"hr.",
|
||||||
output,
|
StringComparison.OrdinalIgnoreCase));
|
||||||
global::System.IO.Compression.ZipArchiveMode.Create,
|
}
|
||||||
true);
|
|
||||||
|
|
||||||
foreach (var entry in archive.Entries)
|
/// <summary>
|
||||||
{
|
/// 生成兼容 Windows 和 Linux 的合同预览下载文件名。
|
||||||
var next = target.CreateEntry(entry.FullName, global::System.IO.Compression.CompressionLevel.Optimal);
|
/// </summary>
|
||||||
using var reader = entry.Open();
|
private static string BuildPreviewFileName(string contractName, int? fileOrder = null)
|
||||||
using var writer = next.Open();
|
{
|
||||||
|
var invalidChars = global::System.IO.Path.GetInvalidFileNameChars();
|
||||||
|
var safeName = new string((contractName ?? "合同")
|
||||||
|
.Select(character => invalidChars.Contains(character) ? '_' : character)
|
||||||
|
.ToArray());
|
||||||
|
var orderPrefix = fileOrder.HasValue ? $"{fileOrder.Value:D2}_" : string.Empty;
|
||||||
|
return $"{orderPrefix}{safeName}_人事预览_{DateTime.Now:yyyyMMddHHmmss}.docx";
|
||||||
|
}
|
||||||
|
|
||||||
var isWordTextXml = entry.FullName == "word/document.xml"
|
/// <summary>
|
||||||
|| ((entry.FullName.StartsWith("word/header") || entry.FullName.StartsWith("word/footer"))
|
/// 创建任务前预览合同所需的已校验数据。
|
||||||
&& entry.FullName.EndsWith(".xml"));
|
/// </summary>
|
||||||
if (!isWordTextXml)
|
private sealed class ContractPreviewContext
|
||||||
{
|
{
|
||||||
reader.CopyTo(writer);
|
public ObCompany Company { get; init; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var textReader = new StreamReader(reader);
|
public List<ObOnboardingTemplateContract> ContractConfigs { get; init; }
|
||||||
var document = global::System.Xml.Linq.XDocument.Parse(textReader.ReadToEnd());
|
|
||||||
var word = global::System.Xml.Linq.XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main");
|
|
||||||
|
|
||||||
foreach (var node in document.Descendants(word + "t"))
|
public List<ObContractTemplate> ContractTemplates { get; init; }
|
||||||
{
|
|
||||||
foreach (var item in values)
|
|
||||||
node.Value = node.Value.Replace("{{" + item.Key + "}}", item.Value ?? string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var textWriter = new StreamWriter(writer);
|
public List<ObContractTemplateVersion> Versions { get; init; }
|
||||||
textWriter.Write(document.ToString(global::System.Xml.Linq.SaveOptions.DisableFormatting));
|
|
||||||
}
|
|
||||||
|
|
||||||
return output.ToArray();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -1,7 +1,13 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义任务合同变量快照业务能力。
|
||||||
|
/// </summary>
|
||||||
public interface IOnboardingContractSnapshotService : ITransient
|
public interface IOnboardingContractSnapshotService : ITransient
|
||||||
{
|
{
|
||||||
/// <summary>根据任务、公司与候选人资料刷新自动变量;人事已手工填写的变量不会被覆盖。</summary>
|
/// <summary>根据任务、公司与候选人资料刷新自动变量;人事已手工填写的变量不会被覆盖。</summary>
|
||||||
Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null);
|
/// <summary>
|
||||||
|
/// 根据任务、公司和候选人资料构建或刷新合同变量快照。
|
||||||
|
/// </summary>
|
||||||
|
Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null, bool invalidateGeneratedPreview = false);
|
||||||
}
|
}
|
||||||
+207
-41
@@ -1,25 +1,57 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
/// <summary>冻结任务合同变量;DOCX 生成阶段只读取本快照,不再读取模板实时值。</summary>
|
/// <summary>
|
||||||
|
/// 构建任务合同变量快照,合同生成阶段只读取本快照。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingContractSnapshotService : IOnboardingContractSnapshotService
|
public class OnboardingContractSnapshotService : IOnboardingContractSnapshotService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarClient _db;
|
private readonly SqlSugarScope _db;
|
||||||
public OnboardingContractSnapshotService(ISqlSugarClient db) => _db = db;
|
|
||||||
|
|
||||||
public async Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null)
|
public OnboardingContractSnapshotService()
|
||||||
{
|
{
|
||||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId) ?? throw Oops.Bah("入职任务不存在");
|
_db = DbContext.DB;
|
||||||
var company = await _db.Queryable<ObCompany>().InSingleAsync(task.CompanyId) ?? throw Oops.Bah("入职公司不存在");
|
}
|
||||||
employeeInfo ??= await _db.Queryable<ObTaskEmployeeInfo>().FirstAsync(x => x.TaskId == taskId);
|
|
||||||
var contracts = await _db.Queryable<ObTaskContract>().Where(x => x.TaskId == taskId).ToListAsync();
|
/// <summary>
|
||||||
|
/// 根据任务、公司、员工固定信息和动态补充字段构建合同变量快照。
|
||||||
|
/// </summary>
|
||||||
|
public async Task Build(
|
||||||
|
long taskId,
|
||||||
|
ObTaskEmployeeInfo employeeInfo = null,
|
||||||
|
bool invalidateGeneratedPreview = false)
|
||||||
|
{
|
||||||
|
await OnboardingConcurrency.RunTask(_db, taskId, () => BuildCore(taskId, employeeInfo, invalidateGeneratedPreview), transactional: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BuildCore(
|
||||||
|
long taskId,
|
||||||
|
ObTaskEmployeeInfo employeeInfo = null,
|
||||||
|
bool invalidateGeneratedPreview = false)
|
||||||
|
{
|
||||||
|
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId)
|
||||||
|
?? throw Oops.Bah("入职任务不存在");
|
||||||
|
var company = await _db.Queryable<ObCompany>().InSingleAsync(task.CompanyId)
|
||||||
|
?? throw Oops.Bah("入职公司不存在");
|
||||||
|
employeeInfo ??= await _db.Queryable<ObTaskEmployeeInfo>()
|
||||||
|
.FirstAsync(item => item.TaskId == taskId);
|
||||||
|
// 员工补充合同变量统一从 CustomVariablesJson 读取,不再访问已移除的旧动态字段表。
|
||||||
|
var dynamicValueMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var contracts = await _db.Queryable<ObTaskContract>()
|
||||||
|
.Where(item => item.TaskId == taskId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
foreach (var contract in contracts)
|
foreach (var contract in contracts)
|
||||||
{
|
{
|
||||||
if (contract.GeneratedDocxFileId.HasValue || contract.GeneratedPdfFileId.HasValue) throw Oops.Bah("合同已生成,不能重建变量快照");
|
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||||
var definitions = await _db.Queryable<ObTemplateVariable>().Where(x => x.TemplateVersionId == contract.TemplateVersionId).OrderBy(x => x.SortCode).ToListAsync();
|
.Where(item => item.TemplateVersionId == contract.TemplateVersionId)
|
||||||
var oldRows = await _db.Queryable<ObTaskContractVariable>().Where(x => x.TaskContractId == contract.Id).ToListAsync();
|
.OrderBy(item => item.SortCode)
|
||||||
|
.ToListAsync();
|
||||||
|
var oldRows = await _db.Queryable<ObTaskContractVariable>()
|
||||||
|
.Where(item => item.TaskContractId == contract.Id)
|
||||||
|
.ToListAsync();
|
||||||
var rows = definitions.Select(definition =>
|
var rows = definitions.Select(definition =>
|
||||||
{
|
{
|
||||||
var old = oldRows.FirstOrDefault(x => x.VariableCode == definition.VariableCode);
|
var old = oldRows.FirstOrDefault(item => item.VariableCode == definition.VariableCode);
|
||||||
var preserveManual = old?.IsManualOverride == true;
|
var preserveManual = old?.IsManualOverride == true;
|
||||||
return new ObTaskContractVariable
|
return new ObTaskContractVariable
|
||||||
{
|
{
|
||||||
@@ -29,54 +61,188 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
|
|||||||
VariableName = definition.VariableName,
|
VariableName = definition.VariableName,
|
||||||
VariableType = definition.VariableType,
|
VariableType = definition.VariableType,
|
||||||
DataType = definition.DataType,
|
DataType = definition.DataType,
|
||||||
VariableValue = preserveManual ? old.VariableValue : ResolveValue(definition, employeeInfo, company, task),
|
SortCode = definition.SortCode ?? 100,
|
||||||
SourceType = preserveManual ? ContractVariableSourceType.HrInput : GetSourceType(definition.VariableType),
|
VariableValue = preserveManual
|
||||||
|
? old.VariableValue
|
||||||
|
: ResolveValue(definition, employeeInfo, dynamicValueMap, company, task),
|
||||||
|
SourceType = preserveManual
|
||||||
|
? ContractVariableSourceType.HrInput
|
||||||
|
: GetSourceType(definition.VariableType),
|
||||||
IsManualOverride = preserveManual
|
IsManualOverride = preserveManual
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
await _db.Deleteable<ObTaskContractVariable>().Where(x => x.TaskContractId == contract.Id).ExecuteCommandAsync();
|
|
||||||
if (rows.Count > 0) await _db.Insertable(rows).ExecuteCommandAsync();
|
// 仅当参与合同渲染的变量编码或变量值发生变化时,才作废已生成文件。
|
||||||
|
// 重复保存完全相同的员工资料不应导致员工刚查看的合同突然失效。
|
||||||
|
var snapshotChanged = oldRows.Count != rows.Count
|
||||||
|
|| rows.Any(row =>
|
||||||
|
{
|
||||||
|
var old = oldRows.FirstOrDefault(item =>
|
||||||
|
item.VariableCode == row.VariableCode);
|
||||||
|
return old == null
|
||||||
|
|| !string.Equals(
|
||||||
|
old.VariableValue,
|
||||||
|
row.VariableValue,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
});
|
||||||
|
|
||||||
|
await _db.Deleteable<ObTaskContractVariable>()
|
||||||
|
.Where(item => item.TaskContractId == contract.Id)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
if (rows.Count > 0)
|
||||||
|
await _db.Insertable(rows).ExecuteCommandAsync();
|
||||||
|
|
||||||
|
if (invalidateGeneratedPreview
|
||||||
|
&& snapshotChanged
|
||||||
|
&& task.TaskStatus is OnboardingTaskStatus.PendingFill or OnboardingTaskStatus.Rejected)
|
||||||
|
{
|
||||||
|
contract.GeneratedDocxFileId = null;
|
||||||
|
contract.GeneratedPdfFileId = null;
|
||||||
|
contract.GeneratedAt = null;
|
||||||
|
contract.ReadAt = null;
|
||||||
|
contract.ConfirmedAt = null;
|
||||||
|
contract.ContractStatus = TaskContractStatus.PendingGenerate;
|
||||||
|
await _db.Updateable(contract).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetSourceType(string variableType) => variableType switch
|
private static string GetSourceType(string variableType)
|
||||||
{
|
{
|
||||||
ContractVariableType.Employee => ContractVariableSourceType.Employee,
|
return variableType switch
|
||||||
ContractVariableType.Company => ContractVariableSourceType.Company,
|
{
|
||||||
ContractVariableType.System => ContractVariableSourceType.System,
|
ContractVariableType.Employee => ContractVariableSourceType.Employee,
|
||||||
_ => ContractVariableSourceType.HrInput
|
ContractVariableType.Company => ContractVariableSourceType.Company,
|
||||||
};
|
ContractVariableType.System => ContractVariableSourceType.System,
|
||||||
|
_ => ContractVariableSourceType.HrInput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveValue(ObTemplateVariable definition, ObTaskEmployeeInfo info, ObCompany company, ObOnboardingTask task)
|
internal static string ResolveValue(
|
||||||
|
ObTemplateVariable definition,
|
||||||
|
ObTaskEmployeeInfo employeeInfo,
|
||||||
|
IReadOnlyDictionary<string, string> dynamicValues,
|
||||||
|
ObCompany company,
|
||||||
|
ObOnboardingTask task,
|
||||||
|
bool useEmployeePlaceholder = false)
|
||||||
{
|
{
|
||||||
|
var sourceField = GetSourceField(definition);
|
||||||
if (definition.VariableType == ContractVariableType.Employee)
|
if (definition.VariableType == ContractVariableType.Employee)
|
||||||
return GetPropertyValue(info, definition.EmployeeField) ?? definition.DefaultValue;
|
{
|
||||||
|
var fixedValue = GetPropertyValue(employeeInfo, ResolveEmployeeField(sourceField), definition.FormatRule);
|
||||||
|
if (!string.IsNullOrWhiteSpace(fixedValue))
|
||||||
|
return fixedValue;
|
||||||
|
|
||||||
|
// 检查员工自定义补充变量 JSON
|
||||||
|
if (!string.IsNullOrWhiteSpace(employeeInfo?.CustomVariablesJson))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = global::System.Text.Json.JsonDocument.Parse(employeeInfo.CustomVariablesJson);
|
||||||
|
if (doc.RootElement.TryGetProperty(sourceField, out var prop)
|
||||||
|
|| doc.RootElement.TryGetProperty(definition.VariableCode, out prop))
|
||||||
|
{
|
||||||
|
var str = prop.ValueKind switch
|
||||||
|
{
|
||||||
|
global::System.Text.Json.JsonValueKind.String => prop.GetString(),
|
||||||
|
global::System.Text.Json.JsonValueKind.Number => prop.ToString(),
|
||||||
|
global::System.Text.Json.JsonValueKind.True => bool.TrueString,
|
||||||
|
global::System.Text.Json.JsonValueKind.False => bool.FalseString,
|
||||||
|
global::System.Text.Json.JsonValueKind.Null => null,
|
||||||
|
_ => throw Oops.Bah($"员工补充变量“{definition.VariableName}”格式不正确")
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(str)) return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (global::System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("员工补充变量数据格式异常,请重新保存入职登记表后再生成合同");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dynamicValues.TryGetValue(sourceField, out var dynamicValue))
|
||||||
|
return dynamicValue;
|
||||||
|
|
||||||
|
return useEmployeePlaceholder
|
||||||
|
? $"【待员工填写:{definition.VariableName ?? definition.VariableCode}】"
|
||||||
|
: definition.DefaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (definition.VariableType == ContractVariableType.Company)
|
if (definition.VariableType == ContractVariableType.Company)
|
||||||
return GetPropertyValue(company, ResolveCompanyField(definition)) ?? definition.DefaultValue;
|
return GetPropertyValue(company, ResolveCompanyField(sourceField), definition.FormatRule) ?? definition.DefaultValue;
|
||||||
if (definition.VariableType == ContractVariableType.System && string.Equals(definition.VariableCode, "TASK_NO", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return task.TaskNo;
|
if (definition.VariableType == ContractVariableType.System)
|
||||||
|
{
|
||||||
|
return sourceField.ToUpperInvariant() switch
|
||||||
|
{
|
||||||
|
"TASKNO" or "TASK_NO" or "CONTRACTNO" or "CONTRACT_NO" => task.TaskNo,
|
||||||
|
"CURRENTDATE" or "CURRENT_DATE" or "SIGNDATE" or "SIGN_DATE" => DateTime.Now.ToString("yyyy年MM月dd日"),
|
||||||
|
_ => definition.DefaultValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return definition.DefaultValue;
|
return definition.DefaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetPropertyValue(object source, string propertyName)
|
private static string GetSourceField(ObTemplateVariable definition)
|
||||||
{
|
{
|
||||||
if (source == null || string.IsNullOrWhiteSpace(propertyName)) return null;
|
if (!string.IsNullOrWhiteSpace(definition.EmployeeField))
|
||||||
return source.GetType().GetProperty(propertyName, global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.IgnoreCase)?.GetValue(source)?.ToString();
|
return definition.EmployeeField;
|
||||||
|
var separatorIndex = definition.VariableCode?.IndexOf('.') ?? -1;
|
||||||
|
return separatorIndex >= 0
|
||||||
|
? definition.VariableCode[(separatorIndex + 1)..]
|
||||||
|
: definition.VariableCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveCompanyField(ObTemplateVariable definition)
|
private static string GetPropertyValue(object source, string propertyName, string formatRule = null)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(definition.EmployeeField)) return definition.EmployeeField;
|
if (source == null || string.IsNullOrWhiteSpace(propertyName))
|
||||||
return definition.VariableCode.ToUpperInvariant() switch
|
return null;
|
||||||
|
|
||||||
|
var value = source.GetType()
|
||||||
|
.GetProperty(
|
||||||
|
propertyName,
|
||||||
|
global::System.Reflection.BindingFlags.Public
|
||||||
|
| global::System.Reflection.BindingFlags.Instance
|
||||||
|
| global::System.Reflection.BindingFlags.IgnoreCase)
|
||||||
|
?.GetValue(source);
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (value is DateTime dateTime)
|
||||||
|
return dateTime.ToString(string.IsNullOrWhiteSpace(formatRule) ? "yyyy年MM月dd日" : formatRule);
|
||||||
|
if (value is DateTimeOffset dateTimeOffset)
|
||||||
|
return dateTimeOffset.ToString(string.IsNullOrWhiteSpace(formatRule) ? "yyyy年MM月dd日" : formatRule);
|
||||||
|
if (!string.IsNullOrWhiteSpace(formatRule) && value is IFormattable formattable)
|
||||||
|
return formattable.ToString(formatRule, null);
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveEmployeeField(string sourceField)
|
||||||
|
{
|
||||||
|
return sourceField.ToUpperInvariant() switch
|
||||||
{
|
{
|
||||||
"COMPANY_CODE" => nameof(ObCompany.CompanyCode),
|
"FULLNAME" or "FULL_NAME" or "NAME" => nameof(ObTaskEmployeeInfo.EmployeeName),
|
||||||
"COMPANY_NAME" => nameof(ObCompany.CompanyName),
|
"MOBILE" or "MOBILEPHONE" or "MOBILE_PHONE" => nameof(ObTaskEmployeeInfo.Phone),
|
||||||
"LEGAL_NAME" or "COMPANY_LEGAL_NAME" => nameof(ObCompany.LegalName),
|
"IDNUMBER" or "ID_NUMBER" or "IDENTITYNUMBER" => nameof(ObTaskEmployeeInfo.IdCardNumber),
|
||||||
"UNIFIED_SOCIAL_CREDIT_CODE" => nameof(ObCompany.UnifiedSocialCreditCode),
|
_ => sourceField
|
||||||
"LEGAL_REPRESENTATIVE" => nameof(ObCompany.LegalRepresentative),
|
};
|
||||||
"REGISTERED_ADDRESS" => nameof(ObCompany.RegisteredAddress),
|
}
|
||||||
"CONTACT_PHONE" => nameof(ObCompany.ContactPhone),
|
private static string ResolveCompanyField(string sourceField)
|
||||||
_ => definition.VariableCode
|
{
|
||||||
|
return sourceField.ToUpperInvariant() switch
|
||||||
|
{
|
||||||
|
// 内置合同变量 CompanyCode 表示统一社会信用代码,保留旧 DOCX 占位符兼容。
|
||||||
|
"COMPANYCODE" or "COMPANY_CODE" => nameof(ObCompany.UnifiedSocialCreditCode),
|
||||||
|
"COMPANYNAME" or "COMPANY_NAME" => nameof(ObCompany.CompanyName),
|
||||||
|
"LEGALNAME" or "LEGAL_NAME" or "COMPANYLEGALNAME" => nameof(ObCompany.LegalName),
|
||||||
|
"UNIFIEDSOCIALCREDITCODE" or "UNIFIED_SOCIAL_CREDIT_CODE" => nameof(ObCompany.UnifiedSocialCreditCode),
|
||||||
|
"LEGALREPRESENTATIVE" or "LEGAL_REPRESENTATIVE" => nameof(ObCompany.LegalRepresentative),
|
||||||
|
"ADDRESS" or "COMPANYADDRESS" or "COMPANY_ADDRESS"
|
||||||
|
or "REGISTEREDADDRESS" or "REGISTERED_ADDRESS" => nameof(ObCompany.RegisteredAddress),
|
||||||
|
"PHONE" or "COMPANYPHONE" or "COMPANY_PHONE"
|
||||||
|
or "CONTACTPHONE" or "CONTACT_PHONE" => nameof(ObCompany.ContactPhone),
|
||||||
|
_ => sourceField
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+264
@@ -0,0 +1,264 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负责从 DOCX 中识别占位符并在不依赖 Word 进程的情况下替换文本节点。
|
||||||
|
/// </summary>
|
||||||
|
public static class DocxTemplateProcessor
|
||||||
|
{
|
||||||
|
private static readonly global::System.Text.RegularExpressions.Regex PlaceholderRegex = new(
|
||||||
|
@"\{\{\s*(?<code>[A-Za-z][A-Za-z0-9_.-]*)\s*\}\}",
|
||||||
|
global::System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly global::System.Xml.Linq.XNamespace WordNamespace =
|
||||||
|
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 扫描正文、表格、页眉和页脚,返回去重后的变量编码。
|
||||||
|
/// </summary>
|
||||||
|
public static List<string> ExtractVariableCodes(byte[] docx)
|
||||||
|
{
|
||||||
|
ValidateDocxHeader(docx);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var input = new MemoryStream(docx);
|
||||||
|
using var archive = new global::System.IO.Compression.ZipArchive(
|
||||||
|
input,
|
||||||
|
global::System.IO.Compression.ZipArchiveMode.Read);
|
||||||
|
if (archive.GetEntry("word/document.xml") == null)
|
||||||
|
throw Oops.Bah("上传文件不是有效的 DOCX 文档,请使用 Word 或 WPS 另存为“Word 文档(*.docx)”后重新上传");
|
||||||
|
|
||||||
|
var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var entry in archive.Entries.Where(entry => IsWordTextXml(entry.FullName)))
|
||||||
|
{
|
||||||
|
using var stream = entry.Open();
|
||||||
|
var document = global::System.Xml.Linq.XDocument.Load(stream);
|
||||||
|
foreach (var paragraph in document.Descendants(WordNamespace + "p"))
|
||||||
|
{
|
||||||
|
var text = string.Concat(paragraph.Descendants(WordNamespace + "t").Select(node => node.Value));
|
||||||
|
ValidatePlaceholderSyntax(text);
|
||||||
|
foreach (global::System.Text.RegularExpressions.Match match in PlaceholderRegex.Matches(text))
|
||||||
|
codes.Add(match.Groups["code"].Value.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return codes.OrderBy(code => code).ToList();
|
||||||
|
}
|
||||||
|
catch (global::System.IO.InvalidDataException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("DOCX 文件结构已经损坏或上传不完整,请重新另存为 DOCX 后上传");
|
||||||
|
}
|
||||||
|
catch (global::System.Xml.XmlException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("DOCX 文件内部内容损坏,Word 可以尝试修复该文件;请修复并重新另存为 DOCX 后上传");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 占位符提取正则只会匹配正确格式,因此必须先主动检查错误格式,
|
||||||
|
/// 避免缺少花括号或变量编码写错时被静默忽略并继续创建版本。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidatePlaceholderSyntax(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (text.Contains("{{", StringComparison.Ordinal)
|
||||||
|
|| text.Contains("}}", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
"DOCX 占位符格式错误:检测到中文全角花括号。请使用英文半角双花括号,例如 {{employee.EmployeeName}}。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.Contains("{{{", StringComparison.Ordinal)
|
||||||
|
|| text.Contains("}}}", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
"DOCX 占位符格式错误:检测到多余的花括号。占位符必须使用成对的英文半角双花括号,例如 {{employee.EmployeeName}}。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchStart = 0;
|
||||||
|
while (searchStart < text.Length)
|
||||||
|
{
|
||||||
|
var openIndex = text.IndexOf("{{", searchStart, StringComparison.Ordinal);
|
||||||
|
var closeIndex = text.IndexOf("}}", searchStart, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (closeIndex >= 0 && (openIndex < 0 || closeIndex < openIndex))
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"DOCX 占位符格式错误:{BuildPlaceholderPreview(text, closeIndex)} 缺少对应的起始符“{{{{”。正确格式示例:{{{{employee.EmployeeName}}}}。");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openIndex < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
closeIndex = text.IndexOf("}}", openIndex + 2, StringComparison.Ordinal);
|
||||||
|
if (closeIndex < 0)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"DOCX 占位符格式错误:{BuildPlaceholderPreview(text, openIndex)} 缺少结束符“}}}}”。正确格式示例:{{{{employee.EmployeeName}}}}。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholder = text.Substring(openIndex, closeIndex + 2 - openIndex);
|
||||||
|
var match = PlaceholderRegex.Match(placeholder);
|
||||||
|
if (!match.Success || match.Length != placeholder.Length)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"DOCX 占位符格式错误:“{placeholder}”。变量编码只能包含英文字母、数字、下划线、点和短横线,并且必须使用成对的英文半角双花括号。");
|
||||||
|
}
|
||||||
|
|
||||||
|
searchStart = closeIndex + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildPlaceholderPreview(string text, int markerIndex)
|
||||||
|
{
|
||||||
|
const int maxLength = 60;
|
||||||
|
var preview = text[markerIndex..].Trim();
|
||||||
|
if (preview.Length > maxLength)
|
||||||
|
preview = preview[..maxLength] + "…";
|
||||||
|
return $"“{preview}”";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用任务变量快照替换 DOCX 中的占位符,支持占位符跨多个 Word 文本节点。
|
||||||
|
/// </summary>
|
||||||
|
public static byte[] Render(byte[] docx, IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
ValidateDocxHeader(docx);
|
||||||
|
using var input = new MemoryStream(docx);
|
||||||
|
using var archive = new global::System.IO.Compression.ZipArchive(
|
||||||
|
input,
|
||||||
|
global::System.IO.Compression.ZipArchiveMode.Read);
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
using var target = new global::System.IO.Compression.ZipArchive(
|
||||||
|
output,
|
||||||
|
global::System.IO.Compression.ZipArchiveMode.Create,
|
||||||
|
true);
|
||||||
|
|
||||||
|
foreach (var entry in archive.Entries)
|
||||||
|
{
|
||||||
|
var next = target.CreateEntry(
|
||||||
|
entry.FullName,
|
||||||
|
global::System.IO.Compression.CompressionLevel.Optimal);
|
||||||
|
using var sourceStream = entry.Open();
|
||||||
|
using var targetStream = next.Open();
|
||||||
|
if (!IsWordTextXml(entry.FullName))
|
||||||
|
{
|
||||||
|
sourceStream.CopyTo(targetStream);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = global::System.Xml.Linq.XDocument.Load(sourceStream);
|
||||||
|
foreach (var paragraph in document.Descendants(WordNamespace + "p"))
|
||||||
|
ReplaceParagraphVariables(paragraph, values);
|
||||||
|
document.Save(targetStream, global::System.Xml.Linq.SaveOptions.DisableFormatting);
|
||||||
|
}
|
||||||
|
|
||||||
|
target.Dispose();
|
||||||
|
return output.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReplaceParagraphVariables(
|
||||||
|
global::System.Xml.Linq.XElement paragraph,
|
||||||
|
IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
var replacements = 0;
|
||||||
|
while (replacements < 1000)
|
||||||
|
{
|
||||||
|
var nodes = paragraph.Descendants(WordNamespace + "t").ToList();
|
||||||
|
var text = string.Concat(nodes.Select(node => node.Value));
|
||||||
|
var match = PlaceholderRegex.Matches(text)
|
||||||
|
.Cast<global::System.Text.RegularExpressions.Match>()
|
||||||
|
.LastOrDefault(item => values.ContainsKey(item.Groups["code"].Value.Trim()));
|
||||||
|
if (match == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var code = match.Groups["code"].Value.Trim();
|
||||||
|
ReplaceRange(nodes, match.Index, match.Length, values[code] ?? string.Empty);
|
||||||
|
replacements++;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Oops.Bah("合同模板单个段落中的变量数量异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReplaceRange(
|
||||||
|
List<global::System.Xml.Linq.XElement> nodes,
|
||||||
|
int start,
|
||||||
|
int length,
|
||||||
|
string replacement)
|
||||||
|
{
|
||||||
|
var end = start + length;
|
||||||
|
var offset = 0;
|
||||||
|
global::System.Xml.Linq.XElement startNode = null;
|
||||||
|
global::System.Xml.Linq.XElement endNode = null;
|
||||||
|
var startIndex = 0;
|
||||||
|
var endIndex = 0;
|
||||||
|
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
var nextOffset = offset + node.Value.Length;
|
||||||
|
if (startNode == null && start >= offset && start < nextOffset)
|
||||||
|
{
|
||||||
|
startNode = node;
|
||||||
|
startIndex = start - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end > offset && end <= nextOffset)
|
||||||
|
{
|
||||||
|
endNode = node;
|
||||||
|
endIndex = end - offset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = nextOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startNode == null || endNode == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ReferenceEquals(startNode, endNode))
|
||||||
|
{
|
||||||
|
startNode.Value = startNode.Value[..startIndex]
|
||||||
|
+ replacement
|
||||||
|
+ startNode.Value[endIndex..];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startNodePosition = nodes.IndexOf(startNode);
|
||||||
|
var endNodePosition = nodes.IndexOf(endNode);
|
||||||
|
startNode.Value = startNode.Value[..startIndex] + replacement;
|
||||||
|
for (var index = startNodePosition + 1; index < endNodePosition; index++)
|
||||||
|
nodes[index].Value = string.Empty;
|
||||||
|
endNode.Value = endNode.Value[endIndex..];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWordTextXml(string entryName)
|
||||||
|
{
|
||||||
|
return entryName == "word/document.xml"
|
||||||
|
|| ((entryName.StartsWith("word/header", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| entryName.StartsWith("word/footer", StringComparison.OrdinalIgnoreCase))
|
||||||
|
&& entryName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据文件头识别真实格式,避免只修改扩展名的旧版 DOC 在解压时变成系统异常。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateDocxHeader(byte[] docx)
|
||||||
|
{
|
||||||
|
if (docx == null || docx.Length < 4)
|
||||||
|
throw Oops.Bah("上传的 DOCX 文件为空或上传不完整,请重新选择文件上传");
|
||||||
|
|
||||||
|
// 旧版 Word 97-2003 二进制 DOC 使用 OLE Compound File 文件头:D0 CF 11 E0。
|
||||||
|
if (docx[0] == 0xD0
|
||||||
|
&& docx[1] == 0xCF
|
||||||
|
&& docx[2] == 0x11
|
||||||
|
&& docx[3] == 0xE0)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("上传文件实际是旧版 DOC 格式,不能直接修改后缀名;请使用 Word 或 WPS 另存为“Word 文档(*.docx)”后重新上传");
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOCX 是 Open XML ZIP 包,正常文件以 PK 开头。
|
||||||
|
if (docx[0] != 0x50 || docx[1] != 0x4B)
|
||||||
|
throw Oops.Bah("上传文件的真实格式不是 DOCX,请使用 Word 或 WPS 另存为“Word 文档(*.docx)”后重新上传");
|
||||||
|
}
|
||||||
|
}
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
namespace SimpleAdmin.Application;
|
|
||||||
|
|
||||||
public class OnboardingContractScopePageInput : BasePageInput
|
|
||||||
{
|
|
||||||
public long? CompanyId { get; set; }
|
|
||||||
public long? PositionId { get; set; }
|
|
||||||
public long? TemplateId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OnboardingContractScopeAddInput
|
|
||||||
{
|
|
||||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
|
||||||
public long CompanyId { get; set; }
|
|
||||||
public long? PositionId { get; set; }
|
|
||||||
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
|
||||||
public long TemplateId { get; set; }
|
|
||||||
[IdNotNull(ErrorMessage = "模板版本Id不能为空")]
|
|
||||||
public long TemplateVersionId { get; set; }
|
|
||||||
public bool Required { get; set; } = true;
|
|
||||||
public int? SortCode { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OnboardingContractScopeEditInput : OnboardingContractScopeAddInput
|
|
||||||
{
|
|
||||||
[IdNotNull(ErrorMessage = "适用范围Id不能为空")]
|
|
||||||
public long Id { get; set; }
|
|
||||||
}
|
|
||||||
+98
@@ -1,26 +1,124 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板分页查询参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingContractTemplatePageInput : BasePageInput
|
public class OnboardingContractTemplatePageInput : BasePageInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板名称。
|
||||||
|
/// </summary>
|
||||||
public string TemplateName { get; set; }
|
public string TemplateName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同类型。
|
||||||
|
/// </summary>
|
||||||
public string ContractType { get; set; }
|
public string ContractType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态或状态筛选条件。
|
||||||
|
/// </summary>
|
||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增合同模板参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingContractTemplateAddInput
|
public class OnboardingContractTemplateAddInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "所属公司不能为空")]
|
||||||
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "所属部门不能为空")]
|
||||||
|
public long DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板适用的岗位 Id 列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<long> PositionIds { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板业务编码。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "模板编码不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "模板编码不能为空")]
|
||||||
public string TemplateCode { get; set; }
|
public string TemplateCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板名称。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "模板名称不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "模板名称不能为空")]
|
||||||
public string TemplateName { get; set; }
|
public string TemplateName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同类型。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "合同类型不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "合同类型不能为空")]
|
||||||
public string ContractType { get; set; }
|
public string ContractType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务说明。
|
||||||
|
/// </summary>
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态或状态筛选条件。
|
||||||
|
/// </summary>
|
||||||
public string Status { get; set; } = "enable";
|
public string Status { get; set; } = "enable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑合同模板参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingContractTemplateEditInput : OnboardingContractTemplateAddInput
|
public class OnboardingContractTemplateEditInput : OnboardingContractTemplateAddInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询公司、部门及岗位可选合同模板参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractTemplateAvailableInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "所属公司不能为空")]
|
||||||
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "所属部门不能为空")]
|
||||||
|
public long DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板已选择的岗位 Id 列表。
|
||||||
|
/// 返回结果必须同时适用于这些岗位。
|
||||||
|
/// </summary>
|
||||||
|
public List<long> PositionIds { get; set; } = new();
|
||||||
|
}
|
||||||
|
|||||||
+72
@@ -1,30 +1,102 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板版本分页查询参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingContractVersionPageInput : BasePageInput
|
public class OnboardingContractVersionPageInput : BasePageInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
||||||
public long TemplateId { get; set; }
|
public long TemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板版本状态。
|
||||||
|
/// </summary>
|
||||||
public string VersionStatus { get; set; }
|
public string VersionStatus { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增合同模板版本参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingContractVersionAddInput
|
public class OnboardingContractVersionAddInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
||||||
public long TemplateId { get; set; }
|
public long TemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板版本号。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "版本号不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "版本号不能为空")]
|
||||||
public string VersionNo { get; set; }
|
public string VersionNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板 DOCX 源文件 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "DOCX源文件不能为空")]
|
[IdNotNull(ErrorMessage = "DOCX源文件不能为空")]
|
||||||
public long SourceDocxFileId { get; set; }
|
public long SourceDocxFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 源文件内容哈希。
|
||||||
|
/// </summary>
|
||||||
public string FileHash { get; set; }
|
public string FileHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生效时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? EffectiveDate { get; set; }
|
public DateTime? EffectiveDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失效时间。
|
||||||
|
/// </summary>
|
||||||
public DateTime? ExpiryDate { get; set; }
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本版本是否需要公司盖章;旧客户端未传时沿用原规则。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本版本是否需要骑缝章;旧客户端未传时沿用系统配置。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
public string Remark { get; set; }
|
public string Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置合同模板版本状态参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingContractVersionStatusInput
|
public class OnboardingContractVersionStatusInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "版本Id不能为空")]
|
[IdNotNull(ErrorMessage = "版本Id不能为空")]
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板版本状态。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "版本状态不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "版本状态不能为空")]
|
||||||
public string VersionStatus { get; set; }
|
public string VersionStatus { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改合同模板版本的盖章配置;未传的开关保持不变。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractVersionSealInput
|
||||||
|
{
|
||||||
|
/// <summary>合同版本 Id。</summary>
|
||||||
|
[IdNotNull(ErrorMessage = "版本Id不能为空")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否需要公司盖章。</summary>
|
||||||
|
public bool? CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否需要骑缝章。</summary>
|
||||||
|
public bool? AcrossPageSealEnabled { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
+66
@@ -1,35 +1,101 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板变量分页查询参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingTemplateVariablePageInput : BasePageInput
|
public class OnboardingTemplateVariablePageInput : BasePageInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板版本 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "模板版本Id不能为空")]
|
[IdNotNull(ErrorMessage = "模板版本Id不能为空")]
|
||||||
public long TemplateVersionId { get; set; }
|
public long TemplateVersionId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增模板变量参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingTemplateVariableAddInput
|
public class OnboardingTemplateVariableAddInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板版本 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "模板版本Id不能为空")]
|
[IdNotNull(ErrorMessage = "模板版本Id不能为空")]
|
||||||
public long TemplateVersionId { get; set; }
|
public long TemplateVersionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同变量编码。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "变量编码不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "变量编码不能为空")]
|
||||||
public string VariableCode { get; set; }
|
public string VariableCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同变量名称。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "变量名称不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "变量名称不能为空")]
|
||||||
public string VariableName { get; set; }
|
public string VariableName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同变量来源类型。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "变量类型不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "变量类型不能为空")]
|
||||||
public string VariableType { get; set; }
|
public string VariableType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字段或变量数据类型。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "数据类型不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "数据类型不能为空")]
|
||||||
public string DataType { get; set; }
|
public string DataType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关联的候选人基础信息字段。
|
||||||
|
/// </summary>
|
||||||
public string EmployeeField { get; set; }
|
public string EmployeeField { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认值。
|
||||||
|
/// </summary>
|
||||||
public string DefaultValue { get; set; }
|
public string DefaultValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可选项 JSON。
|
||||||
|
/// </summary>
|
||||||
public string OptionsJson { get; set; }
|
public string OptionsJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化规则。
|
||||||
|
/// </summary>
|
||||||
public string FormatRule { get; set; }
|
public string FormatRule { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否必填、必传或必签。
|
||||||
|
/// </summary>
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事是否可以编辑。
|
||||||
|
/// </summary>
|
||||||
public bool HrEditable { get; set; }
|
public bool HrEditable { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示排序码。
|
||||||
|
/// </summary>
|
||||||
public int? SortCode { get; set; }
|
public int? SortCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
public string Remark { get; set; }
|
public string Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑模板变量参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingTemplateVariableEditInput : OnboardingTemplateVariableAddInput
|
public class OnboardingTemplateVariableEditInput : OnboardingTemplateVariableAddInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "变量Id不能为空")]
|
[IdNotNull(ErrorMessage = "变量Id不能为空")]
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-5
@@ -1,19 +1,72 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义合同模板、DOCX 版本和自动识别变量的管理能力。
|
||||||
|
/// </summary>
|
||||||
public interface IOnboardingContractTemplateService : ITransient
|
public interface IOnboardingContractTemplateService : ITransient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询合同模板主档案。
|
||||||
|
/// </summary>
|
||||||
Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input);
|
Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询合同模板详情。
|
||||||
|
/// </summary>
|
||||||
Task<ObContractTemplate> Detail(BaseIdInput input);
|
Task<ObContractTemplate> Detail(BaseIdInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询同时适用于指定公司、部门和全部已选岗位的启用合同模板。
|
||||||
|
/// </summary>
|
||||||
|
Task<List<ObContractTemplate>> ListAvailable(OnboardingContractTemplateAvailableInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增合同模板主档案。
|
||||||
|
/// </summary>
|
||||||
Task Add(OnboardingContractTemplateAddInput input);
|
Task Add(OnboardingContractTemplateAddInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑合同模板主档案。
|
||||||
|
/// </summary>
|
||||||
Task Edit(OnboardingContractTemplateEditInput input);
|
Task Edit(OnboardingContractTemplateEditInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被版本、入职模板或任务合同引用的模板。
|
||||||
|
/// </summary>
|
||||||
Task Delete(BaseIdListInput input);
|
Task Delete(BaseIdListInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询指定合同模板的版本。
|
||||||
|
/// </summary>
|
||||||
Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input);
|
Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增合同模板 DOCX 版本并自动识别变量。
|
||||||
|
/// </summary>
|
||||||
Task AddVersion(OnboardingContractVersionAddInput input);
|
Task AddVersion(OnboardingContractVersionAddInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用或停用合同模板版本。
|
||||||
|
/// </summary>
|
||||||
Task SetVersionStatus(OnboardingContractVersionStatusInput input);
|
Task SetVersionStatus(OnboardingContractVersionStatusInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改合同模板版本的盖章配置,仅影响后续新建任务。
|
||||||
|
/// </summary>
|
||||||
|
Task SetVersionSeals(OnboardingContractVersionSealInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被入职模板或任务使用的合同版本。
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteVersion(BaseIdInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询指定模板版本自动识别出的变量。
|
||||||
|
/// </summary>
|
||||||
Task<SqlSugarPagedList<ObTemplateVariable>> VariablePage(OnboardingTemplateVariablePageInput input);
|
Task<SqlSugarPagedList<ObTemplateVariable>> VariablePage(OnboardingTemplateVariablePageInput input);
|
||||||
Task AddVariable(OnboardingTemplateVariableAddInput input);
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑并确认模板版本中的变量定义。
|
||||||
|
/// </summary>
|
||||||
Task EditVariable(OnboardingTemplateVariableEditInput input);
|
Task EditVariable(OnboardingTemplateVariableEditInput input);
|
||||||
Task DeleteVariable(BaseIdListInput input); Task<SqlSugarPagedList<ObContractScope>> ScopePage(OnboardingContractScopePageInput input);
|
}
|
||||||
Task AddScope(OnboardingContractScopeAddInput input);
|
|
||||||
Task EditScope(OnboardingContractScopeEditInput input);
|
|
||||||
Task DeleteScope(BaseIdListInput input);}
|
|
||||||
|
|||||||
+640
-120
@@ -5,86 +5,369 @@ namespace SimpleAdmin.Application;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class OnboardingContractTemplateService : DbRepository<ObContractTemplate>, IOnboardingContractTemplateService
|
public class OnboardingContractTemplateService : DbRepository<ObContractTemplate>, IOnboardingContractTemplateService
|
||||||
{
|
{
|
||||||
|
private readonly IFileService _fileService;
|
||||||
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
|
public OnboardingContractTemplateService(
|
||||||
|
IFileService fileService,
|
||||||
|
IOnboardingDataPermissionService dataPermission)
|
||||||
|
{
|
||||||
|
_fileService = fileService;
|
||||||
|
_dataPermission = dataPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询合同模板主档案。
|
||||||
|
/// </summary>
|
||||||
public async Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input)
|
public async Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input)
|
||||||
{
|
{
|
||||||
return await Context.Queryable<ObContractTemplate>()
|
var query = Context.Queryable<ObContractTemplate>()
|
||||||
|
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
|
||||||
|
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
|
||||||
.WhereIF(
|
.WhereIF(
|
||||||
!string.IsNullOrWhiteSpace(input.TemplateName),
|
!string.IsNullOrWhiteSpace(input.TemplateName),
|
||||||
it => it.TemplateName.Contains(input.TemplateName) || it.TemplateCode.Contains(input.TemplateName))
|
it => it.TemplateName.Contains(input.TemplateName) || it.TemplateCode.Contains(input.TemplateName))
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(input.ContractType), it => it.ContractType == input.ContractType)
|
.WhereIF(!string.IsNullOrWhiteSpace(input.ContractType), it => it.ContractType == input.ContractType)
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status);
|
||||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
|
||||||
|
if (input.PositionId.HasValue)
|
||||||
|
{
|
||||||
|
var templateIds = await Context.Queryable<ObContractTemplatePosition>()
|
||||||
|
.Where(item => item.PositionId == input.PositionId.Value)
|
||||||
|
.Select(item => item.ContractTemplateId)
|
||||||
|
.ToListAsync();
|
||||||
|
query = query.Where(item => templateIds.Contains(item.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var page = await query
|
||||||
|
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), input.GetSafeOrderBy("Id", "Status", "CreateTime", "UpdateTime", "CreateUser", "CreateUserId", "CompanyId", "DepartmentId", "TemplateCode", "TemplateName", "ContractType"))
|
||||||
|
.OrderByDescending(it => it.CreateTime)
|
||||||
.OrderBy(it => it.TemplateName)
|
.OrderBy(it => it.TemplateName)
|
||||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||||
|
await PopulateScopeDisplay(page.List.ToList());
|
||||||
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询合同模板详情。
|
||||||
|
/// </summary>
|
||||||
public async Task<ObContractTemplate> Detail(BaseIdInput input)
|
public async Task<ObContractTemplate> Detail(BaseIdInput input)
|
||||||
{
|
{
|
||||||
return await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在");
|
var template = await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在");
|
||||||
|
await PopulateScopeDisplay(new List<ObContractTemplate> { template });
|
||||||
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询同时覆盖全部已选岗位的启用合同模板。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<ObContractTemplate>> ListAvailable(
|
||||||
|
OnboardingContractTemplateAvailableInput input)
|
||||||
|
{
|
||||||
|
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
|
||||||
|
if (input.PositionIds.Count == 0)
|
||||||
|
return new List<ObContractTemplate>();
|
||||||
|
|
||||||
|
await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
|
||||||
|
|
||||||
|
var candidates = await Context.Queryable<ObContractTemplate>()
|
||||||
|
.Where(item => item.CompanyId == input.CompanyId
|
||||||
|
&& item.DepartmentId == input.DepartmentId
|
||||||
|
&& item.Status == "enable")
|
||||||
|
.OrderBy(item => item.TemplateName)
|
||||||
|
.ToListAsync();
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
return candidates;
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var candidateIds = candidates.Select(item => item.Id).ToList();
|
||||||
|
var availableVersionTemplateIds = (await Context.Queryable<ObContractTemplateVersion>()
|
||||||
|
.Where(item => candidateIds.Contains(item.TemplateId))
|
||||||
|
.ToListAsync())
|
||||||
|
.Where(item => ContractTemplateVersionValidity.IsAvailable(item, now))
|
||||||
|
.Select(item => item.TemplateId)
|
||||||
|
.ToHashSet();
|
||||||
|
candidates = candidates
|
||||||
|
.Where(item => availableVersionTemplateIds.Contains(item.Id))
|
||||||
|
.ToList();
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
return candidates;
|
||||||
|
|
||||||
|
candidateIds = candidates.Select(item => item.Id).ToList();
|
||||||
|
var relations = await Context.Queryable<ObContractTemplatePosition>()
|
||||||
|
.Where(item => candidateIds.Contains(item.ContractTemplateId))
|
||||||
|
.ToListAsync();
|
||||||
|
var availableIds = relations
|
||||||
|
.GroupBy(item => item.ContractTemplateId)
|
||||||
|
.Where(group => input.PositionIds.All(positionId =>
|
||||||
|
group.Any(item => item.PositionId == positionId)))
|
||||||
|
.Select(group => group.Key)
|
||||||
|
.ToHashSet();
|
||||||
|
var availableTemplates = candidates
|
||||||
|
.Where(item => availableIds.Contains(item.Id))
|
||||||
|
.ToList();
|
||||||
|
await PopulateScopeDisplay(availableTemplates);
|
||||||
|
return availableTemplates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增合同模板主档案。
|
||||||
|
/// </summary>
|
||||||
public async Task Add(OnboardingContractTemplateAddInput input)
|
public async Task Add(OnboardingContractTemplateAddInput input)
|
||||||
{
|
{
|
||||||
|
NormalizeInput(input);
|
||||||
|
await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
|
||||||
await EnsureCodeUnique(input.TemplateCode, 0);
|
await EnsureCodeUnique(input.TemplateCode, 0);
|
||||||
await InsertAsync(input.Adapt<ObContractTemplate>());
|
|
||||||
|
var templateId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||||
|
var entity = BuildEntity(templateId, input);
|
||||||
|
await Context.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Context.Insertable(entity).ExecuteCommandAsync();
|
||||||
|
await InsertPositionRelations(templateId, input.PositionIds);
|
||||||
|
await Context.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Context.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑合同模板主档案。
|
||||||
|
/// </summary>
|
||||||
public async Task Edit(OnboardingContractTemplateEditInput input)
|
public async Task Edit(OnboardingContractTemplateEditInput input)
|
||||||
{
|
{
|
||||||
await Detail(new BaseIdInput { Id = input.Id });
|
var template = await Detail(new BaseIdInput { Id = input.Id });
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
template.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.ContractManageAll,
|
||||||
|
"合同模板");
|
||||||
|
NormalizeInput(input);
|
||||||
|
await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
|
||||||
|
await ValidateReferencedOnboardingTemplates(input, template.Status);
|
||||||
await EnsureCodeUnique(input.TemplateCode, input.Id);
|
await EnsureCodeUnique(input.TemplateCode, input.Id);
|
||||||
await UpdateAsync(input.Adapt<ObContractTemplate>());
|
|
||||||
|
var entity = BuildEntity(input.Id, input);
|
||||||
|
await Context.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Context.Updateable(entity)
|
||||||
|
.IgnoreColumns(ignoreAllNullColumns: true)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await Context.Deleteable<ObContractTemplatePosition>()
|
||||||
|
.Where(item => item.ContractTemplateId == input.Id)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await InsertPositionRelations(input.Id, input.PositionIds);
|
||||||
|
await Context.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Context.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被入职模板或任务合同引用的模板(级联删除未使用的版本和变量)。
|
||||||
|
/// </summary>
|
||||||
public async Task Delete(BaseIdListInput input)
|
public async Task Delete(BaseIdListInput input)
|
||||||
{
|
{
|
||||||
if (input.Ids.Count == 0)
|
if (input.Ids.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var hasVersions = await Context.Queryable<ObContractTemplateVersion>()
|
var templates = await Context.Queryable<ObContractTemplate>()
|
||||||
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
.Where(item => input.Ids.Contains(item.Id))
|
||||||
if (hasVersions)
|
.ToListAsync();
|
||||||
throw Oops.Bah("模板已有版本,不能删除");
|
if (templates.Count != input.Ids.Distinct().Count())
|
||||||
|
throw Oops.Bah("合同模板不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
templates.Select(item => item.CreateUserId),
|
||||||
|
OnboardingPermissionCodes.ContractManageAll,
|
||||||
|
"合同模板");
|
||||||
|
|
||||||
var hasScopes = await Context.Queryable<ObContractScope>()
|
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplateContract>()
|
||||||
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
.AnyAsync(it => input.Ids.Contains(it.ContractTemplateId));
|
||||||
if (hasScopes)
|
if (hasOnboardingTemplates)
|
||||||
throw Oops.Bah("模板已配置适用范围,不能删除");
|
throw Oops.Bah("模板已被入职模板引用,不能删除,请先在入职模板中移除关联");
|
||||||
|
|
||||||
var hasContracts = await Context.Queryable<ObTaskContract>()
|
var hasContracts = await Context.Queryable<ObTaskContract>()
|
||||||
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
||||||
if (hasContracts)
|
if (hasContracts)
|
||||||
throw Oops.Bah("模板已生成入职合同,不能删除");
|
throw Oops.Bah("模板已生成入职任务合同,不能删除,可改为停用");
|
||||||
|
|
||||||
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
|
var versionIds = await Context.Queryable<ObContractTemplateVersion>()
|
||||||
|
.Where(it => input.Ids.Contains(it.TemplateId))
|
||||||
|
.Select(it => it.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
await Context.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (versionIds.Count > 0)
|
||||||
|
{
|
||||||
|
await Context.Deleteable<ObTemplateVariable>()
|
||||||
|
.Where(it => versionIds.Contains(it.TemplateVersionId))
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await Context.Deleteable<ObContractTemplateVersion>()
|
||||||
|
.In(versionIds)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Context.Deleteable<ObContractTemplatePosition>()
|
||||||
|
.Where(item => input.Ids.Contains(item.ContractTemplateId))
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
|
||||||
|
await Context.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Context.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被入职模板或任务使用的合同版本。
|
||||||
|
/// </summary>
|
||||||
|
public async Task DeleteVersion(BaseIdInput input)
|
||||||
|
{
|
||||||
|
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
|
||||||
|
?? throw Oops.Bah("合同版本不存在");
|
||||||
|
await EnsureTemplateCanManage(version.TemplateId);
|
||||||
|
|
||||||
|
var isUsedInOnboardingTemplate = await Context.Queryable<ObOnboardingTemplateContract>()
|
||||||
|
.AnyAsync(it => it.ContractTemplateVersionId == version.Id);
|
||||||
|
if (isUsedInOnboardingTemplate)
|
||||||
|
throw Oops.Bah("该版本已被入职模板引用,不能删除,请先在入职模板中移除关联");
|
||||||
|
|
||||||
|
var isUsedInTask = await Context.Queryable<ObTaskContract>()
|
||||||
|
.AnyAsync(it => it.TemplateVersionId == version.Id);
|
||||||
|
if (isUsedInTask)
|
||||||
|
throw Oops.Bah("该版本已用于实际入职任务,不能删除,可改为停用");
|
||||||
|
|
||||||
|
await Context.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Context.Deleteable<ObTemplateVariable>()
|
||||||
|
.Where(it => it.TemplateVersionId == version.Id)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await Context.Deleteable<ObContractTemplateVersion>()
|
||||||
|
.In(version.Id)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await Context.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Context.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询指定合同模板的版本。
|
||||||
|
/// </summary>
|
||||||
public async Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input)
|
public async Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input)
|
||||||
{
|
{
|
||||||
return await Context.Queryable<ObContractTemplateVersion>()
|
var page = await Context.Queryable<ObContractTemplateVersion>()
|
||||||
.Where(it => it.TemplateId == input.TemplateId)
|
.Where(it => it.TemplateId == input.TemplateId)
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(input.VersionStatus), it => it.VersionStatus == input.VersionStatus)
|
.WhereIF(!string.IsNullOrWhiteSpace(input.VersionStatus), it => it.VersionStatus == input.VersionStatus)
|
||||||
.OrderByDescending(it => it.CreateTime)
|
.OrderByDescending(it => it.CreateTime)
|
||||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||||
|
|
||||||
|
// 仅解析返回值,不回写历史空值,使列表开关显示真实生效的规则。
|
||||||
|
var setting = App.GetConfig<QiyuesuoSettingOptions>("QiyuesuoSetting", true)
|
||||||
|
?? new QiyuesuoSettingOptions();
|
||||||
|
foreach (var version in page.List)
|
||||||
|
{
|
||||||
|
version.CompanySealEnabled = QiyuesuoContractRequestBuilder.CompanySealEnabled(version.CompanySealEnabled);
|
||||||
|
version.AcrossPageSealEnabled = QiyuesuoContractRequestBuilder.AcrossPageSealEnabled(version.AcrossPageSealEnabled, setting);
|
||||||
|
}
|
||||||
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增合同模板 DOCX 版本。
|
||||||
|
/// </summary>
|
||||||
public async Task AddVersion(OnboardingContractVersionAddInput input)
|
public async Task AddVersion(OnboardingContractVersionAddInput input)
|
||||||
{
|
{
|
||||||
await Detail(new BaseIdInput { Id = input.TemplateId });
|
ContractTemplateVersionValidity.ValidateRange(input.EffectiveDate, input.ExpiryDate);
|
||||||
|
var template = await Detail(new BaseIdInput { Id = input.TemplateId });
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
template.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.ContractManageAll,
|
||||||
|
"合同模板");
|
||||||
var file = await Context.Queryable<SysFile>().InSingleAsync(input.SourceDocxFileId)
|
var file = await Context.Queryable<SysFile>().InSingleAsync(input.SourceDocxFileId)
|
||||||
?? throw Oops.Bah("DOCX源文件不存在");
|
?? throw Oops.Bah("DOCX 源文件不存在");
|
||||||
if (!string.Equals(file.Suffix, "docx", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(file.Suffix, "docx", StringComparison.OrdinalIgnoreCase))
|
||||||
throw Oops.Bah("合同模板仅支持DOCX文件");
|
throw Oops.Bah("合同模板仅支持 DOCX 文件");
|
||||||
|
|
||||||
var versionExists = await Context.Queryable<ObContractTemplateVersion>()
|
var versionExists = await Context.Queryable<ObContractTemplateVersion>()
|
||||||
.AnyAsync(it => it.TemplateId == input.TemplateId && it.VersionNo == input.VersionNo);
|
.AnyAsync(item => item.TemplateId == input.TemplateId && item.VersionNo == input.VersionNo);
|
||||||
if (versionExists)
|
if (versionExists)
|
||||||
throw Oops.Bah("该模板版本号已存在");
|
throw Oops.Bah("该模板版本号已存在");
|
||||||
|
|
||||||
await Context.Insertable(input.Adapt<ObContractTemplateVersion>()).ExecuteCommandAsync();
|
var sourceResult = await _fileService.Download(new BaseIdInput { Id = file.Id })
|
||||||
|
?? throw Oops.Bah("无法读取 DOCX 源文件");
|
||||||
|
await using var sourceStream = sourceResult.FileStream;
|
||||||
|
using var sourceBytes = new MemoryStream();
|
||||||
|
await sourceStream.CopyToAsync(sourceBytes);
|
||||||
|
var bytes = sourceBytes.ToArray();
|
||||||
|
var variableCodes = DocxTemplateProcessor.ExtractVariableCodes(bytes);
|
||||||
|
|
||||||
|
// 严格比对变量字典库,防止手误和非法占位符
|
||||||
|
var dictList = await Context.Queryable<ObVariableDefinition>().ToListAsync();
|
||||||
|
var duplicateDefinition = dictList
|
||||||
|
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
|
if (duplicateDefinition != null)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"变量字典编码“{duplicateDefinition.Key}”存在重复数据,请先清理后再导入合同版本");
|
||||||
|
}
|
||||||
|
|
||||||
|
var dictMap = dictList.ToDictionary(it => it.VariableCode, it => it, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var undefinedCodes = variableCodes.Where(code => !dictMap.ContainsKey(code)).ToList();
|
||||||
|
if (undefinedCodes.Count > 0)
|
||||||
|
{
|
||||||
|
throw Oops.Bah($"DOCX 模板包含未在字典库中注册的占位符:{string.Join(",", undefinedCodes)}。请检查拼写或先在【变量字典】中添加该变量。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||||
|
var version = input.Adapt<ObContractTemplateVersion>();
|
||||||
|
version.Id = versionId;
|
||||||
|
version.VersionStatus = ContractTemplateVersionStatus.Draft;
|
||||||
|
version.FileHash = string.IsNullOrWhiteSpace(input.FileHash)
|
||||||
|
? Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData(bytes))
|
||||||
|
: input.FileHash;
|
||||||
|
|
||||||
|
var variables = variableCodes.Select((code, index) =>
|
||||||
|
{
|
||||||
|
var def = dictMap.TryGetValue(code, out var d) ? d : null;
|
||||||
|
return BuildDetectedVariable(versionId, code, def, index);
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
await Context.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Context.Insertable(version).ExecuteCommandAsync();
|
||||||
|
if (variables.Count > 0)
|
||||||
|
await Context.Insertable(variables).ExecuteCommandAsync();
|
||||||
|
await Context.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Context.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用或停用合同模板版本。
|
||||||
|
/// </summary>
|
||||||
public async Task SetVersionStatus(OnboardingContractVersionStatusInput input)
|
public async Task SetVersionStatus(OnboardingContractVersionStatusInput input)
|
||||||
{
|
{
|
||||||
if (input.VersionStatus != ContractTemplateVersionStatus.Enabled
|
if (input.VersionStatus != ContractTemplateVersionStatus.Enabled
|
||||||
@@ -95,19 +378,53 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
|||||||
|
|
||||||
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
|
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
|
||||||
?? throw Oops.Bah("模板版本不存在");
|
?? throw Oops.Bah("模板版本不存在");
|
||||||
|
await EnsureTemplateCanManage(version.TemplateId);
|
||||||
|
|
||||||
|
if (version.VersionStatus == ContractTemplateVersionStatus.Enabled
|
||||||
|
&& input.VersionStatus == ContractTemplateVersionStatus.Disabled)
|
||||||
|
{
|
||||||
|
var referencedNames = await GetEnabledOnboardingTemplateNames(
|
||||||
|
version.TemplateId,
|
||||||
|
version.Id);
|
||||||
|
if (referencedNames.Count > 0)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"该签署材料版本正在被启用中的入职模板{FormatTemplateNames(referencedNames)}引用,不能停用,请先调整或停用这些入职模板");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (input.VersionStatus == ContractTemplateVersionStatus.Enabled)
|
if (input.VersionStatus == ContractTemplateVersionStatus.Enabled)
|
||||||
{
|
{
|
||||||
var hasVariables = await Context.Queryable<ObTemplateVariable>()
|
ContractTemplateVersionValidity.ValidateRange(version.EffectiveDate, version.ExpiryDate);
|
||||||
.AnyAsync(it => it.TemplateVersionId == input.Id);
|
if (version.ExpiryDate.HasValue && version.ExpiryDate.Value <= DateTime.Now)
|
||||||
if (!hasVariables)
|
throw Oops.Bah("该签署材料版本已经失效,不能启用");
|
||||||
throw Oops.Bah("请先配置模板变量后再启用");
|
var hasUnresolvedVariables = await Context.Queryable<ObTemplateVariable>()
|
||||||
|
.AnyAsync(item => item.TemplateVersionId == input.Id
|
||||||
|
&& item.VariableType == ContractVariableType.Unresolved);
|
||||||
|
if (hasUnresolvedVariables)
|
||||||
|
throw Oops.Bah("仍有未识别来源的合同变量,请逐项确认后再启用");
|
||||||
}
|
}
|
||||||
|
|
||||||
version.VersionStatus = input.VersionStatus;
|
version.VersionStatus = input.VersionStatus;
|
||||||
await Context.Updateable(version).ExecuteCommandAsync();
|
await Context.Updateable(version)
|
||||||
|
.UpdateColumns(item => new { item.VersionStatus, item.UpdateTime, item.UpdateUserId, item.UpdateUser })
|
||||||
|
.ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改版本盖章开关;任务和电子签约已经保存独立快照,不随版本变更。
|
||||||
|
/// </summary>
|
||||||
|
public async Task SetVersionSeals(OnboardingContractVersionSealInput input)
|
||||||
|
{
|
||||||
|
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
|
||||||
|
?? throw Oops.Bah("模板版本不存在");
|
||||||
|
await EnsureTemplateCanManage(version.TemplateId);
|
||||||
|
|
||||||
|
await ContractTemplateVersionSealConfiguration.BuildUpdate(Context, input).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询指定模板版本的变量定义。
|
||||||
|
/// </summary>
|
||||||
public async Task<SqlSugarPagedList<ObTemplateVariable>> VariablePage(OnboardingTemplateVariablePageInput input)
|
public async Task<SqlSugarPagedList<ObTemplateVariable>> VariablePage(OnboardingTemplateVariablePageInput input)
|
||||||
{
|
{
|
||||||
return await Context.Queryable<ObTemplateVariable>()
|
return await Context.Queryable<ObTemplateVariable>()
|
||||||
@@ -116,76 +433,94 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
|||||||
.OrderBy(it => it.VariableCode)
|
.OrderBy(it => it.VariableCode)
|
||||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
public async Task AddVariable(OnboardingTemplateVariableAddInput input)
|
/// 编辑模板版本中自动识别出的变量,不允许修改 DOCX 占位符编码。
|
||||||
{
|
/// </summary>
|
||||||
await EnsureVersionDraft(input.TemplateVersionId);
|
|
||||||
await EnsureVariableUnique(input.TemplateVersionId, input.VariableCode, 0);
|
|
||||||
await Context.Insertable(input.Adapt<ObTemplateVariable>()).ExecuteCommandAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task EditVariable(OnboardingTemplateVariableEditInput input)
|
public async Task EditVariable(OnboardingTemplateVariableEditInput input)
|
||||||
{
|
{
|
||||||
var variable = await Context.Queryable<ObTemplateVariable>().InSingleAsync(input.Id)
|
var variable = await Context.Queryable<ObTemplateVariable>().InSingleAsync(input.Id)
|
||||||
?? throw Oops.Bah("模板变量不存在");
|
?? throw Oops.Bah("模板变量不存在");
|
||||||
if (variable.TemplateVersionId != input.TemplateVersionId)
|
if (variable.TemplateVersionId != input.TemplateVersionId)
|
||||||
throw Oops.Bah("不允许变更变量所属版本");
|
throw Oops.Bah("不允许变更变量所属版本");
|
||||||
|
if (!string.Equals(variable.VariableCode, input.VariableCode, StringComparison.Ordinal))
|
||||||
|
throw Oops.Bah("DOCX 占位符编码由系统识别,不能修改");
|
||||||
|
|
||||||
await EnsureVersionDraft(input.TemplateVersionId);
|
var version = await Context.Queryable<ObContractTemplateVersion>()
|
||||||
await EnsureVariableUnique(input.TemplateVersionId, input.VariableCode, input.Id);
|
.InSingleAsync(variable.TemplateVersionId)
|
||||||
await Context.Updateable(input.Adapt<ObTemplateVariable>()).ExecuteCommandAsync();
|
?? throw Oops.Bah("模板版本不存在");
|
||||||
|
await EnsureTemplateCanManage(version.TemplateId);
|
||||||
|
|
||||||
|
var isUsedInTask = await Context.Queryable<ObTaskContract>()
|
||||||
|
.AnyAsync(it => it.TemplateVersionId == input.TemplateVersionId);
|
||||||
|
if (isUsedInTask)
|
||||||
|
throw Oops.Bah("该版本已生成实际入职任务合同,变量配置已锁定");
|
||||||
|
|
||||||
|
var entity = input.Adapt<ObTemplateVariable>();
|
||||||
|
entity.VariableCode = variable.VariableCode;
|
||||||
|
await Context.Updateable(entity).ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteVariable(BaseIdListInput input)
|
private async Task EnsureTemplateCanManage(long templateId)
|
||||||
{
|
{
|
||||||
if (input.Ids.Count == 0)
|
var template = await Context.Queryable<ObContractTemplate>().InSingleAsync(templateId)
|
||||||
return;
|
?? throw Oops.Bah("合同模板不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
var versionIds = await Context.Queryable<ObTemplateVariable>()
|
template.CreateUserId,
|
||||||
.Where(it => input.Ids.Contains(it.Id))
|
OnboardingPermissionCodes.ContractManageAll,
|
||||||
.Select(it => it.TemplateVersionId)
|
"合同模板");
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
foreach (var versionId in versionIds.Distinct())
|
|
||||||
await EnsureVersionDraft(versionId);
|
|
||||||
|
|
||||||
await Context.Deleteable<ObTemplateVariable>().In(input.Ids).ExecuteCommandAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SqlSugarPagedList<ObContractScope>> ScopePage(OnboardingContractScopePageInput input)
|
private static ObTemplateVariable BuildDetectedVariable(long versionId, string code, ObVariableDefinition def, int index)
|
||||||
{
|
{
|
||||||
return await Context.Queryable<ObContractScope>()
|
var separatorIndex = code.IndexOf('.');
|
||||||
.WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value)
|
var prefix = separatorIndex > 0 ? code[..separatorIndex].ToLowerInvariant() : string.Empty;
|
||||||
.WhereIF(input.PositionId.HasValue, it => it.PositionId == input.PositionId.Value)
|
var sourceField = separatorIndex > 0 ? code[(separatorIndex + 1)..] : code;
|
||||||
.WhereIF(input.TemplateId.HasValue, it => it.TemplateId == input.TemplateId.Value)
|
var variableType = prefix switch
|
||||||
.OrderBy(it => it.SortCode)
|
{
|
||||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
"employee" => ContractVariableType.Employee,
|
||||||
|
"company" => ContractVariableType.Company,
|
||||||
|
"hr" => ContractVariableType.Contract,
|
||||||
|
"system" => ContractVariableType.System,
|
||||||
|
_ => ContractVariableType.Unresolved
|
||||||
|
};
|
||||||
|
|
||||||
|
var varName = def != null && !string.IsNullOrWhiteSpace(def.VariableName) ? def.VariableName : sourceField;
|
||||||
|
var dataType = def != null && !string.IsNullOrWhiteSpace(def.DataType) ? def.DataType : InferDataType(sourceField);
|
||||||
|
|
||||||
|
return new ObTemplateVariable
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
TemplateVersionId = versionId,
|
||||||
|
VariableCode = code,
|
||||||
|
VariableName = varName,
|
||||||
|
VariableType = variableType,
|
||||||
|
DataType = dataType,
|
||||||
|
EmployeeField = variableType is ContractVariableType.Employee or ContractVariableType.Company
|
||||||
|
? sourceField
|
||||||
|
: null,
|
||||||
|
DefaultValue = def?.DefaultValue,
|
||||||
|
Required = def?.Required ?? true,
|
||||||
|
HrEditable = variableType == ContractVariableType.Contract,
|
||||||
|
// 合同版本首次识别时继承全局变量字典排序;未登记排序时才使用识别顺序兜底。
|
||||||
|
SortCode = def?.SortCode ?? (index + 1) * 10,
|
||||||
|
Remark = def?.Remark
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddScope(OnboardingContractScopeAddInput input)
|
private static string InferDataType(string sourceField)
|
||||||
{
|
{
|
||||||
await EnsureScopeValid(input.CompanyId, input.PositionId, input.TemplateId, input.TemplateVersionId);
|
var normalized = sourceField.ToLowerInvariant();
|
||||||
await EnsureScopeUnique(input.CompanyId, input.PositionId, input.TemplateVersionId, 0);
|
if (normalized.Contains("date") || normalized.EndsWith("at"))
|
||||||
await Context.Insertable(input.Adapt<ObContractScope>()).ExecuteCommandAsync();
|
return "date";
|
||||||
}
|
if (normalized.Contains("salary")
|
||||||
|
|| normalized.Contains("amount")
|
||||||
|
|| normalized.Contains("wage")
|
||||||
|
|| normalized.Contains("copies"))
|
||||||
|
{
|
||||||
|
return "decimal";
|
||||||
|
}
|
||||||
|
|
||||||
public async Task EditScope(OnboardingContractScopeEditInput input)
|
return "string";
|
||||||
{
|
|
||||||
var scopeExists = await Context.Queryable<ObContractScope>().AnyAsync(it => it.Id == input.Id);
|
|
||||||
if (!scopeExists)
|
|
||||||
throw Oops.Bah("合同适用范围不存在");
|
|
||||||
|
|
||||||
await EnsureScopeValid(input.CompanyId, input.PositionId, input.TemplateId, input.TemplateVersionId);
|
|
||||||
await EnsureScopeUnique(input.CompanyId, input.PositionId, input.TemplateVersionId, input.Id);
|
|
||||||
await Context.Updateable(input.Adapt<ObContractScope>()).ExecuteCommandAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteScope(BaseIdListInput input)
|
|
||||||
{
|
|
||||||
if (input.Ids.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await Context.Deleteable<ObContractScope>().In(input.Ids).ExecuteCommandAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureCodeUnique(string code, long id)
|
private async Task EnsureCodeUnique(string code, long id)
|
||||||
@@ -196,51 +531,236 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
|||||||
throw Oops.Bah("模板编码已存在");
|
throw Oops.Bah("模板编码已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureVersionDraft(long versionId)
|
/// <summary>
|
||||||
|
/// 标准化合同模板保存参数并执行基础业务校验。
|
||||||
|
/// </summary>
|
||||||
|
private static void NormalizeInput(OnboardingContractTemplateAddInput input)
|
||||||
{
|
{
|
||||||
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(versionId)
|
input.TemplateCode = OnboardingCodeValidator.Normalize(input.TemplateCode, "签署材料模板编码", 50);
|
||||||
?? throw Oops.Bah("模板版本不存在");
|
input.TemplateName = input.TemplateName?.Trim();
|
||||||
if (version.VersionStatus != ContractTemplateVersionStatus.Draft)
|
input.ContractType = input.ContractType?.Trim();
|
||||||
throw Oops.Bah("只有草稿版本可维护变量");
|
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureVariableUnique(long versionId, string code, long id)
|
if (string.IsNullOrWhiteSpace(input.TemplateName)
|
||||||
{
|
|| string.IsNullOrWhiteSpace(input.ContractType))
|
||||||
var exists = await Context.Queryable<ObTemplateVariable>()
|
|
||||||
.AnyAsync(it => it.TemplateVersionId == versionId && it.VariableCode == code && it.Id != id);
|
|
||||||
if (exists)
|
|
||||||
throw Oops.Bah("该版本变量编码已存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureScopeValid(long companyId, long? positionId, long templateId, long versionId)
|
|
||||||
{
|
|
||||||
var companyExists = await Context.Queryable<ObCompany>()
|
|
||||||
.AnyAsync(it => it.Id == companyId && it.Status == "enable");
|
|
||||||
if (!companyExists)
|
|
||||||
throw Oops.Bah("所属公司不存在或未启用");
|
|
||||||
|
|
||||||
if (positionId.HasValue)
|
|
||||||
{
|
{
|
||||||
var positionExists = await Context.Queryable<ObPosition>()
|
throw Oops.Bah("请填写模板编码、模板名称和合同类型");
|
||||||
.AnyAsync(it => it.Id == positionId.Value && it.CompanyId == companyId && it.Status == "enable");
|
|
||||||
if (!positionExists)
|
|
||||||
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(versionId)
|
if (input.PositionIds.Count == 0)
|
||||||
?? throw Oops.Bah("合同版本不存在");
|
throw Oops.Bah("请至少选择一个适用岗位");
|
||||||
if (version.TemplateId != templateId || version.VersionStatus != ContractTemplateVersionStatus.Enabled)
|
if (input.Status != "enable" && input.Status != "disable")
|
||||||
throw Oops.Bah("只能配置该模板的已启用合同版本");
|
throw Oops.Bah("合同模板状态不正确");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureScopeUnique(long companyId, long? positionId, long versionId, long id)
|
/// <summary>
|
||||||
|
/// 校验公司、部门、岗位属于同一条启用的业务层级。
|
||||||
|
/// </summary>
|
||||||
|
private async Task ValidateBusinessScope(
|
||||||
|
long companyId,
|
||||||
|
long departmentId,
|
||||||
|
List<long> positionIds)
|
||||||
{
|
{
|
||||||
var exists = await Context.Queryable<ObContractScope>()
|
var companyEnabled = await Context.Queryable<ObCompany>()
|
||||||
.AnyAsync(it => it.CompanyId == companyId
|
.AnyAsync(item => item.Id == companyId && item.Status == "enable");
|
||||||
&& it.PositionId == positionId
|
if (!companyEnabled)
|
||||||
&& it.TemplateVersionId == versionId
|
throw Oops.Bah("所属公司不存在或未启用");
|
||||||
&& it.Id != id);
|
|
||||||
if (exists)
|
var departmentEnabled = await Context.Queryable<ObDepartment>()
|
||||||
throw Oops.Bah("该公司、岗位和合同版本的适用范围已存在");
|
.AnyAsync(item => item.Id == departmentId
|
||||||
|
&& item.CompanyId == companyId
|
||||||
|
&& item.Status == "enable");
|
||||||
|
if (!departmentEnabled)
|
||||||
|
throw Oops.Bah("所属部门不存在、未启用或不属于该公司");
|
||||||
|
|
||||||
|
var positionCount = await Context.Queryable<ObPosition>()
|
||||||
|
.CountAsync(item => positionIds.Contains(item.Id)
|
||||||
|
&& item.CompanyId == companyId
|
||||||
|
&& item.DepartmentId == departmentId
|
||||||
|
&& item.Status == "enable");
|
||||||
|
if (positionCount != positionIds.Count)
|
||||||
|
throw Oops.Bah("存在未启用或不属于该公司、部门的岗位");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 防止修改合同模板归属后破坏已经保存的入职模板配置。
|
||||||
|
/// </summary>
|
||||||
|
private async Task ValidateReferencedOnboardingTemplates(
|
||||||
|
OnboardingContractTemplateEditInput input,
|
||||||
|
string currentStatus)
|
||||||
|
{
|
||||||
|
var relations = await Context.Queryable<ObOnboardingTemplateContract>()
|
||||||
|
.Where(item => item.ContractTemplateId == input.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
if (relations.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var onboardingTemplateIds = relations
|
||||||
|
.Select(item => item.OnboardingTemplateId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var onboardingTemplates = await Context.Queryable<ObOnboardingTemplate>()
|
||||||
|
.Where(item => onboardingTemplateIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (string.Equals(currentStatus, "enable", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(input.Status, "enable", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var enabledTemplateNames = onboardingTemplates
|
||||||
|
.Where(item => !item.IsDelete && item.Status == "enable")
|
||||||
|
.Select(item => item.TemplateName)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(item => item)
|
||||||
|
.ToList();
|
||||||
|
if (enabledTemplateNames.Count > 0)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"该签署材料模板正在被启用中的入职模板{FormatTemplateNames(enabledTemplateNames)}引用,不能停用,请先调整或停用这些入职模板");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var positionRelations = await Context.Queryable<ObOnboardingTemplatePosition>()
|
||||||
|
.Where(item => onboardingTemplateIds.Contains(item.OnboardingTemplateId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var template in onboardingTemplates)
|
||||||
|
{
|
||||||
|
var positionIds = positionRelations
|
||||||
|
.Where(item => item.OnboardingTemplateId == template.Id)
|
||||||
|
.Select(item => item.PositionId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var departmentMatches = !template.DepartmentId.HasValue
|
||||||
|
|| template.DepartmentId.Value == input.DepartmentId;
|
||||||
|
var scopeMatches = template.CompanyId == input.CompanyId
|
||||||
|
&& departmentMatches
|
||||||
|
&& positionIds.All(input.PositionIds.Contains);
|
||||||
|
if (!scopeMatches)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同模板仍被入职模板“{template.TemplateName}”引用,当前归属调整会使其失效,请先调整入职模板");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<string>> GetEnabledOnboardingTemplateNames(
|
||||||
|
long contractTemplateId,
|
||||||
|
long? contractTemplateVersionId = null)
|
||||||
|
{
|
||||||
|
var relations = await Context.Queryable<ObOnboardingTemplateContract>()
|
||||||
|
.Where(item => item.ContractTemplateId == contractTemplateId)
|
||||||
|
.WhereIF(contractTemplateVersionId.HasValue,
|
||||||
|
item => item.ContractTemplateVersionId == contractTemplateVersionId.Value)
|
||||||
|
.ToListAsync();
|
||||||
|
if (relations.Count == 0)
|
||||||
|
return new List<string>();
|
||||||
|
|
||||||
|
var onboardingTemplateIds = relations
|
||||||
|
.Select(item => item.OnboardingTemplateId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var names = await Context.Queryable<ObOnboardingTemplate>()
|
||||||
|
.Where(item => onboardingTemplateIds.Contains(item.Id)
|
||||||
|
&& !item.IsDelete
|
||||||
|
&& item.Status == "enable")
|
||||||
|
.Select(item => item.TemplateName)
|
||||||
|
.ToListAsync();
|
||||||
|
return names
|
||||||
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(item => item)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatTemplateNames(IEnumerable<string> templateNames)
|
||||||
|
{
|
||||||
|
return string.Join("、", templateNames.Select(item => $"“{item}”"));
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 构建合同模板主档案实体。
|
||||||
|
/// </summary>
|
||||||
|
private static ObContractTemplate BuildEntity(
|
||||||
|
long templateId,
|
||||||
|
OnboardingContractTemplateAddInput input)
|
||||||
|
{
|
||||||
|
return new ObContractTemplate
|
||||||
|
{
|
||||||
|
Id = templateId,
|
||||||
|
CompanyId = input.CompanyId,
|
||||||
|
DepartmentId = input.DepartmentId,
|
||||||
|
TemplateCode = input.TemplateCode,
|
||||||
|
TemplateName = input.TemplateName,
|
||||||
|
ContractType = input.ContractType,
|
||||||
|
Description = input.Description,
|
||||||
|
Status = input.Status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存合同模板适用岗位关系。
|
||||||
|
/// </summary>
|
||||||
|
private async Task InsertPositionRelations(long templateId, List<long> positionIds)
|
||||||
|
{
|
||||||
|
var relations = positionIds.Select(positionId => new ObContractTemplatePosition
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
ContractTemplateId = templateId,
|
||||||
|
PositionId = positionId
|
||||||
|
}).ToList();
|
||||||
|
await Context.Insertable(relations).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 回填合同模板的公司、部门和适用岗位展示信息。
|
||||||
|
/// </summary>
|
||||||
|
private async Task PopulateScopeDisplay(List<ObContractTemplate> templates)
|
||||||
|
{
|
||||||
|
if (templates.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var templateIds = templates.Select(item => item.Id).ToList();
|
||||||
|
var companyIds = templates
|
||||||
|
.Where(item => item.CompanyId.HasValue)
|
||||||
|
.Select(item => item.CompanyId.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var departmentIds = templates
|
||||||
|
.Where(item => item.DepartmentId.HasValue)
|
||||||
|
.Select(item => item.DepartmentId.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var companies = companyIds.Count == 0
|
||||||
|
? new List<ObCompany>()
|
||||||
|
: await Context.Queryable<ObCompany>()
|
||||||
|
.Where(item => companyIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var departments = departmentIds.Count == 0
|
||||||
|
? new List<ObDepartment>()
|
||||||
|
: await Context.Queryable<ObDepartment>()
|
||||||
|
.Where(item => departmentIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var relations = await Context.Queryable<ObContractTemplatePosition>()
|
||||||
|
.Where(item => templateIds.Contains(item.ContractTemplateId))
|
||||||
|
.ToListAsync();
|
||||||
|
var positionIds = relations.Select(item => item.PositionId).Distinct().ToList();
|
||||||
|
var positions = positionIds.Count == 0
|
||||||
|
? new List<ObPosition>()
|
||||||
|
: await Context.Queryable<ObPosition>()
|
||||||
|
.Where(item => positionIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var template in templates)
|
||||||
|
{
|
||||||
|
template.CompanyName = companies
|
||||||
|
.FirstOrDefault(item => item.Id == template.CompanyId)?.CompanyName;
|
||||||
|
template.DepartmentName = departments
|
||||||
|
.FirstOrDefault(item => item.Id == template.DepartmentId)?.DepartmentName;
|
||||||
|
template.PositionIds = relations
|
||||||
|
.Where(item => item.ContractTemplateId == template.Id)
|
||||||
|
.Select(item => item.PositionId)
|
||||||
|
.ToList();
|
||||||
|
template.PositionNames = template.PositionIds
|
||||||
|
.Select(positionId => positions.FirstOrDefault(item => item.Id == positionId)?.PositionName)
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验任务合同变量的必填状态和员工动态变量数据类型。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingContractVariableValidationService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 校验指定任务的全部合同变量快照。
|
||||||
|
/// </summary>
|
||||||
|
Task Validate(long taskId);
|
||||||
|
}
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一校验任务合同变量,确保最终提交的数据可以安全用于合同生成。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractVariableValidationService
|
||||||
|
: IOnboardingContractVariableValidationService
|
||||||
|
{
|
||||||
|
private readonly SqlSugarScope _db;
|
||||||
|
|
||||||
|
public OnboardingContractVariableValidationService()
|
||||||
|
{
|
||||||
|
_db = DbContext.DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Validate(long taskId)
|
||||||
|
{
|
||||||
|
var contracts = await _db.Queryable<ObTaskContract>()
|
||||||
|
.Where(item => item.TaskId == taskId)
|
||||||
|
.ToListAsync();
|
||||||
|
if (contracts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var contractIds = contracts
|
||||||
|
.Select(item => item.Id)
|
||||||
|
.ToList();
|
||||||
|
var templateVersionIds = contracts
|
||||||
|
.Select(item => item.TemplateVersionId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var variables = await _db.Queryable<ObTaskContractVariable>()
|
||||||
|
.Where(item => contractIds.Contains(item.TaskContractId))
|
||||||
|
.ToListAsync();
|
||||||
|
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||||
|
.Where(item => templateVersionIds.Contains(item.TemplateVersionId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var contract in contracts)
|
||||||
|
{
|
||||||
|
var contractDefinitions = definitions
|
||||||
|
.Where(item => item.TemplateVersionId == contract.TemplateVersionId)
|
||||||
|
.OrderBy(item => item.SortCode);
|
||||||
|
|
||||||
|
foreach (var definition in contractDefinitions)
|
||||||
|
{
|
||||||
|
var variable = variables.FirstOrDefault(item =>
|
||||||
|
item.TaskContractId == contract.Id
|
||||||
|
&& item.VariableCode == definition.VariableCode);
|
||||||
|
var value = variable?.VariableValue?.Trim();
|
||||||
|
|
||||||
|
if (definition.Required && string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contract.ContractName}”的必填变量“{definition.VariableName}”尚未填写");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value)
|
||||||
|
|| definition.VariableType != ContractVariableType.Employee)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateDataType(contract.ContractName, definition, value);
|
||||||
|
ValidateOptions(contract.ContractName, definition, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateDataType(
|
||||||
|
string contractName,
|
||||||
|
ObTemplateVariable definition,
|
||||||
|
string value)
|
||||||
|
{
|
||||||
|
var dataType = definition.DataType?.Trim().ToLowerInvariant() ?? "string";
|
||||||
|
switch (dataType)
|
||||||
|
{
|
||||||
|
case "string":
|
||||||
|
return;
|
||||||
|
case "number":
|
||||||
|
var numberIsValid = decimal.TryParse(
|
||||||
|
value,
|
||||||
|
global::System.Globalization.NumberStyles.Number,
|
||||||
|
global::System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out _);
|
||||||
|
if (!numberIsValid)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”必须填写数字");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
case "date":
|
||||||
|
var dateIsValid = DateTime.TryParseExact(
|
||||||
|
value,
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
global::System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
global::System.Globalization.DateTimeStyles.None,
|
||||||
|
out _);
|
||||||
|
if (!dateIsValid)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”必须填写正确日期");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”数据类型配置不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateOptions(
|
||||||
|
string contractName,
|
||||||
|
ObTemplateVariable definition,
|
||||||
|
string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(definition.OptionsJson))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var options = ParseOptions(contractName, definition);
|
||||||
|
if (options.Contains(value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”必须从配置选项中选择");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<string> ParseOptions(
|
||||||
|
string contractName,
|
||||||
|
ObTemplateVariable definition)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = global::System.Text.Json.JsonDocument.Parse(
|
||||||
|
definition.OptionsJson);
|
||||||
|
if (document.RootElement.ValueKind != global::System.Text.Json.JsonValueKind.Array)
|
||||||
|
throw new global::System.Text.Json.JsonException();
|
||||||
|
|
||||||
|
var options = document.RootElement
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(GetOptionValue)
|
||||||
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
if (options.Count == 0)
|
||||||
|
throw new global::System.Text.Json.JsonException();
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
catch (global::System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”可选项配置不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetOptionValue(
|
||||||
|
global::System.Text.Json.JsonElement option)
|
||||||
|
{
|
||||||
|
if (option.ValueKind == global::System.Text.Json.JsonValueKind.String)
|
||||||
|
return option.GetString()?.Trim();
|
||||||
|
|
||||||
|
if (option.ValueKind == global::System.Text.Json.JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
if (option.TryGetProperty("value", out var value))
|
||||||
|
return GetJsonText(value);
|
||||||
|
if (option.TryGetProperty("label", out var label))
|
||||||
|
return GetJsonText(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetJsonText(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetJsonText(
|
||||||
|
global::System.Text.Json.JsonElement value)
|
||||||
|
{
|
||||||
|
return value.ValueKind switch
|
||||||
|
{
|
||||||
|
global::System.Text.Json.JsonValueKind.String => value.GetString()?.Trim(),
|
||||||
|
global::System.Text.Json.JsonValueKind.Number => value.ToString(),
|
||||||
|
global::System.Text.Json.JsonValueKind.True => bool.TrueString,
|
||||||
|
global::System.Text.Json.JsonValueKind.False => bool.FalseString,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务部门分页查询参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingDepartmentPageInput : BasePageInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 部门编码。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string DepartmentCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 部门名称。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string DepartmentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增入职业务部门参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingDepartmentAddInput : ObDepartment
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属公司不能为空")]
|
||||||
|
public new long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 部门业务编码。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "部门编码不能为空")]
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(50, ErrorMessage = "部门编码不能超过50个字符")]
|
||||||
|
public override string DepartmentCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 部门名称。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "部门名称不能为空")]
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(50, ErrorMessage = "部门名称不能超过50个字符")]
|
||||||
|
public override string DepartmentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(200, ErrorMessage = "备注不能超过200个字符")]
|
||||||
|
public override string Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑入职业务部门参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingDepartmentEditInput : OnboardingDepartmentAddInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "部门Id不能为空")]
|
||||||
|
public override long Id { get; set; }
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义入职业务部门业务能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingDepartmentService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询入职业务部门。
|
||||||
|
/// </summary>
|
||||||
|
Task<SqlSugarPagedList<ObDepartment>> Page(OnboardingDepartmentPageInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询指定公司下全部已启用部门列表。
|
||||||
|
/// </summary>
|
||||||
|
Task<List<ObDepartment>> ListEnabled(long companyId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询部门详情。
|
||||||
|
/// </summary>
|
||||||
|
Task<ObDepartment> Detail(BaseIdInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增入职业务部门。
|
||||||
|
/// </summary>
|
||||||
|
Task Add(OnboardingDepartmentAddInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑入职业务部门。
|
||||||
|
/// </summary>
|
||||||
|
Task Edit(OnboardingDepartmentEditInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量删除部门。
|
||||||
|
/// </summary>
|
||||||
|
Task Delete(BaseIdListInput input);
|
||||||
|
}
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现入职业务部门业务能力。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingDepartmentService : DbRepository<ObDepartment>, IOnboardingDepartmentService
|
||||||
|
{
|
||||||
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
|
public OnboardingDepartmentService(IOnboardingDataPermissionService dataPermission)
|
||||||
|
{
|
||||||
|
_dataPermission = dataPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询入职业务部门。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<SqlSugarPagedList<ObDepartment>> Page(OnboardingDepartmentPageInput input)
|
||||||
|
{
|
||||||
|
var list = await Context.Queryable<ObDepartment>()
|
||||||
|
.WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value)
|
||||||
|
.WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentCode), it => it.DepartmentCode.Contains(input.DepartmentCode))
|
||||||
|
.WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentName), it => it.DepartmentName.Contains(input.DepartmentName))
|
||||||
|
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
||||||
|
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), input.GetSafeOrderBy("Id", "Status", "CreateTime", "UpdateTime", "CreateUser", "CreateUserId", "CompanyId", "DepartmentCode", "DepartmentName", "SortCode"))
|
||||||
|
.OrderBy(it => it.SortCode)
|
||||||
|
.OrderBy(it => it.DepartmentName)
|
||||||
|
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||||
|
|
||||||
|
if (list.List != null && list.List.Any())
|
||||||
|
{
|
||||||
|
var companyIds = list.List.Select(d => d.CompanyId).Distinct().ToList();
|
||||||
|
var companies = await Context.Queryable<ObCompany>()
|
||||||
|
.Where(c => companyIds.Contains(c.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var companyMap = companies.ToDictionary(c => c.Id, c => c.CompanyName);
|
||||||
|
|
||||||
|
foreach (var item in list.List)
|
||||||
|
{
|
||||||
|
if (companyMap.TryGetValue(item.CompanyId, out var cName))
|
||||||
|
{
|
||||||
|
item.CompanyName = cName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询指定公司下全部已启用部门列表。
|
||||||
|
/// </summary>
|
||||||
|
public Task<List<ObDepartment>> ListEnabled(long companyId)
|
||||||
|
{
|
||||||
|
return Context.Queryable<ObDepartment>()
|
||||||
|
.Where(it => it.CompanyId == companyId && it.Status == "enable")
|
||||||
|
.OrderBy(it => it.SortCode)
|
||||||
|
.OrderBy(it => it.DepartmentName)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询部门详情。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ObDepartment> Detail(BaseIdInput input)
|
||||||
|
{
|
||||||
|
return await GetByIdAsync(input.Id) ?? throw Oops.Bah("部门不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增入职业务部门。
|
||||||
|
/// </summary>
|
||||||
|
public async Task Add(OnboardingDepartmentAddInput input)
|
||||||
|
{
|
||||||
|
NormalizeInput(input);
|
||||||
|
await EnsureCompanyExists(input.CompanyId);
|
||||||
|
await EnsureCodeUnique(input.CompanyId, input.DepartmentCode, 0);
|
||||||
|
await InsertAsync(input.Adapt<ObDepartment>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑入职业务部门。
|
||||||
|
/// </summary>
|
||||||
|
public async Task Edit(OnboardingDepartmentEditInput input)
|
||||||
|
{
|
||||||
|
NormalizeInput(input);
|
||||||
|
var department = await Detail(new BaseIdInput { Id = input.Id });
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
department.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.SetupManageAll,
|
||||||
|
"部门");
|
||||||
|
await EnsureCompanyCanChange(department, input.CompanyId);
|
||||||
|
await EnsureCompanyExists(input.CompanyId);
|
||||||
|
await EnsureCodeUnique(input.CompanyId, input.DepartmentCode, input.Id);
|
||||||
|
await UpdateAsync(input.Adapt<ObDepartment>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被岗位或任务引用的部门。
|
||||||
|
/// </summary>
|
||||||
|
public async Task Delete(BaseIdListInput input)
|
||||||
|
{
|
||||||
|
if (input.Ids.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var departments = await Context.Queryable<ObDepartment>()
|
||||||
|
.Where(item => input.Ids.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (departments.Count != input.Ids.Distinct().Count())
|
||||||
|
throw Oops.Bah("部门不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
departments.Select(item => item.CreateUserId),
|
||||||
|
OnboardingPermissionCodes.SetupManageAll,
|
||||||
|
"部门");
|
||||||
|
|
||||||
|
var hasPositions = await Context.Queryable<ObPosition>()
|
||||||
|
.AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
|
||||||
|
if (hasPositions)
|
||||||
|
throw Oops.Bah("部门下已有岗位,不能删除");
|
||||||
|
|
||||||
|
var hasTasks = await Context.Queryable<ObOnboardingTask>()
|
||||||
|
.AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
|
||||||
|
if (hasTasks)
|
||||||
|
throw Oops.Bah("部门已有入职任务,不能删除");
|
||||||
|
|
||||||
|
var hasEmployees = await Context.Queryable<ObEmployee>()
|
||||||
|
.AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
|
||||||
|
if (hasEmployees)
|
||||||
|
throw Oops.Bah("部门已有正式员工,不能删除");
|
||||||
|
|
||||||
|
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplate>()
|
||||||
|
.AnyAsync(item => item.DepartmentId.HasValue && input.Ids.Contains(item.DepartmentId.Value));
|
||||||
|
if (hasOnboardingTemplates)
|
||||||
|
throw Oops.Bah("部门已被入职模板引用,不能删除");
|
||||||
|
|
||||||
|
var hasContractTemplates = await Context.Queryable<ObContractTemplate>()
|
||||||
|
.AnyAsync(item => item.DepartmentId.HasValue && input.Ids.Contains(item.DepartmentId.Value));
|
||||||
|
if (hasContractTemplates)
|
||||||
|
throw Oops.Bah("部门已被合同模板引用,不能删除");
|
||||||
|
|
||||||
|
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureCompanyExists(long companyId)
|
||||||
|
{
|
||||||
|
var exists = await Context.Queryable<ObCompany>()
|
||||||
|
.AnyAsync(it => it.Id == companyId && it.Status == "enable");
|
||||||
|
if (!exists)
|
||||||
|
throw Oops.Bah("所属公司不存在、已删除或未启用");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准化部门输入并拒绝非法状态。
|
||||||
|
/// </summary>
|
||||||
|
private static void NormalizeInput(OnboardingDepartmentAddInput input)
|
||||||
|
{
|
||||||
|
input.DepartmentCode = OnboardingCodeValidator.Normalize(input.DepartmentCode, "部门编码", 50);
|
||||||
|
input.DepartmentName = input.DepartmentName?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(input.DepartmentName))
|
||||||
|
{
|
||||||
|
throw Oops.Bah("请填写部门名称");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Status != "enable" && input.Status != "disable")
|
||||||
|
throw Oops.Bah("部门状态不正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已产生下级或业务引用的部门不允许直接变更所属公司。
|
||||||
|
/// </summary>
|
||||||
|
private async Task EnsureCompanyCanChange(ObDepartment department, long companyId)
|
||||||
|
{
|
||||||
|
if (department.CompanyId == companyId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var hasPosition = await Context.Queryable<ObPosition>()
|
||||||
|
.AnyAsync(item => item.DepartmentId == department.Id);
|
||||||
|
var hasTask = await Context.Queryable<ObOnboardingTask>()
|
||||||
|
.AnyAsync(item => item.DepartmentId == department.Id);
|
||||||
|
var hasEmployee = await Context.Queryable<ObEmployee>()
|
||||||
|
.AnyAsync(item => item.DepartmentId == department.Id);
|
||||||
|
var hasOnboardingTemplate = await Context.Queryable<ObOnboardingTemplate>()
|
||||||
|
.AnyAsync(item => item.DepartmentId == department.Id);
|
||||||
|
var hasContractTemplate = await Context.Queryable<ObContractTemplate>()
|
||||||
|
.AnyAsync(item => item.DepartmentId == department.Id);
|
||||||
|
if (hasPosition || hasTask || hasEmployee || hasOnboardingTemplate || hasContractTemplate)
|
||||||
|
throw Oops.Bah("部门已有岗位、模板、任务或员工引用,不能直接变更所属公司");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureCodeUnique(long companyId, string code, long id)
|
||||||
|
{
|
||||||
|
var exists = await Context.Queryable<ObDepartment>()
|
||||||
|
.AnyAsync(it => it.CompanyId == companyId && it.DepartmentCode == code && it.Id != id);
|
||||||
|
if (exists)
|
||||||
|
throw Oops.Bah("该公司下部门编码已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
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 OnboardingElectronicSignPreviewInput : OnboardingElectronicSignTaskInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本地电子签文档 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "电子签文档Id不能为空")]
|
||||||
|
public long SignDocumentId { 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; }
|
||||||
|
}
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
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 OnboardingElectronicSignPreviewOutput
|
||||||
|
{
|
||||||
|
/// <summary>本地电子签文档 Id。</summary>
|
||||||
|
public long SignDocumentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁生成的只读浏览页面地址。</summary>
|
||||||
|
public string PreviewUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
|
||||||
|
/// <summary>当前管理员流程中是否已生成过员工签后预览。</summary>
|
||||||
|
public bool EmployeeSignedPreviewed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本份文件是否需要公司盖章。</summary>
|
||||||
|
public bool CompanySealEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本份文件是否需要骑缝章。</summary>
|
||||||
|
public bool AcrossPageSealEnabled { get; set; }
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
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<OnboardingElectronicSignPreviewOutput> PreviewEmployeeSignedDocument(
|
||||||
|
OnboardingElectronicSignPreviewInput input);
|
||||||
|
|
||||||
|
/// <summary>确认全部文件已核对后,由后台执行企业盖章。</summary>
|
||||||
|
Task<OnboardingElectronicSignDetailOutput> CompanySeal(
|
||||||
|
OnboardingElectronicSignTaskInput input);
|
||||||
|
|
||||||
|
/// <summary>催办当前待签员工。</summary>
|
||||||
|
Task Notice(OnboardingElectronicSignTaskInput input);
|
||||||
|
|
||||||
|
/// <summary>作废尚未结束的签约流程。</summary>
|
||||||
|
Task Invalid(OnboardingElectronicSignInvalidInput input);
|
||||||
|
|
||||||
|
/// <summary>处理契约锁服务器明文回调。</summary>
|
||||||
|
Task HandleCallback(string payloadJson);
|
||||||
|
}
|
||||||
+1686
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>同步第三方状态时的单向迁移保护;未知/陈旧结果不能覆盖已确认的签署进度。</summary>
|
||||||
|
internal static class OnboardingSignStateGuard
|
||||||
|
{
|
||||||
|
internal static string Resolve(string current, string incoming)
|
||||||
|
{
|
||||||
|
if (current == ElectronicSignStatus.Archived && incoming == ElectronicSignStatus.Completed)
|
||||||
|
return ElectronicSignStatus.Archived;
|
||||||
|
if (IsTerminal(current) && current != incoming)
|
||||||
|
throw Oops.Bah("契约锁返回状态与本地终态不一致,已保留本地状态,请核实后重试");
|
||||||
|
if (current == ElectronicSignStatus.PendingCompanySeal
|
||||||
|
&& incoming is not (ElectronicSignStatus.PendingCompanySeal
|
||||||
|
or ElectronicSignStatus.Completed or ElectronicSignStatus.Archived
|
||||||
|
or ElectronicSignStatus.Rejected or ElectronicSignStatus.Invalid or ElectronicSignStatus.Expired))
|
||||||
|
throw Oops.Bah("契约锁状态暂未同步,已保留员工签署结果,请稍后重试");
|
||||||
|
return incoming;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTerminal(string status)
|
||||||
|
=> status is ElectronicSignStatus.Completed or ElectronicSignStatus.Archived
|
||||||
|
or ElectronicSignStatus.Rejected or ElectronicSignStatus.Invalid or ElectronicSignStatus.Expired;
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
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<string> GetViewPageUrl(
|
||||||
|
QiyuesuoViewPageInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询契约锁合同详情。
|
||||||
|
/// </summary>
|
||||||
|
Task<QiyuesuoContractResult> GetContractDetail(string contractId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 催办当前待签署人。
|
||||||
|
/// </summary>
|
||||||
|
Task Notice(string contractId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 由后台管理员确认后执行企业盖章节点。
|
||||||
|
/// </summary>
|
||||||
|
Task CompanySign(QiyuesuoCompanySignInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 作废尚未完成的合同流程。
|
||||||
|
/// </summary>
|
||||||
|
Task Invalid(string contractId, string sealId, string reason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载单份签署完成的 PDF 文档。
|
||||||
|
/// </summary>
|
||||||
|
Task<byte[]> DownloadDocument(string documentId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载合同原文、签署文件与签署日志组成的完整归档 ZIP。
|
||||||
|
/// </summary>
|
||||||
|
Task<byte[]> DownloadArchive(string contractId);
|
||||||
|
}
|
||||||
+667
@@ -0,0 +1,667 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
return await PostJson<QiyuesuoContractResult>(
|
||||||
|
"/v2/contract/draft",
|
||||||
|
QiyuesuoContractRequestBuilder.BuildDraft(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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,
|
||||||
|
// 入职合同的签署位置由系统统一预设,禁止签署人自行增加位置,
|
||||||
|
// 避免同一份材料出现“预设签名 + 手动新增签名”的重复签名。
|
||||||
|
stamperRule = "MUST_FORBID",
|
||||||
|
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 async Task<string> GetViewPageUrl(
|
||||||
|
QiyuesuoViewPageInput input)
|
||||||
|
{
|
||||||
|
var result = await GetJsonElement(
|
||||||
|
"/v2/contract/viewurl",
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["contractId"] = input.ContractId,
|
||||||
|
["documentId"] = input.DocumentId,
|
||||||
|
["tenantName"] = input.CompanyName,
|
||||||
|
["downloadButton"] = "false",
|
||||||
|
["printButton"] = "false"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ValueKind == JsonValueKind.String)
|
||||||
|
return result.GetString();
|
||||||
|
if (TryGetString(result, "url", out var url)
|
||||||
|
|| TryGetString(result, "viewUrl", out url)
|
||||||
|
|| TryGetString(result, "pageUrl", out url))
|
||||||
|
{
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Oops.Bah("契约锁未返回合同浏览页面地址");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<QiyuesuoContractResult> GetContractDetail(string contractId)
|
||||||
|
{
|
||||||
|
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 CompanySign(QiyuesuoCompanySignInput input)
|
||||||
|
{
|
||||||
|
await PostJsonElement(
|
||||||
|
"/v2/contract/companysign",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
contractId = input.ContractId,
|
||||||
|
tenantName = input.CompanyName,
|
||||||
|
sealId = input.SealId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Invalid(string contractId, string sealId, string reason)
|
||||||
|
{
|
||||||
|
await PostJsonElement(
|
||||||
|
"/v2/contract/invalid",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
contractId,
|
||||||
|
sealId,
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var result = await GetJsonElement(path, query);
|
||||||
|
return result.Deserialize<T>(JsonOptions)
|
||||||
|
?? throw Oops.Bah("契约锁返回数据为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<JsonElement> GetJsonElement(
|
||||||
|
string path,
|
||||||
|
Dictionary<string, string> query)
|
||||||
|
{
|
||||||
|
ValidateSettings();
|
||||||
|
using var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Get,
|
||||||
|
BuildUrl(path, query));
|
||||||
|
AddAuthorizationHeaders(
|
||||||
|
request,
|
||||||
|
path,
|
||||||
|
HttpMethod.Get,
|
||||||
|
query);
|
||||||
|
return await SendForJsonResult(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+259
@@ -0,0 +1,259 @@
|
|||||||
|
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 bool RequiresCompanySign { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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,多个 Id 使用英文逗号分隔。</summary>
|
||||||
|
public string DocumentIds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>骑缝章位置指定方式。</summary>
|
||||||
|
public string AcrossType { 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; }
|
||||||
|
|
||||||
|
/// <summary>相对关键字的横向偏移量;契约锁要求必传,不偏移时为 0。</summary>
|
||||||
|
public decimal OffsetX { get; set; }
|
||||||
|
|
||||||
|
/// <summary>相对关键字的纵向偏移量;契约锁要求必传,不偏移时为 0。</summary>
|
||||||
|
public decimal OffsetY { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 QiyuesuoViewPageInput
|
||||||
|
{
|
||||||
|
/// <summary>契约锁合同流程 Id。</summary>
|
||||||
|
public string ContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>契约锁合同文档 Id。</summary>
|
||||||
|
public string DocumentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署企业法定名称;集团模式下用于定位子公司。</summary>
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁企业人工盖章所需参数。
|
||||||
|
/// </summary>
|
||||||
|
public class QiyuesuoCompanySignInput
|
||||||
|
{
|
||||||
|
/// <summary>契约锁合同流程 Id。</summary>
|
||||||
|
public string ContractId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>签署企业法定名称;集团模式下用于定位子公司。</summary>
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>当前流程使用的企业印章 Id 快照。</summary>
|
||||||
|
public string SealId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁合同返回对象。
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>发起签约前按文件冻结的签字、盖章要求。</summary>
|
||||||
|
internal sealed record OnboardingSigningDocument(
|
||||||
|
string DocumentId,
|
||||||
|
string EmployeeSignKeyword,
|
||||||
|
string CompanySealKeyword,
|
||||||
|
bool? CompanySealEnabled,
|
||||||
|
bool? AcrossPageSealEnabled);
|
||||||
|
|
||||||
|
/// <summary>构造契约锁请求;无数据库和网络访问,员工签字始终覆盖全部文件1。</summary>
|
||||||
|
internal static class QiyuesuoContractRequestBuilder
|
||||||
|
{
|
||||||
|
internal static bool CompanySealEnabled(bool? enabled) => enabled ?? true;
|
||||||
|
|
||||||
|
internal static bool AcrossPageSealEnabled(bool? enabled, QiyuesuoSettingOptions setting) =>
|
||||||
|
enabled ?? setting.EnableAcrossPageSeal;
|
||||||
|
|
||||||
|
internal static bool RequiresCompanySign(
|
||||||
|
bool? companySealEnabled,
|
||||||
|
bool? acrossPageSealEnabled,
|
||||||
|
QiyuesuoSettingOptions setting) =>
|
||||||
|
CompanySealEnabled(companySealEnabled) || AcrossPageSealEnabled(acrossPageSealEnabled, setting);
|
||||||
|
|
||||||
|
internal static object BuildDraft(QiyuesuoCreateDraftInput input)
|
||||||
|
{
|
||||||
|
long sealId = 0;
|
||||||
|
if (input.RequiresCompanySign && !long.TryParse(input.SealId, out sealId))
|
||||||
|
throw Oops.Bah("契约锁 SealId 配置格式不正确");
|
||||||
|
|
||||||
|
var signatories = new List<object>
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
tenantType = "PERSONAL",
|
||||||
|
tenantName = input.EmployeeName,
|
||||||
|
receiver = new
|
||||||
|
{
|
||||||
|
name = input.EmployeeName,
|
||||||
|
contact = input.EmployeeMobile,
|
||||||
|
contactType = "MOBILE"
|
||||||
|
},
|
||||||
|
serialNo = 1,
|
||||||
|
signatoryNo = "employee",
|
||||||
|
actions = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
type = "PERSONAL",
|
||||||
|
name = "员工本人签署",
|
||||||
|
serialNo = 0,
|
||||||
|
actionNo = "employee_sign"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (input.RequiresCompanySign)
|
||||||
|
{
|
||||||
|
signatories.Add(new
|
||||||
|
{
|
||||||
|
tenantType = "COMPANY",
|
||||||
|
tenantName = input.CompanyName,
|
||||||
|
receiver = new
|
||||||
|
{
|
||||||
|
name = input.CreatorName,
|
||||||
|
contact = input.CreatorMobile,
|
||||||
|
contactType = "MOBILE"
|
||||||
|
},
|
||||||
|
serialNo = 2,
|
||||||
|
signatoryNo = "company",
|
||||||
|
actions = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
type = "COMPANY",
|
||||||
|
name = "企业人工盖章",
|
||||||
|
serialNo = 0,
|
||||||
|
autoSign = false,
|
||||||
|
sealId = input.SealId,
|
||||||
|
corpSealIds = new[] { sealId },
|
||||||
|
actionNo = "company_seal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
bizId = input.BizId,
|
||||||
|
subject = input.Subject,
|
||||||
|
tenantName = input.CompanyName,
|
||||||
|
description = "OnboardX 入职电子签",
|
||||||
|
send = false,
|
||||||
|
callbackUrl = string.IsNullOrWhiteSpace(input.CallbackUrl) ? null : input.CallbackUrl.Trim(),
|
||||||
|
category = new { id = input.CategoryId },
|
||||||
|
creator = new
|
||||||
|
{
|
||||||
|
name = input.CreatorName,
|
||||||
|
contact = input.CreatorMobile,
|
||||||
|
contactType = "MOBILE"
|
||||||
|
},
|
||||||
|
signatories
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static List<QiyuesuoStamperInput> BuildStampers(
|
||||||
|
IReadOnlyList<OnboardingSigningDocument> documents,
|
||||||
|
string personalSignatoryId,
|
||||||
|
string companyActionId,
|
||||||
|
string sealId,
|
||||||
|
QiyuesuoSettingOptions setting)
|
||||||
|
{
|
||||||
|
if (documents.Count == 0)
|
||||||
|
throw Oops.Bah("当前电子签流程没有可签署的文件");
|
||||||
|
if (documents.Any(item => string.IsNullOrWhiteSpace(item.DocumentId)))
|
||||||
|
throw Oops.Bah("契约锁未返回签署文档 Id");
|
||||||
|
if (string.IsNullOrWhiteSpace(personalSignatoryId))
|
||||||
|
throw Oops.Bah("契约锁未返回员工签署方 Id");
|
||||||
|
if (documents.Any(item => RequiresCompanySign(item.CompanySealEnabled, item.AcrossPageSealEnabled, setting))
|
||||||
|
&& string.IsNullOrWhiteSpace(companyActionId))
|
||||||
|
throw Oops.Bah("契约锁草稿未返回企业盖章动作");
|
||||||
|
|
||||||
|
var stampers = new List<QiyuesuoStamperInput>();
|
||||||
|
foreach (var document in documents)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(document.EmployeeSignKeyword))
|
||||||
|
throw Oops.Bah("请配置员工签名定位关键字");
|
||||||
|
stampers.Add(new QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
SignatoryId = personalSignatoryId,
|
||||||
|
Type = "PERSONAL",
|
||||||
|
DocumentId = document.DocumentId,
|
||||||
|
Keyword = document.EmployeeSignKeyword,
|
||||||
|
KeywordIndex = Math.Max(1, setting.EmployeeSignKeywordIndex)
|
||||||
|
});
|
||||||
|
if (CompanySealEnabled(document.CompanySealEnabled))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(document.CompanySealKeyword))
|
||||||
|
throw Oops.Bah("需要公司盖章的文件未配置企业盖章定位关键字");
|
||||||
|
stampers.Add(new QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
ActionId = companyActionId,
|
||||||
|
Type = "COMPANY",
|
||||||
|
DocumentId = document.DocumentId,
|
||||||
|
SealId = sealId,
|
||||||
|
Keyword = document.CompanySealKeyword,
|
||||||
|
KeywordIndex = Math.Max(1, setting.CompanySealKeywordIndex),
|
||||||
|
OffsetX = setting.CompanySealOffsetX,
|
||||||
|
OffsetY = setting.CompanySealOffsetY
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并骑缝章也只能包含已开启该开关的文件,不能把只需个人签字的材料混入。
|
||||||
|
var acrossDocuments = documents
|
||||||
|
.Where(item => AcrossPageSealEnabled(item.AcrossPageSealEnabled, setting))
|
||||||
|
.Select(item => item.DocumentId)
|
||||||
|
.ToList();
|
||||||
|
if (acrossDocuments.Count == 0)
|
||||||
|
return stampers;
|
||||||
|
var acrossType = setting.AcrossPageSealType?.Trim().ToUpperInvariant();
|
||||||
|
if (acrossType is not ("ALL_DOCUMENT" or "SINGLE_DOCUMENT"))
|
||||||
|
throw Oops.Bah("契约锁骑缝章模式只能配置为 ALL_DOCUMENT 或 SINGLE_DOCUMENT");
|
||||||
|
if (acrossType == "ALL_DOCUMENT")
|
||||||
|
{
|
||||||
|
stampers.Add(new QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
ActionId = companyActionId,
|
||||||
|
Type = "ACROSS_PAGE",
|
||||||
|
DocumentId = acrossDocuments[0],
|
||||||
|
DocumentIds = string.Join(",", acrossDocuments),
|
||||||
|
SealId = sealId,
|
||||||
|
AcrossType = acrossType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var documentId in acrossDocuments)
|
||||||
|
{
|
||||||
|
stampers.Add(new QiyuesuoStamperInput
|
||||||
|
{
|
||||||
|
ActionId = companyActionId,
|
||||||
|
Type = "ACROSS_PAGE",
|
||||||
|
DocumentId = documentId,
|
||||||
|
SealId = sealId,
|
||||||
|
AcrossType = acrossType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stampers;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string ResolvePendingStatus(QiyuesuoContractResult contract)
|
||||||
|
{
|
||||||
|
var employeeSigned = contract.Signatories.Any(item =>
|
||||||
|
string.Equals(item.TenantType, "PERSONAL", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(item.Status, "SIGNED", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var hasCompanyAction = contract.Signatories.Any(item =>
|
||||||
|
string.Equals(item.TenantType, "COMPANY", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& item.Actions.Any(action => string.Equals(action.Type, "COMPANY", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
// 纯员工签字流程等待平台 COMPLETE 回执,不制造不存在的企业盖章阶段。
|
||||||
|
return employeeSigned && hasCompanyAction
|
||||||
|
? ElectronicSignStatus.PendingCompanySeal
|
||||||
|
: ElectronicSignStatus.PendingEmployeeSign;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,218 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正式员工档案分页查询参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingEmployeePageInput : BasePageInput
|
public class OnboardingEmployeePageInput : BasePageInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 员工姓名。
|
||||||
|
/// </summary>
|
||||||
public string EmployeeName { get; set; }
|
public string EmployeeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号码。
|
||||||
|
/// </summary>
|
||||||
public string Phone { get; set; }
|
public string Phone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
public long? CompanyId { get; set; }
|
public long? CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位 Id。
|
||||||
|
/// </summary>
|
||||||
public long? PositionId { get; set; }
|
public long? PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用工类型。
|
||||||
|
/// </summary>
|
||||||
|
public string EmploymentType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转正状态。
|
||||||
|
/// </summary>
|
||||||
|
public string RegularizationStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 社保状态。
|
||||||
|
/// </summary>
|
||||||
|
public string SocialInsuranceStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同即将到期筛选(30天内到期或已到期)。
|
||||||
|
/// </summary>
|
||||||
|
public bool? ContractExpiringSoon { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正式员工档案分页数据及当前查询条件下的维度统计。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingEmployeePageOutput : SqlSugarPagedList<ObEmployee>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 不受快捷维度筛选影响的员工维度统计。
|
||||||
|
/// </summary>
|
||||||
|
public OnboardingEmployeeStatisticsOutput Statistics { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正式员工档案维度统计。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingEmployeeStatisticsOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 在册总数。
|
||||||
|
/// </summary>
|
||||||
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正式员工数量。
|
||||||
|
/// </summary>
|
||||||
|
public int FullTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兼职员工数量。
|
||||||
|
/// </summary>
|
||||||
|
public int PartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 试用期待转正数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Probation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 社保已缴纳数量。
|
||||||
|
/// </summary>
|
||||||
|
public int SocialPurchased { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同即将到期数量(30天内到期或已到期)。
|
||||||
|
/// </summary>
|
||||||
|
public int ExpiringContract { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑员工人事信息。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingEmployeeHrUpdateInput
|
||||||
|
{
|
||||||
|
[IdNotNull(ErrorMessage = "员工Id不能为空")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "请选择用工类型")]
|
||||||
|
public string EmploymentType { get; set; }
|
||||||
|
|
||||||
|
public string ContractNature { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "请选择社保状态")]
|
||||||
|
public string SocialInsuranceStatus { get; set; }
|
||||||
|
|
||||||
|
public string SocialInsuranceCompany { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工转正办理参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingEmployeeRegularizeInput
|
||||||
|
{
|
||||||
|
[IdNotNull(ErrorMessage = "员工Id不能为空")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "请选择转正日期")]
|
||||||
|
public DateTime? RegularizationDate { get; set; }
|
||||||
|
|
||||||
|
public string Remark { get; set; }
|
||||||
|
|
||||||
|
public List<long> FileIds { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销员工转正参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingEmployeeRegularizationRevokeInput
|
||||||
|
{
|
||||||
|
[IdNotNull(ErrorMessage = "员工Id不能为空")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "请填写撤销原因")]
|
||||||
|
public string Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正式员工档案导出结果。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingEmployeeExportFile
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Excel 文件字节。
|
||||||
|
/// </summary>
|
||||||
|
public byte[] FileBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正式员工档案详情输出。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingEmployeeDetailOutput
|
public class OnboardingEmployeeDetailOutput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 正式员工档案。
|
||||||
|
/// </summary>
|
||||||
public ObEmployee Employee { get; set; }
|
public ObEmployee Employee { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过时对应的完整入职登记表。
|
||||||
|
/// </summary>
|
||||||
|
public ObTaskEmployeeInfo EmployeeInfo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务主数据。
|
||||||
|
/// </summary>
|
||||||
public ObOnboardingTask Task { get; set; }
|
public ObOnboardingTask Task { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务合同列表。
|
||||||
|
/// </summary>
|
||||||
public List<ObTaskContract> Contracts { get; set; } = new List<ObTaskContract>();
|
public List<ObTaskContract> Contracts { get; set; } = new List<ObTaskContract>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核建档时冻结的员工合同业务快照,包含常用字段及全部自定义变量。
|
||||||
|
/// </summary>
|
||||||
|
public List<ObEmployeeContractSnapshot> ContractSnapshots { get; set; }
|
||||||
|
= new List<ObEmployeeContractSnapshot>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工入职时上传的身份证、学历证明、体检报告等资料。
|
||||||
|
/// </summary>
|
||||||
|
public List<OnboardingTaskMaterialDetailOutput> Materials { get; set; }
|
||||||
|
= new List<OnboardingTaskMaterialDetailOutput>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 契约锁签署文件与签署日志组成的完整归档 ZIP 文件 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? ElectronicSignArchiveFileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提交与审核记录列表。
|
||||||
|
/// </summary>
|
||||||
public List<ObTaskAudit> Audits { get; set; } = new List<ObTaskAudit>();
|
public List<ObTaskAudit> Audits { get; set; } = new List<ObTaskAudit>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工转正及人事维护历史。
|
||||||
|
/// </summary>
|
||||||
|
public List<ObEmployeeChangeRecord> ChangeRecords { get; set; }
|
||||||
|
= new List<ObEmployeeChangeRecord>();
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,32 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义员工档案查询和人事维护业务能力。
|
||||||
|
/// </summary>
|
||||||
public interface IOnboardingEmployeeService : ITransient
|
public interface IOnboardingEmployeeService : ITransient
|
||||||
{
|
{
|
||||||
Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input);
|
/// <summary>
|
||||||
|
/// 分页查询审核通过后生成的正式员工档案及维度统计。
|
||||||
|
/// </summary>
|
||||||
|
Task<OnboardingEmployeePageOutput> Page(OnboardingEmployeePageInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 按当前列表筛选条件导出员工明细。
|
||||||
|
/// </summary>
|
||||||
|
Task<OnboardingEmployeeExportFile> Export(OnboardingEmployeePageInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑员工用工、社保及合同性质等人事信息。
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateHrInfo(OnboardingEmployeeHrUpdateInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 人工办理员工转正。
|
||||||
|
/// </summary>
|
||||||
|
Task Regularize(OnboardingEmployeeRegularizeInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销一次员工转正,历史记录和附件继续保留。
|
||||||
|
/// </summary>
|
||||||
|
Task RevokeRegularization(OnboardingEmployeeRegularizationRevokeInput input);
|
||||||
|
/// <summary>
|
||||||
|
/// 查询正式员工及其来源任务、合同和审核记录。
|
||||||
|
/// </summary>
|
||||||
Task<OnboardingEmployeeDetailOutput> Detail(BaseIdInput input);
|
Task<OnboardingEmployeeDetailOutput> Detail(BaseIdInput input);
|
||||||
}
|
}
|
||||||
+1506
-13
File diff suppressed because it is too large
Load Diff
+10
@@ -0,0 +1,10 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义正式员工合同快照归档能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingEmployeeContractSnapshotService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>冻结一名员工来源任务中的全部合同和变量。</summary>
|
||||||
|
Task Build(long employeeId, long taskId);
|
||||||
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在正式员工建档时,将每份任务合同及其全部变量冻结为长期业务快照。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingEmployeeContractSnapshotService : IOnboardingEmployeeContractSnapshotService
|
||||||
|
{
|
||||||
|
private readonly SqlSugarScope _db;
|
||||||
|
|
||||||
|
public OnboardingEmployeeContractSnapshotService()
|
||||||
|
{
|
||||||
|
_db = DbContext.DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Build(long employeeId, long taskId)
|
||||||
|
{
|
||||||
|
var contracts = await _db.Queryable<ObTaskContract>()
|
||||||
|
.Where(item => item.TaskId == taskId)
|
||||||
|
.OrderBy(item => item.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
if (contracts.Count == 0)
|
||||||
|
throw Oops.Bah("入职任务没有合同,无法生成员工合同快照");
|
||||||
|
|
||||||
|
var contractIds = contracts.Select(item => item.Id).ToList();
|
||||||
|
var allVariables = await _db.Queryable<ObTaskContractVariable>()
|
||||||
|
.Where(item => contractIds.Contains(item.TaskContractId))
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.OrderBy(item => item.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var snapshots = new List<ObEmployeeContractSnapshot>();
|
||||||
|
|
||||||
|
foreach (var contract in contracts)
|
||||||
|
{
|
||||||
|
var variables = allVariables
|
||||||
|
.Where(item => item.TaskContractId == contract.Id)
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.ThenBy(item => item.Id)
|
||||||
|
.ToList();
|
||||||
|
var valueMap = variables
|
||||||
|
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
group => group.Key,
|
||||||
|
group => group.Last().VariableValue,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
var variableSnapshot = variables
|
||||||
|
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
group => group.Key,
|
||||||
|
group =>
|
||||||
|
{
|
||||||
|
// 历史任务若意外存在重复变量,以排序后的最后一条最终值为准,避免建档时报 500。
|
||||||
|
var item = group.Last();
|
||||||
|
return (object)new
|
||||||
|
{
|
||||||
|
variableName = item.VariableName,
|
||||||
|
variableType = item.VariableType,
|
||||||
|
dataType = item.DataType,
|
||||||
|
variableValue = item.VariableValue,
|
||||||
|
sourceType = item.SourceType,
|
||||||
|
isManualOverride = item.IsManualOverride,
|
||||||
|
sortCode = item.SortCode
|
||||||
|
};
|
||||||
|
},
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
snapshots.Add(new ObEmployeeContractSnapshot
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
EmployeeId = employeeId,
|
||||||
|
TaskId = taskId,
|
||||||
|
TaskContractId = contract.Id,
|
||||||
|
TemplateId = contract.TemplateId,
|
||||||
|
TemplateVersionId = contract.TemplateVersionId,
|
||||||
|
ContractName = contract.ContractName,
|
||||||
|
ContractType = contract.ContractType,
|
||||||
|
ContractStartDate = ParseDate(GetValue(valueMap, "hr.ContractStartDate")),
|
||||||
|
ContractEndDate = ParseDate(GetValue(valueMap, "hr.ContractEndDate")),
|
||||||
|
ContractTerm = GetValue(valueMap, "hr.ContractTerm"),
|
||||||
|
ProbationStartDate = ParseDate(GetValue(valueMap, "hr.ProbationStartDate")),
|
||||||
|
ProbationEndDate = ParseDate(GetValue(valueMap, "hr.ProbationEndDate")),
|
||||||
|
ProbationTerm = GetValue(valueMap, "hr.ProbationTerm"),
|
||||||
|
ProbationSalary = ParseAmount(GetValue(valueMap, "hr.ProbationSalary")),
|
||||||
|
BaseSalary = ParseAmount(GetValue(valueMap, "hr.BaseSalary")),
|
||||||
|
PositionSalary = ParseAmount(GetValue(valueMap, "hr.PositionSalary")),
|
||||||
|
PerformanceAllowance = ParseAmount(GetValue(valueMap, "hr.PerformanceAllowance")),
|
||||||
|
AttendanceBonus = ParseAmount(GetValue(valueMap, "hr.AttendanceBonus")),
|
||||||
|
RegularSalary = ParseAmount(GetValue(valueMap, "hr.RegularSalary")),
|
||||||
|
WorkCity = GetValue(valueMap, "hr.WorkCity"),
|
||||||
|
PositionName = GetValue(valueMap, "hr.PositionName"),
|
||||||
|
EmployeeNo = GetValue(valueMap, "hr.EmployeeNo"),
|
||||||
|
VariablesJson = global::System.Text.Json.JsonSerializer.Serialize(variableSnapshot),
|
||||||
|
SnapshotVersion = 1,
|
||||||
|
SnapshotAt = now,
|
||||||
|
Status = CommonStatusConst.ENABLE,
|
||||||
|
CreateTime = now,
|
||||||
|
CreateUserId = UserManager.UserId,
|
||||||
|
CreateUser = UserManager.Name,
|
||||||
|
IsDelete = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build 只在审核建档事务内执行;先删后建使异常重试保持幂等。
|
||||||
|
await _db.Deleteable<ObEmployeeContractSnapshot>()
|
||||||
|
.Where(item => item.TaskId == taskId)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await _db.Insertable(snapshots).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetValue(
|
||||||
|
IReadOnlyDictionary<string, string> valueMap,
|
||||||
|
string variableCode)
|
||||||
|
{
|
||||||
|
return valueMap.TryGetValue(variableCode, out var value) && !string.IsNullOrWhiteSpace(value)
|
||||||
|
? value.Trim()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? ParseDate(string value)
|
||||||
|
{
|
||||||
|
return DateTime.TryParse(value, out var date) ? date.Date : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal? ParseAmount(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var normalized = value
|
||||||
|
.Replace("¥", string.Empty)
|
||||||
|
.Replace("¥", string.Empty)
|
||||||
|
.Replace("元", string.Empty)
|
||||||
|
.Trim();
|
||||||
|
if (decimal.TryParse(
|
||||||
|
normalized,
|
||||||
|
NumberStyles.Number | NumberStyles.AllowCurrencySymbol,
|
||||||
|
CultureInfo.GetCultureInfo("zh-CN"),
|
||||||
|
out var amount)
|
||||||
|
|| decimal.TryParse(
|
||||||
|
normalized,
|
||||||
|
NumberStyles.Number,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out amount))
|
||||||
|
{
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-32
@@ -1,68 +1,93 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存动态入职字段参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingFormFieldSaveInput
|
public class OnboardingFormFieldSaveInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据 Id。
|
||||||
|
/// </summary>
|
||||||
public long? Id { get; set; }
|
public long? Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段编码。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "字段编码不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "字段编码不能为空")]
|
||||||
public string FieldCode { get; set; }
|
public string FieldCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段名称。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "字段名称不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "字段名称不能为空")]
|
||||||
public string FieldName { get; set; }
|
public string FieldName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字段或变量数据类型。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "数据类型不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "数据类型不能为空")]
|
||||||
public string DataType { get; set; }
|
public string DataType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可选项 JSON。
|
||||||
|
/// </summary>
|
||||||
public string OptionsJson { get; set; }
|
public string OptionsJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字段校验规则。
|
||||||
|
/// </summary>
|
||||||
public string ValidationRule { get; set; }
|
public string ValidationRule { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工端输入提示。
|
||||||
|
/// </summary>
|
||||||
public string Placeholder { get; set; }
|
public string Placeholder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态或状态筛选条件。
|
||||||
|
/// </summary>
|
||||||
public string Status { get; set; } = "enable";
|
public string Status { get; set; } = "enable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存入职材料类型参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingMaterialTypeSaveInput
|
public class OnboardingMaterialTypeSaveInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据 Id。
|
||||||
|
/// </summary>
|
||||||
public long? Id { get; set; }
|
public long? Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料类型编码。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "资料编码不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "资料编码不能为空")]
|
||||||
public string MaterialCode { get; set; }
|
public string MaterialCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料名称。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "资料名称不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "资料名称不能为空")]
|
||||||
public string MaterialName { get; set; }
|
public string MaterialName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 允许上传的文件扩展名。
|
||||||
|
/// </summary>
|
||||||
public string AllowedExtensions { get; set; }
|
public string AllowedExtensions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 允许上传的最大文件数量。
|
||||||
|
/// </summary>
|
||||||
public int MaxFileCount { get; set; } = 1;
|
public int MaxFileCount { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个文件最大大小,单位为 MB。
|
||||||
|
/// </summary>
|
||||||
public int? MaxFileSizeMb { get; set; }
|
public int? MaxFileSizeMb { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态或状态筛选条件。
|
||||||
|
/// </summary>
|
||||||
public string Status { get; set; } = "enable";
|
public string Status { get; set; } = "enable";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OnboardingFormRequirementSaveInput
|
|
||||||
{
|
|
||||||
public long? Id { get; set; }
|
|
||||||
|
|
||||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
|
||||||
public long CompanyId { get; set; }
|
|
||||||
|
|
||||||
public long? PositionId { get; set; }
|
|
||||||
|
|
||||||
[IdNotNull(ErrorMessage = "字段Id不能为空")]
|
|
||||||
public long FieldId { get; set; }
|
|
||||||
|
|
||||||
public bool Required { get; set; }
|
|
||||||
public int? SortCode { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OnboardingMaterialRequirementSaveInput
|
|
||||||
{
|
|
||||||
public long? Id { get; set; }
|
|
||||||
|
|
||||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
|
||||||
public long CompanyId { get; set; }
|
|
||||||
|
|
||||||
public long? PositionId { get; set; }
|
|
||||||
|
|
||||||
[IdNotNull(ErrorMessage = "资料类型Id不能为空")]
|
|
||||||
public long MaterialTypeId { get; set; }
|
|
||||||
|
|
||||||
public bool Required { get; set; }
|
|
||||||
public int? SortCode { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
+26
-6
@@ -1,17 +1,37 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义员工动态补充字段与上传材料类型的基础配置能力。
|
||||||
|
/// </summary>
|
||||||
public interface IOnboardingEntryConfigService : ITransient
|
public interface IOnboardingEntryConfigService : ITransient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 查询全部动态入职字段。
|
||||||
|
/// </summary>
|
||||||
Task<List<ObFormField>> ListFormFields();
|
Task<List<ObFormField>> ListFormFields();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增或编辑动态入职字段。
|
||||||
|
/// </summary>
|
||||||
Task SaveFormField(OnboardingFormFieldSaveInput input);
|
Task SaveFormField(OnboardingFormFieldSaveInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被入职模板引用的动态字段。
|
||||||
|
/// </summary>
|
||||||
Task DeleteFormFields(BaseIdListInput input);
|
Task DeleteFormFields(BaseIdListInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询全部入职材料类型。
|
||||||
|
/// </summary>
|
||||||
Task<List<ObMaterialType>> ListMaterialTypes();
|
Task<List<ObMaterialType>> ListMaterialTypes();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增或编辑入职材料类型。
|
||||||
|
/// </summary>
|
||||||
Task SaveMaterialType(OnboardingMaterialTypeSaveInput input);
|
Task SaveMaterialType(OnboardingMaterialTypeSaveInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被入职模板或上传记录引用的材料类型。
|
||||||
|
/// </summary>
|
||||||
Task DeleteMaterialTypes(BaseIdListInput input);
|
Task DeleteMaterialTypes(BaseIdListInput input);
|
||||||
Task<List<ObFormRequirement>> ListFormRequirements(long companyId);
|
|
||||||
Task SaveFormRequirement(OnboardingFormRequirementSaveInput input);
|
|
||||||
Task DeleteFormRequirements(BaseIdListInput input);
|
|
||||||
Task<List<ObMaterialRequirement>> ListMaterialRequirements(long companyId);
|
|
||||||
Task SaveMaterialRequirement(OnboardingMaterialRequirementSaveInput input);
|
|
||||||
Task DeleteMaterialRequirements(BaseIdListInput input);
|
|
||||||
}
|
}
|
||||||
+129
-141
@@ -1,17 +1,22 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 维护员工端动态字段、材料类型及公司岗位要求。
|
/// 维护员工端动态补充字段和材料类型;与公司、岗位的组合关系由入职模板统一管理。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarClient _db;
|
private readonly SqlSugarScope _db;
|
||||||
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
public OnboardingEntryConfigService(ISqlSugarClient db)
|
public OnboardingEntryConfigService(IOnboardingDataPermissionService dataPermission)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = DbContext.DB;
|
||||||
|
_dataPermission = dataPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询全部动态入职字段。
|
||||||
|
/// </summary>
|
||||||
public Task<List<ObFormField>> ListFormFields()
|
public Task<List<ObFormField>> ListFormFields()
|
||||||
{
|
{
|
||||||
return _db.Queryable<ObFormField>()
|
return _db.Queryable<ObFormField>()
|
||||||
@@ -19,10 +24,22 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增或编辑动态入职字段。
|
||||||
|
/// </summary>
|
||||||
public async Task SaveFormField(OnboardingFormFieldSaveInput input)
|
public async Task SaveFormField(OnboardingFormFieldSaveInput input)
|
||||||
{
|
{
|
||||||
var fieldCodeExists = await _db.Queryable<ObFormField>()
|
input.FieldCode = OnboardingCodeValidator.Normalize(input.FieldCode, "字段编码", 100);
|
||||||
.AnyAsync(x => x.FieldCode == input.FieldCode && x.Id != input.Id);
|
input.FieldName = input.FieldName?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(input.FieldName))
|
||||||
|
throw Oops.Bah("字段名称不能为空");
|
||||||
|
|
||||||
|
var fieldCodeQuery = _db.Queryable<ObFormField>()
|
||||||
|
.Where(x => x.FieldCode == input.FieldCode);
|
||||||
|
if (input.Id.HasValue)
|
||||||
|
fieldCodeQuery = fieldCodeQuery.Where(x => x.Id != input.Id.Value);
|
||||||
|
|
||||||
|
var fieldCodeExists = await fieldCodeQuery.AnyAsync();
|
||||||
if (fieldCodeExists)
|
if (fieldCodeExists)
|
||||||
throw Oops.Bah("字段编码已存在");
|
throw Oops.Bah("字段编码已存在");
|
||||||
|
|
||||||
@@ -32,7 +49,7 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
|||||||
FieldCode = input.FieldCode,
|
FieldCode = input.FieldCode,
|
||||||
FieldName = input.FieldName,
|
FieldName = input.FieldName,
|
||||||
DataType = input.DataType,
|
DataType = input.DataType,
|
||||||
OptionsJson = input.OptionsJson,
|
OptionsJson = NormalizeOptionsJson(input.OptionsJson),
|
||||||
ValidationRule = input.ValidationRule,
|
ValidationRule = input.ValidationRule,
|
||||||
Placeholder = input.Placeholder,
|
Placeholder = input.Placeholder,
|
||||||
Status = input.Status
|
Status = input.Status
|
||||||
@@ -40,10 +57,15 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
|||||||
|
|
||||||
if (input.Id.HasValue)
|
if (input.Id.HasValue)
|
||||||
{
|
{
|
||||||
var exists = await _db.Queryable<ObFormField>().AnyAsync(x => x.Id == input.Id.Value);
|
var exists = await _db.Queryable<ObFormField>().InSingleAsync(input.Id.Value)
|
||||||
if (!exists)
|
?? throw Oops.Bah("字段不存在");
|
||||||
throw Oops.Bah("字段不存在");
|
await _dataPermission.EnsureCanManage(
|
||||||
|
exists.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.EntryConfigManageAll,
|
||||||
|
"入职字段");
|
||||||
|
entity.CreateUserId = exists.CreateUserId;
|
||||||
|
entity.CreateUser = exists.CreateUser;
|
||||||
|
entity.CreateTime = exists.CreateTime;
|
||||||
await _db.Updateable(entity).ExecuteCommandAsync();
|
await _db.Updateable(entity).ExecuteCommandAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -51,19 +73,57 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
|||||||
await _db.Insertable(entity).ExecuteCommandAsync();
|
await _db.Insertable(entity).ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将空白的可选项转换为 null,避免 MySQL JSON 字段写入空字符串时报错。
|
||||||
|
/// </summary>
|
||||||
|
private static string NormalizeOptionsJson(string optionsJson)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(optionsJson))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var normalizedJson = optionsJson.Trim();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var jsonDocument = global::System.Text.Json.JsonDocument.Parse(normalizedJson);
|
||||||
|
return normalizedJson;
|
||||||
|
}
|
||||||
|
catch (global::System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("可选项不是有效的 JSON 格式");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被填写要求引用的动态字段。
|
||||||
|
/// </summary>
|
||||||
public async Task DeleteFormFields(BaseIdListInput input)
|
public async Task DeleteFormFields(BaseIdListInput input)
|
||||||
{
|
{
|
||||||
if (input.Ids.Count == 0)
|
if (input.Ids.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var isReferenced = await _db.Queryable<ObFormRequirement>()
|
var fields = await _db.Queryable<ObFormField>()
|
||||||
|
.Where(item => input.Ids.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (fields.Count != input.Ids.Distinct().Count())
|
||||||
|
throw Oops.Bah("字段不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
fields.Select(item => item.CreateUserId),
|
||||||
|
OnboardingPermissionCodes.EntryConfigManageAll,
|
||||||
|
"入职字段");
|
||||||
|
|
||||||
|
|
||||||
|
var isReferencedByTask = await _db.Queryable<ObTaskFormRequirement>()
|
||||||
.AnyAsync(x => input.Ids.Contains(x.FieldId));
|
.AnyAsync(x => input.Ids.Contains(x.FieldId));
|
||||||
if (isReferenced)
|
if (isReferencedByTask)
|
||||||
throw Oops.Bah("字段已被填写要求引用,不能删除");
|
throw Oops.Bah("字段已被入职任务引用,不能删除,可改为停用");
|
||||||
|
|
||||||
await _db.Deleteable<ObFormField>().In(input.Ids).ExecuteCommandAsync();
|
await _db.Deleteable<ObFormField>().In(input.Ids).ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询全部入职材料类型。
|
||||||
|
/// </summary>
|
||||||
public Task<List<ObMaterialType>> ListMaterialTypes()
|
public Task<List<ObMaterialType>> ListMaterialTypes()
|
||||||
{
|
{
|
||||||
return _db.Queryable<ObMaterialType>()
|
return _db.Queryable<ObMaterialType>()
|
||||||
@@ -71,10 +131,35 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增或编辑入职材料类型。
|
||||||
|
/// </summary>
|
||||||
public async Task SaveMaterialType(OnboardingMaterialTypeSaveInput input)
|
public async Task SaveMaterialType(OnboardingMaterialTypeSaveInput input)
|
||||||
{
|
{
|
||||||
|
input.MaterialCode = OnboardingCodeValidator.Normalize(input.MaterialCode, "资料编码", 100);
|
||||||
|
input.MaterialName = input.MaterialName?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(input.MaterialName))
|
||||||
|
throw Oops.Bah("资料名称不能为空");
|
||||||
if (input.MaxFileCount <= 0)
|
if (input.MaxFileCount <= 0)
|
||||||
throw Oops.Bah("最大文件数量必须大于0");
|
throw Oops.Bah("最大文件数量必须大于0");
|
||||||
|
if (input.MaxFileSizeMb.HasValue && (input.MaxFileSizeMb.Value <= 0 || input.MaxFileSizeMb.Value > 20))
|
||||||
|
throw Oops.Bah("单个文件大小必须设置在1至20MB之间");
|
||||||
|
|
||||||
|
var supportedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"png", "jpg", "jpeg", "gif", "bmp", "webp", "pdf"
|
||||||
|
};
|
||||||
|
var allowedExtensions = (input.AllowedExtensions ?? string.Empty)
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(item => item.TrimStart('.').ToLowerInvariant())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
if (allowedExtensions.Count == 0)
|
||||||
|
allowedExtensions.AddRange(new[] { "png", "jpg", "jpeg", "pdf" });
|
||||||
|
var unsupportedExtensions = allowedExtensions.Where(item => !supportedExtensions.Contains(item)).ToList();
|
||||||
|
if (unsupportedExtensions.Count > 0)
|
||||||
|
throw Oops.Bah($"不支持的材料扩展名:{string.Join("、", unsupportedExtensions)},仅允许图片或PDF文件");
|
||||||
|
input.AllowedExtensions = string.Join(",", allowedExtensions);
|
||||||
|
|
||||||
var materialCodeExists = await _db.Queryable<ObMaterialType>()
|
var materialCodeExists = await _db.Queryable<ObMaterialType>()
|
||||||
.AnyAsync(x => x.MaterialCode == input.MaterialCode && x.Id != input.Id);
|
.AnyAsync(x => x.MaterialCode == input.MaterialCode && x.Id != input.Id);
|
||||||
@@ -94,10 +179,15 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
|||||||
|
|
||||||
if (input.Id.HasValue)
|
if (input.Id.HasValue)
|
||||||
{
|
{
|
||||||
var exists = await _db.Queryable<ObMaterialType>().AnyAsync(x => x.Id == input.Id.Value);
|
var exists = await _db.Queryable<ObMaterialType>().InSingleAsync(input.Id.Value)
|
||||||
if (!exists)
|
?? throw Oops.Bah("资料类型不存在");
|
||||||
throw Oops.Bah("资料类型不存在");
|
await _dataPermission.EnsureCanManage(
|
||||||
|
exists.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.EntryConfigManageAll,
|
||||||
|
"资料类型");
|
||||||
|
entity.CreateUserId = exists.CreateUserId;
|
||||||
|
entity.CreateUser = exists.CreateUser;
|
||||||
|
entity.CreateTime = exists.CreateTime;
|
||||||
await _db.Updateable(entity).ExecuteCommandAsync();
|
await _db.Updateable(entity).ExecuteCommandAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -105,15 +195,33 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
|||||||
await _db.Insertable(entity).ExecuteCommandAsync();
|
await _db.Insertable(entity).ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被要求或上传记录引用的材料类型。
|
||||||
|
/// </summary>
|
||||||
public async Task DeleteMaterialTypes(BaseIdListInput input)
|
public async Task DeleteMaterialTypes(BaseIdListInput input)
|
||||||
{
|
{
|
||||||
if (input.Ids.Count == 0)
|
if (input.Ids.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var isReferenced = await _db.Queryable<ObMaterialRequirement>()
|
var materialTypes = await _db.Queryable<ObMaterialType>()
|
||||||
|
.Where(item => input.Ids.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (materialTypes.Count != input.Ids.Distinct().Count())
|
||||||
|
throw Oops.Bah("资料类型不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
materialTypes.Select(item => item.CreateUserId),
|
||||||
|
OnboardingPermissionCodes.EntryConfigManageAll,
|
||||||
|
"资料类型");
|
||||||
|
|
||||||
|
var isReferencedByTemplate = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||||
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||||
if (isReferenced)
|
if (isReferencedByTemplate)
|
||||||
throw Oops.Bah("资料类型已被资料要求引用,不能删除");
|
throw Oops.Bah("资料类型已被入职模板引用,不能删除");
|
||||||
|
|
||||||
|
var isReferencedByTask = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||||
|
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||||
|
if (isReferencedByTask)
|
||||||
|
throw Oops.Bah("资料类型已被入职任务引用,不能删除,可改为停用");
|
||||||
|
|
||||||
var hasUploads = await _db.Queryable<ObTaskMaterial>()
|
var hasUploads = await _db.Queryable<ObTaskMaterial>()
|
||||||
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||||
@@ -122,124 +230,4 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
|||||||
|
|
||||||
await _db.Deleteable<ObMaterialType>().In(input.Ids).ExecuteCommandAsync();
|
await _db.Deleteable<ObMaterialType>().In(input.Ids).ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<ObFormRequirement>> ListFormRequirements(long companyId)
|
|
||||||
{
|
|
||||||
return _db.Queryable<ObFormRequirement>()
|
|
||||||
.Where(x => x.CompanyId == companyId)
|
|
||||||
.OrderBy(x => x.SortCode)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveFormRequirement(OnboardingFormRequirementSaveInput input)
|
|
||||||
{
|
|
||||||
await EnsureCompanyAndPosition(input.CompanyId, input.PositionId);
|
|
||||||
|
|
||||||
var fieldExists = await _db.Queryable<ObFormField>()
|
|
||||||
.AnyAsync(x => x.Id == input.FieldId && x.Status == "enable");
|
|
||||||
if (!fieldExists)
|
|
||||||
throw Oops.Bah("动态字段不存在或未启用");
|
|
||||||
|
|
||||||
var requirementExists = await _db.Queryable<ObFormRequirement>()
|
|
||||||
.AnyAsync(x => x.CompanyId == input.CompanyId
|
|
||||||
&& x.PositionId == input.PositionId
|
|
||||||
&& x.FieldId == input.FieldId
|
|
||||||
&& x.Id != input.Id);
|
|
||||||
if (requirementExists)
|
|
||||||
throw Oops.Bah("该公司、岗位和字段的要求已存在");
|
|
||||||
|
|
||||||
var entity = new ObFormRequirement
|
|
||||||
{
|
|
||||||
Id = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
|
||||||
CompanyId = input.CompanyId,
|
|
||||||
PositionId = input.PositionId,
|
|
||||||
FieldId = input.FieldId,
|
|
||||||
Required = input.Required,
|
|
||||||
SortCode = input.SortCode
|
|
||||||
};
|
|
||||||
|
|
||||||
if (input.Id.HasValue)
|
|
||||||
{
|
|
||||||
await _db.Updateable(entity).ExecuteCommandAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.Insertable(entity).ExecuteCommandAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task DeleteFormRequirements(BaseIdListInput input)
|
|
||||||
{
|
|
||||||
if (input.Ids.Count == 0)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
return _db.Deleteable<ObFormRequirement>().In(input.Ids).ExecuteCommandAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<List<ObMaterialRequirement>> ListMaterialRequirements(long companyId)
|
|
||||||
{
|
|
||||||
return _db.Queryable<ObMaterialRequirement>()
|
|
||||||
.Where(x => x.CompanyId == companyId)
|
|
||||||
.OrderBy(x => x.SortCode)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveMaterialRequirement(OnboardingMaterialRequirementSaveInput input)
|
|
||||||
{
|
|
||||||
await EnsureCompanyAndPosition(input.CompanyId, input.PositionId);
|
|
||||||
|
|
||||||
var materialTypeExists = await _db.Queryable<ObMaterialType>()
|
|
||||||
.AnyAsync(x => x.Id == input.MaterialTypeId && x.Status == "enable");
|
|
||||||
if (!materialTypeExists)
|
|
||||||
throw Oops.Bah("资料类型不存在或未启用");
|
|
||||||
|
|
||||||
var requirementExists = await _db.Queryable<ObMaterialRequirement>()
|
|
||||||
.AnyAsync(x => x.CompanyId == input.CompanyId
|
|
||||||
&& x.PositionId == input.PositionId
|
|
||||||
&& x.MaterialTypeId == input.MaterialTypeId
|
|
||||||
&& x.Id != input.Id);
|
|
||||||
if (requirementExists)
|
|
||||||
throw Oops.Bah("该公司、岗位和资料类型的要求已存在");
|
|
||||||
|
|
||||||
var entity = new ObMaterialRequirement
|
|
||||||
{
|
|
||||||
Id = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
|
||||||
CompanyId = input.CompanyId,
|
|
||||||
PositionId = input.PositionId,
|
|
||||||
MaterialTypeId = input.MaterialTypeId,
|
|
||||||
Required = input.Required,
|
|
||||||
SortCode = input.SortCode
|
|
||||||
};
|
|
||||||
|
|
||||||
if (input.Id.HasValue)
|
|
||||||
{
|
|
||||||
await _db.Updateable(entity).ExecuteCommandAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.Insertable(entity).ExecuteCommandAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task DeleteMaterialRequirements(BaseIdListInput input)
|
|
||||||
{
|
|
||||||
if (input.Ids.Count == 0)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
return _db.Deleteable<ObMaterialRequirement>().In(input.Ids).ExecuteCommandAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureCompanyAndPosition(long companyId, long? positionId)
|
|
||||||
{
|
|
||||||
var companyExists = await _db.Queryable<ObCompany>()
|
|
||||||
.AnyAsync(x => x.Id == companyId && x.Status == "enable");
|
|
||||||
if (!companyExists)
|
|
||||||
throw Oops.Bah("公司不存在或未启用");
|
|
||||||
|
|
||||||
if (!positionId.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var positionExists = await _db.Queryable<ObPosition>()
|
|
||||||
.AnyAsync(x => x.Id == positionId.Value && x.CompanyId == companyId && x.Status == "enable");
|
|
||||||
if (!positionExists)
|
|
||||||
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义候选人身份重复校验能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingIdentityValidationService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 校验姓名和身份证号没有出现在其他有效入职流程或正式员工档案中。
|
||||||
|
/// </summary>
|
||||||
|
Task EnsureNoDuplicate(
|
||||||
|
long currentTaskId,
|
||||||
|
string employeeName,
|
||||||
|
string idCardNumber);
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一校验候选人身份,避免同一人员通过多个链接重复提交入职资料。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingIdentityValidationService : IOnboardingIdentityValidationService
|
||||||
|
{
|
||||||
|
private readonly SqlSugarScope _db;
|
||||||
|
|
||||||
|
public OnboardingIdentityValidationService()
|
||||||
|
{
|
||||||
|
_db = DbContext.DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task EnsureNoDuplicate(
|
||||||
|
long currentTaskId,
|
||||||
|
string employeeName,
|
||||||
|
string idCardNumber)
|
||||||
|
{
|
||||||
|
employeeName = employeeName?.Trim();
|
||||||
|
idCardNumber = idCardNumber?.Trim().ToUpperInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(employeeName) || string.IsNullOrWhiteSpace(idCardNumber))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var formalEmployeeExists = await _db.Queryable<ObEmployee>()
|
||||||
|
.AnyAsync(item => item.EmployeeName == employeeName
|
||||||
|
&& item.IdCardNumber == idCardNumber);
|
||||||
|
if (formalEmployeeExists)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("该姓名和身份证号已完成正式员工建档,请勿重复提交入职资料");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 身份证号码代表唯一自然人。即使姓名填写不同,也不能绕过正式员工重复校验。
|
||||||
|
var formalEmployeeIdCardExists = await _db.Queryable<ObEmployee>()
|
||||||
|
.AnyAsync(item => item.IdCardNumber == idCardNumber);
|
||||||
|
if (formalEmployeeIdCardExists)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("该身份证号码已完成正式员工建档,请核对姓名和身份证号码;如需更正,请联系 HR");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待填写任务不占用身份,以实际先提交的任务为准;撤销或过期任务允许重新发起。
|
||||||
|
var duplicateTaskExists = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||||
|
.InnerJoin<ObOnboardingTask>((info, task) => info.TaskId == task.Id)
|
||||||
|
.Where((info, task) => info.TaskId != currentTaskId
|
||||||
|
&& info.EmployeeName == employeeName
|
||||||
|
&& info.IdCardNumber == idCardNumber
|
||||||
|
&& (task.TaskStatus == OnboardingTaskStatus.Reviewing
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Rejected
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Approved
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Archived))
|
||||||
|
.AnyAsync();
|
||||||
|
if (duplicateTaskExists)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("该姓名和身份证号已存在正在办理的入职任务,请勿重复提交;如需补充材料,请联系 HR 继续使用原任务链接");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再单独按身份证号码校验,防止不同姓名使用同一证件重复提交不同任务。
|
||||||
|
var duplicateIdCardTaskExists = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||||
|
.InnerJoin<ObOnboardingTask>((info, task) => info.TaskId == task.Id)
|
||||||
|
.Where((info, task) => info.TaskId != currentTaskId
|
||||||
|
&& info.IdCardNumber == idCardNumber
|
||||||
|
&& (task.TaskStatus == OnboardingTaskStatus.Reviewing
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Rejected
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Approved
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Archived))
|
||||||
|
.AnyAsync();
|
||||||
|
if (duplicateIdCardTaskExists)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("该身份证号码已存在正在办理的入职任务,请核对姓名和身份证号码;如需补充材料,请联系 HR 继续使用原任务链接");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-1
@@ -1,2 +1,22 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
public interface IOnboardingTaskLifecycleService : ITransient { Task Archive(OnboardingAuditActionInput input); Task Revoke(OnboardingAuditActionInput input); }
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义入职任务撤销和归档能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingTaskLifecycleService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 归档已经审核通过的入职任务。
|
||||||
|
/// </summary>
|
||||||
|
Task Archive(OnboardingAuditActionInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销尚未归档的入职任务并使员工链接失效。
|
||||||
|
/// </summary>
|
||||||
|
Task Revoke(OnboardingAuditActionInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除已撤销的入职任务及其全部关联快照数据。
|
||||||
|
/// </summary>
|
||||||
|
Task Delete(BaseIdInput input);
|
||||||
|
}
|
||||||
|
|||||||
+158
-41
@@ -1,17 +1,28 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现入职任务生命周期业务能力。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
|
public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarClient _db;
|
private readonly SqlSugarScope _db;
|
||||||
private readonly IConfigService _config;
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
public OnboardingTaskLifecycleService(ISqlSugarClient db, IConfigService config)
|
public OnboardingTaskLifecycleService(IOnboardingDataPermissionService dataPermission)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = DbContext.DB;
|
||||||
_config = config;
|
_dataPermission = dataPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 归档已审核通过的入职任务。
|
||||||
|
/// </summary>
|
||||||
public async Task Archive(OnboardingAuditActionInput input)
|
public async Task Archive(OnboardingAuditActionInput input)
|
||||||
|
{
|
||||||
|
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => ArchiveCore(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ArchiveCore(OnboardingAuditActionInput input)
|
||||||
{
|
{
|
||||||
var task = await Authorize(input.TaskId);
|
var task = await Authorize(input.TaskId);
|
||||||
if (task.TaskStatus != OnboardingTaskStatus.Approved)
|
if (task.TaskStatus != OnboardingTaskStatus.Approved)
|
||||||
@@ -19,57 +30,163 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
|
|||||||
throw Oops.Bah("仅审核通过任务可归档");
|
throw Oops.Bah("仅审核通过任务可归档");
|
||||||
}
|
}
|
||||||
|
|
||||||
task.TaskStatus = OnboardingTaskStatus.Archived;
|
await _db.Ado.BeginTranAsync();
|
||||||
task.ArchivedAt = DateTime.Now;
|
try
|
||||||
await _db.Updateable(task).ExecuteCommandAsync();
|
{
|
||||||
await Log(task, OnboardingAuditAction.Archive, input.AuditOpinion);
|
var archivedAt = DateTime.Now;
|
||||||
|
var updatedRows = await _db.Updateable<ObOnboardingTask>()
|
||||||
|
.SetColumns(item => new ObOnboardingTask
|
||||||
|
{
|
||||||
|
TaskStatus = OnboardingTaskStatus.Archived,
|
||||||
|
ArchivedAt = archivedAt
|
||||||
|
})
|
||||||
|
.Where(item => item.Id == task.Id
|
||||||
|
&& item.TaskStatus == OnboardingTaskStatus.Approved)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
if (updatedRows != 1)
|
||||||
|
throw Oops.Bah("该入职任务状态已变化,请刷新后重试");
|
||||||
|
|
||||||
|
task.TaskStatus = OnboardingTaskStatus.Archived;
|
||||||
|
task.ArchivedAt = archivedAt;
|
||||||
|
await Log(task, OnboardingAuditAction.Archive, input.AuditOpinion);
|
||||||
|
await _db.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _db.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销入职任务并同步撤销仍然有效的员工填写链接。
|
||||||
|
/// </summary>
|
||||||
public async Task Revoke(OnboardingAuditActionInput input)
|
public async Task Revoke(OnboardingAuditActionInput input)
|
||||||
|
{
|
||||||
|
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => RevokeCore(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RevokeCore(OnboardingAuditActionInput input)
|
||||||
{
|
{
|
||||||
var task = await Authorize(input.TaskId);
|
var task = await Authorize(input.TaskId);
|
||||||
if (task.TaskStatus == OnboardingTaskStatus.Approved || task.TaskStatus == OnboardingTaskStatus.Archived)
|
if (task.TaskStatus == OnboardingTaskStatus.Approved
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Archived
|
||||||
|
|| task.TaskStatus == OnboardingTaskStatus.Revoked)
|
||||||
{
|
{
|
||||||
throw Oops.Bah("已通过或已归档任务不能撤销");
|
throw Oops.Bah("已通过、已归档或已撤销任务不能再次撤销");
|
||||||
}
|
}
|
||||||
|
|
||||||
task.TaskStatus = OnboardingTaskStatus.Revoked;
|
await _db.Ado.BeginTranAsync();
|
||||||
await _db.Updateable(task).ExecuteCommandAsync();
|
try
|
||||||
await _db.Updateable<ObTaskLink>()
|
{
|
||||||
.SetColumns(item => new ObTaskLink
|
var originalStatus = task.TaskStatus;
|
||||||
|
var revokedAt = DateTime.Now;
|
||||||
|
var updatedRows = await _db.Updateable<ObOnboardingTask>()
|
||||||
|
.SetColumns(item => item.TaskStatus == OnboardingTaskStatus.Revoked)
|
||||||
|
.Where(item => item.Id == task.Id && item.TaskStatus == originalStatus)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
if (updatedRows != 1)
|
||||||
|
throw Oops.Bah("该入职任务状态已变化,请刷新后重试");
|
||||||
|
|
||||||
|
await _db.Updateable<ObTaskLink>()
|
||||||
|
.SetColumns(item => new ObTaskLink
|
||||||
|
{
|
||||||
|
LinkStatus = OnboardingLinkStatus.Revoked,
|
||||||
|
RevokedAt = revokedAt
|
||||||
|
})
|
||||||
|
.Where(item => item.TaskId == task.Id
|
||||||
|
&& item.LinkStatus == OnboardingLinkStatus.Active)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
task.TaskStatus = OnboardingTaskStatus.Revoked;
|
||||||
|
await Log(task, OnboardingAuditAction.Revoke, input.AuditOpinion);
|
||||||
|
await _db.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _db.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除已撤销的入职任务及其全部关联快照数据。
|
||||||
|
/// </summary>
|
||||||
|
public async Task Delete(BaseIdInput input)
|
||||||
|
{
|
||||||
|
await OnboardingConcurrency.RunTask(_db, input.Id, () => DeleteCore(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteCore(BaseIdInput input)
|
||||||
|
{
|
||||||
|
var task = await Authorize(input.Id);
|
||||||
|
if (task.TaskStatus != OnboardingTaskStatus.Revoked)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("仅已撤销的任务可以删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var taskContractIds = await _db.Queryable<ObTaskContract>()
|
||||||
|
.Where(x => x.TaskId == task.Id)
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var signProcessIds = await _db.Queryable<ObElectronicSignProcess>()
|
||||||
|
.Where(x => x.TaskId == task.Id)
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (signProcessIds.Count > 0)
|
||||||
{
|
{
|
||||||
LinkStatus = OnboardingLinkStatus.Revoked,
|
await _db.Deleteable<ObElectronicSignEvent>()
|
||||||
RevokedAt = DateTime.Now
|
.Where(x => x.SignProcessId.HasValue
|
||||||
})
|
&& signProcessIds.Contains(x.SignProcessId.Value))
|
||||||
.Where(item => item.TaskId == task.Id && item.LinkStatus == OnboardingLinkStatus.Active)
|
.ExecuteCommandAsync();
|
||||||
.ExecuteCommandAsync();
|
await _db.Deleteable<ObElectronicSignDocument>()
|
||||||
await Log(task, OnboardingAuditAction.Revoke, input.AuditOpinion);
|
.Where(x => signProcessIds.Contains(x.SignProcessId))
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObElectronicSignProcess>()
|
||||||
|
.Where(x => signProcessIds.Contains(x.Id))
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.Deleteable<ObEmployeeContractSnapshot>()
|
||||||
|
.Where(x => x.TaskId == task.Id)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
if (taskContractIds.Count > 0)
|
||||||
|
{
|
||||||
|
await _db.Deleteable<ObTaskContractVariable>()
|
||||||
|
.Where(x => taskContractIds.Contains(x.TaskContractId))
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.Deleteable<ObTaskContract>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObTaskLink>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObTaskMaterialRequirement>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObTaskMaterial>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObTaskEmployeeInfo>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObTaskFormRequirement>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObTaskOperationLog>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObTaskAudit>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObOnboardingTask>().In(task.Id).ExecuteCommandAsync();
|
||||||
|
await _db.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _db.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ObOnboardingTask> Authorize(long taskId)
|
private async Task<ObOnboardingTask> Authorize(long taskId)
|
||||||
{
|
{
|
||||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId)
|
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId)
|
||||||
?? throw Oops.Bah("入职任务不存在");
|
?? throw Oops.Bah("入职任务不存在");
|
||||||
var config = await _config.GetByConfigKey(CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.reviewer.userIds")
|
await _dataPermission.EnsureCanManage(
|
||||||
?? throw Oops.Bah("请配置入职审核人");
|
task.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.TaskManageAll,
|
||||||
using var configJson = global::System.Text.Json.JsonDocument.Parse(config.ConfigValue);
|
"入职任务");
|
||||||
var reviewerIds = new List<long>();
|
|
||||||
if (configJson.RootElement.TryGetProperty("all", out var allReviewers))
|
|
||||||
{
|
|
||||||
reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configJson.RootElement.TryGetProperty("company", out var companyReviewers)
|
|
||||||
&& companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers))
|
|
||||||
{
|
|
||||||
reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reviewerIds.Contains(UserManager.UserId))
|
|
||||||
{
|
|
||||||
throw Oops.Bah("您不是该入职任务的审核人");
|
|
||||||
}
|
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -1,10 +1,23 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核候选人上传材料参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingMaterialReviewInput
|
public class OnboardingMaterialReviewInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 任务材料记录 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "任务资料Id不能为空")]
|
[IdNotNull(ErrorMessage = "任务资料Id不能为空")]
|
||||||
public long TaskMaterialId { get; set; }
|
public long TaskMaterialId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料审核状态。
|
||||||
|
/// </summary>
|
||||||
public string ReviewStatus { get; set; }
|
public string ReviewStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 材料审核意见。
|
||||||
|
/// </summary>
|
||||||
public string ReviewRemark { get; set; }
|
public string ReviewRemark { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -1,2 +1,12 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
public interface IOnboardingMaterialReviewService : ITransient { Task Review(OnboardingMaterialReviewInput input); }
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义候选人上传材料的审核能力。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingMaterialReviewService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 审核单份候选人上传材料。
|
||||||
|
/// </summary>
|
||||||
|
Task Review(OnboardingMaterialReviewInput input);
|
||||||
|
}
|
||||||
|
|||||||
+22
-25
@@ -1,17 +1,29 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现候选人上传材料审核业务能力。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingMaterialReviewService : IOnboardingMaterialReviewService
|
public class OnboardingMaterialReviewService : IOnboardingMaterialReviewService
|
||||||
{
|
{
|
||||||
private readonly ISqlSugarClient _db;
|
private readonly SqlSugarScope _db;
|
||||||
private readonly IConfigService _config;
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
public OnboardingMaterialReviewService(ISqlSugarClient db, IConfigService config)
|
public OnboardingMaterialReviewService(
|
||||||
|
IOnboardingDataPermissionService dataPermission)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = DbContext.DB;
|
||||||
_config = config;
|
_dataPermission = dataPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核单份候选人上传材料并记录审核意见。
|
||||||
|
/// </summary>
|
||||||
public async Task Review(OnboardingMaterialReviewInput input)
|
public async Task Review(OnboardingMaterialReviewInput input)
|
||||||
|
{
|
||||||
|
await OnboardingConcurrency.RunMaterial(_db, input.TaskMaterialId, () => ReviewCore(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReviewCore(OnboardingMaterialReviewInput input)
|
||||||
{
|
{
|
||||||
if (input.ReviewStatus != TaskMaterialReviewStatus.Passed
|
if (input.ReviewStatus != TaskMaterialReviewStatus.Passed
|
||||||
&& input.ReviewStatus != TaskMaterialReviewStatus.NeedSupplement)
|
&& input.ReviewStatus != TaskMaterialReviewStatus.NeedSupplement)
|
||||||
@@ -40,26 +52,11 @@ public class OnboardingMaterialReviewService : IOnboardingMaterialReviewService
|
|||||||
await _db.Updateable(material).ExecuteCommandAsync();
|
await _db.Updateable(material).ExecuteCommandAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureReviewer(ObOnboardingTask task)
|
private Task EnsureReviewer(ObOnboardingTask task)
|
||||||
{
|
{
|
||||||
var config = await _config.GetByConfigKey(CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.reviewer.userIds")
|
return _dataPermission.EnsureCanManage(
|
||||||
?? throw Oops.Bah("请配置入职审核人");
|
task.CreateUserId,
|
||||||
using var configJson = global::System.Text.Json.JsonDocument.Parse(config.ConfigValue);
|
OnboardingPermissionCodes.TaskManageAll,
|
||||||
var reviewerIds = new List<long>();
|
"入职任务");
|
||||||
if (configJson.RootElement.TryGetProperty("all", out var allReviewers))
|
|
||||||
{
|
|
||||||
reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configJson.RootElement.TryGetProperty("company", out var companyReviewers)
|
|
||||||
&& companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers))
|
|
||||||
{
|
|
||||||
reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reviewerIds.Contains(UserManager.UserId))
|
|
||||||
{
|
|
||||||
throw Oops.Bah("您不是该入职任务的审核人");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板分页查询参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTemplatePageInput : BasePageInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模板编码或名称关键词。
|
||||||
|
/// </summary>
|
||||||
|
public string Keyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? PositionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存入职模板参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTemplateSaveInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑时传入的入职模板 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板业务编码。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板名称。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "所属部门不能为空")]
|
||||||
|
public long DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位 Id 列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<long> PositionIds { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同配置列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<OnboardingTemplateContractInput> Contracts { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态补充字段列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<OnboardingTemplateFormFieldInput> FormFields { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上传材料列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<OnboardingTemplateMaterialInput> Materials { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板状态。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "enable";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板说明。
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板合同配置参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTemplateContractInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long ContractTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同模板版本 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long ContractTemplateVersionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否必须查看确认。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打印份数。
|
||||||
|
/// </summary>
|
||||||
|
public int PrintCopies { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序码。
|
||||||
|
/// </summary>
|
||||||
|
public int? SortCode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板补充字段参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTemplateFormFieldInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 动态字段 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long FieldId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否必填。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序码。
|
||||||
|
/// </summary>
|
||||||
|
public int? SortCode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板上传材料参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTemplateMaterialInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 材料类型 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long MaterialTypeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否必传。
|
||||||
|
/// </summary>
|
||||||
|
public bool Required { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序码。
|
||||||
|
/// </summary>
|
||||||
|
public int? SortCode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板列表输出。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTemplateOutput : ObOnboardingTemplate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司名称。
|
||||||
|
/// </summary>
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门名称。
|
||||||
|
/// </summary>
|
||||||
|
public string DepartmentName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位 Id 列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<long> PositionIds { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用岗位名称列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> PositionNames { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合同配置数量。
|
||||||
|
/// </summary>
|
||||||
|
public int ContractCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态补充字段数量。
|
||||||
|
/// </summary>
|
||||||
|
public int FormFieldCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上传材料数量。
|
||||||
|
/// </summary>
|
||||||
|
public int MaterialCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板详情输出。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTemplateDetailOutput : OnboardingTemplateOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 合同配置明细。
|
||||||
|
/// </summary>
|
||||||
|
public List<ObOnboardingTemplateContract> Contracts { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 动态补充字段配置明细。
|
||||||
|
/// </summary>
|
||||||
|
public List<ObOnboardingTemplateFormField> FormFields { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上传材料配置明细。
|
||||||
|
/// </summary>
|
||||||
|
public List<ObOnboardingTemplateMaterial> Materials { get; set; } = new();
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职模板聚合配置服务。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingTemplateService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询入职模板。
|
||||||
|
/// </summary>
|
||||||
|
Task<SqlSugarPagedList<OnboardingTemplateOutput>> Page(OnboardingTemplatePageInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询入职模板详情。
|
||||||
|
/// </summary>
|
||||||
|
Task<OnboardingTemplateDetailOutput> Detail(BaseIdInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增或编辑入职模板及全部关联配置。
|
||||||
|
/// </summary>
|
||||||
|
Task<long> Save(OnboardingTemplateSaveInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除未被任务使用的入职模板。
|
||||||
|
/// </summary>
|
||||||
|
Task Delete(BaseIdListInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询公司、部门和岗位可使用的入职模板。
|
||||||
|
/// </summary>
|
||||||
|
Task<List<OnboardingTemplateOutput>> ListEnabled(
|
||||||
|
long companyId,
|
||||||
|
long departmentId,
|
||||||
|
long positionId);
|
||||||
|
}
|
||||||
+541
@@ -0,0 +1,541 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现入职模板聚合配置及公司岗位匹配能力。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTemplateService : IOnboardingTemplateService
|
||||||
|
{
|
||||||
|
private readonly SqlSugarScope _db;
|
||||||
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||||
|
|
||||||
|
public OnboardingTemplateService(IOnboardingDataPermissionService dataPermission)
|
||||||
|
{
|
||||||
|
_db = DbContext.DB;
|
||||||
|
_dataPermission = dataPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<SqlSugarPagedList<OnboardingTemplateOutput>> Page(OnboardingTemplatePageInput input)
|
||||||
|
{
|
||||||
|
var query = _db.Queryable<ObOnboardingTemplate>()
|
||||||
|
.Where(item => !item.IsDelete)
|
||||||
|
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword),
|
||||||
|
item => item.TemplateCode.Contains(input.Keyword) || item.TemplateName.Contains(input.Keyword))
|
||||||
|
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
|
||||||
|
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
|
||||||
|
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), item => item.Status == input.Status);
|
||||||
|
|
||||||
|
if (input.PositionId.HasValue)
|
||||||
|
{
|
||||||
|
var templateIds = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||||
|
.Where(item => item.PositionId == input.PositionId.Value)
|
||||||
|
.Select(item => item.OnboardingTemplateId)
|
||||||
|
.ToListAsync();
|
||||||
|
query = query.Where(item => templateIds.Contains(item.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var page = await query
|
||||||
|
.OrderByDescending(item => item.CreateTime)
|
||||||
|
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||||
|
var rows = await BuildOutputs(page.List.ToList());
|
||||||
|
|
||||||
|
return new SqlSugarPagedList<OnboardingTemplateOutput>
|
||||||
|
{
|
||||||
|
PageNum = page.PageNum,
|
||||||
|
PageSize = page.PageSize,
|
||||||
|
Total = page.Total,
|
||||||
|
Pages = page.Pages,
|
||||||
|
HasPrevPages = page.HasPrevPages,
|
||||||
|
HasNextPages = page.HasNextPages,
|
||||||
|
List = rows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<OnboardingTemplateDetailOutput> Detail(BaseIdInput input)
|
||||||
|
{
|
||||||
|
var template = await _db.Queryable<ObOnboardingTemplate>().InSingleAsync(input.Id)
|
||||||
|
?? throw Oops.Bah("入职模板不存在");
|
||||||
|
var summary = (await BuildOutputs(new List<ObOnboardingTemplate> { template })).Single();
|
||||||
|
|
||||||
|
return new OnboardingTemplateDetailOutput
|
||||||
|
{
|
||||||
|
Id = summary.Id,
|
||||||
|
CreateUserId = summary.CreateUserId,
|
||||||
|
CreateUser = summary.CreateUser,
|
||||||
|
TemplateCode = summary.TemplateCode,
|
||||||
|
TemplateName = summary.TemplateName,
|
||||||
|
CompanyId = summary.CompanyId,
|
||||||
|
CompanyName = summary.CompanyName,
|
||||||
|
DepartmentId = summary.DepartmentId,
|
||||||
|
DepartmentName = summary.DepartmentName,
|
||||||
|
Description = summary.Description,
|
||||||
|
Status = summary.Status,
|
||||||
|
CreateTime = summary.CreateTime,
|
||||||
|
UpdateTime = summary.UpdateTime,
|
||||||
|
PositionIds = summary.PositionIds,
|
||||||
|
PositionNames = summary.PositionNames,
|
||||||
|
ContractCount = summary.ContractCount,
|
||||||
|
FormFieldCount = summary.FormFieldCount,
|
||||||
|
MaterialCount = summary.MaterialCount,
|
||||||
|
Contracts = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||||
|
.Where(item => item.OnboardingTemplateId == template.Id)
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.ToListAsync(),
|
||||||
|
FormFields = new List<ObOnboardingTemplateFormField>(),
|
||||||
|
Materials = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||||
|
.Where(item => item.OnboardingTemplateId == template.Id)
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.ToListAsync()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<long> Save(OnboardingTemplateSaveInput input)
|
||||||
|
{
|
||||||
|
NormalizeAndValidateInput(input);
|
||||||
|
await ValidateReferences(input);
|
||||||
|
|
||||||
|
var codeOwnerQuery = _db.Queryable<ObOnboardingTemplate>()
|
||||||
|
.ClearFilter()
|
||||||
|
.Where(item => item.TemplateCode == input.TemplateCode);
|
||||||
|
if (input.Id.HasValue)
|
||||||
|
{
|
||||||
|
var currentTemplateId = input.Id.Value;
|
||||||
|
codeOwnerQuery = codeOwnerQuery.Where(item => item.Id != currentTemplateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var codeOwner = await codeOwnerQuery.FirstAsync();
|
||||||
|
if (codeOwner != null && !codeOwner.IsDelete)
|
||||||
|
throw Oops.Bah("入职模板编码已存在");
|
||||||
|
|
||||||
|
if (input.Id.HasValue && codeOwner != null && codeOwner.IsDelete)
|
||||||
|
throw Oops.Bah("该入职模板编码属于一条已删除记录,请通过新增模板重新启用该编码");
|
||||||
|
|
||||||
|
// 新增时如果编码命中历史软删除记录,复用原 Id 并恢复,避免唯一索引直接抛 500。
|
||||||
|
var isRestore = !input.Id.HasValue && codeOwner != null && codeOwner.IsDelete;
|
||||||
|
var templateId = isRestore
|
||||||
|
? codeOwner.Id
|
||||||
|
: input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||||
|
|
||||||
|
if (input.Id.HasValue || isRestore)
|
||||||
|
{
|
||||||
|
var existing = isRestore
|
||||||
|
? codeOwner
|
||||||
|
: await _db.Queryable<ObOnboardingTemplate>().InSingleAsync(templateId);
|
||||||
|
if (existing == null)
|
||||||
|
throw Oops.Bah("入职模板不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
existing.CreateUserId,
|
||||||
|
OnboardingPermissionCodes.EntryConfigManageAll,
|
||||||
|
"入职模板");
|
||||||
|
}
|
||||||
|
|
||||||
|
var entity = new ObOnboardingTemplate
|
||||||
|
{
|
||||||
|
Id = templateId,
|
||||||
|
TemplateCode = input.TemplateCode.Trim(),
|
||||||
|
TemplateName = input.TemplateName.Trim(),
|
||||||
|
CompanyId = input.CompanyId,
|
||||||
|
DepartmentId = input.DepartmentId,
|
||||||
|
Description = input.Description,
|
||||||
|
Status = input.Status,
|
||||||
|
IsDelete = false
|
||||||
|
};
|
||||||
|
|
||||||
|
await _db.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (input.Id.HasValue || isRestore)
|
||||||
|
{
|
||||||
|
var exists = isRestore || await _db.Queryable<ObOnboardingTemplate>()
|
||||||
|
.AnyAsync(item => item.Id == templateId);
|
||||||
|
if (!exists)
|
||||||
|
throw Oops.Bah("入职模板不存在");
|
||||||
|
await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
|
||||||
|
await DeleteRelations(templateId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _db.Insertable(entity).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await InsertRelations(templateId, input);
|
||||||
|
await _db.Ado.CommitTranAsync();
|
||||||
|
return templateId;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _db.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Delete(BaseIdListInput input)
|
||||||
|
{
|
||||||
|
if (input.Ids.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var templates = await _db.Queryable<ObOnboardingTemplate>()
|
||||||
|
.Where(item => input.Ids.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (templates.Count != input.Ids.Distinct().Count())
|
||||||
|
throw Oops.Bah("入职模板不存在");
|
||||||
|
await _dataPermission.EnsureCanManage(
|
||||||
|
templates.Select(item => item.CreateUserId),
|
||||||
|
OnboardingPermissionCodes.EntryConfigManageAll,
|
||||||
|
"入职模板");
|
||||||
|
|
||||||
|
var isUsed = await _db.Queryable<ObOnboardingTask>()
|
||||||
|
.AnyAsync(item => input.Ids.Contains(item.OnboardingTemplateId));
|
||||||
|
if (isUsed)
|
||||||
|
throw Oops.Bah("入职模板已生成任务,不能删除,可以改为停用");
|
||||||
|
|
||||||
|
await _db.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var id in input.Ids)
|
||||||
|
await DeleteRelations(id);
|
||||||
|
|
||||||
|
await _db.Updateable<ObOnboardingTemplate>()
|
||||||
|
.SetColumns(it => it.IsDelete == true)
|
||||||
|
.Where(it => input.Ids.Contains(it.Id))
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
await _db.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _db.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<OnboardingTemplateOutput>> ListEnabled(
|
||||||
|
long companyId,
|
||||||
|
long departmentId,
|
||||||
|
long positionId)
|
||||||
|
{
|
||||||
|
var templateIds = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||||
|
.Where(item => item.PositionId == positionId)
|
||||||
|
.Select(item => item.OnboardingTemplateId)
|
||||||
|
.ToListAsync();
|
||||||
|
var templates = await _db.Queryable<ObOnboardingTemplate>()
|
||||||
|
.Where(item => item.CompanyId == companyId
|
||||||
|
&& item.DepartmentId == departmentId
|
||||||
|
&& item.Status == "enable"
|
||||||
|
&& templateIds.Contains(item.Id))
|
||||||
|
.OrderBy(item => item.TemplateName)
|
||||||
|
.ToListAsync();
|
||||||
|
if (templates.Count == 0)
|
||||||
|
return await BuildOutputs(templates);
|
||||||
|
|
||||||
|
var onboardingTemplateIds = templates.Select(item => item.Id).ToList();
|
||||||
|
var contractConfigs = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||||
|
.Where(item => onboardingTemplateIds.Contains(item.OnboardingTemplateId))
|
||||||
|
.ToListAsync();
|
||||||
|
if (contractConfigs.Count == 0)
|
||||||
|
return await BuildOutputs(new List<ObOnboardingTemplate>());
|
||||||
|
|
||||||
|
var contractTemplateIds = contractConfigs
|
||||||
|
.Select(item => item.ContractTemplateId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var contractVersionIds = contractConfigs
|
||||||
|
.Select(item => item.ContractTemplateVersionId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var contractTemplates = await _db.Queryable<ObContractTemplate>()
|
||||||
|
.Where(item => contractTemplateIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var contractVersions = await _db.Queryable<ObContractTemplateVersion>()
|
||||||
|
.Where(item => contractVersionIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var contractPositions = await _db.Queryable<ObContractTemplatePosition>()
|
||||||
|
.Where(item => contractTemplateIds.Contains(item.ContractTemplateId)
|
||||||
|
&& item.PositionId == positionId)
|
||||||
|
.ToListAsync();
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var availableTemplates = templates.Where(template =>
|
||||||
|
{
|
||||||
|
var configs = contractConfigs
|
||||||
|
.Where(item => item.OnboardingTemplateId == template.Id)
|
||||||
|
.ToList();
|
||||||
|
if (configs.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return configs.All(config =>
|
||||||
|
{
|
||||||
|
var contractTemplate = contractTemplates
|
||||||
|
.FirstOrDefault(item => item.Id == config.ContractTemplateId);
|
||||||
|
var version = contractVersions
|
||||||
|
.FirstOrDefault(item => item.Id == config.ContractTemplateVersionId);
|
||||||
|
return contractTemplate != null
|
||||||
|
&& string.Equals(
|
||||||
|
contractTemplate.Status,
|
||||||
|
"enable",
|
||||||
|
StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& contractTemplate.CompanyId == template.CompanyId
|
||||||
|
&& contractTemplate.DepartmentId == template.DepartmentId
|
||||||
|
&& contractPositions.Any(item =>
|
||||||
|
item.ContractTemplateId == config.ContractTemplateId)
|
||||||
|
&& version != null
|
||||||
|
&& version.TemplateId == config.ContractTemplateId
|
||||||
|
&& ContractTemplateVersionValidity.IsAvailable(version, now);
|
||||||
|
});
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return await BuildOutputs(availableTemplates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NormalizeAndValidateInput(OnboardingTemplateSaveInput input)
|
||||||
|
{
|
||||||
|
input.TemplateCode = OnboardingCodeValidator.Normalize(
|
||||||
|
input.TemplateCode,
|
||||||
|
"入职模板编码",
|
||||||
|
100);
|
||||||
|
input.TemplateName = input.TemplateName?.Trim();
|
||||||
|
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
|
||||||
|
input.Contracts ??= new List<OnboardingTemplateContractInput>();
|
||||||
|
var duplicateContract = input.Contracts
|
||||||
|
.GroupBy(item => item.ContractTemplateId)
|
||||||
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
|
if (duplicateContract != null)
|
||||||
|
throw Oops.Bah("同一份合同模板在入职模板中只能选择一个版本");
|
||||||
|
|
||||||
|
input.FormFields = input.FormFields?
|
||||||
|
.GroupBy(item => item.FieldId)
|
||||||
|
.Select(group => group.First())
|
||||||
|
.ToList() ?? new List<OnboardingTemplateFormFieldInput>();
|
||||||
|
input.Materials = input.Materials?
|
||||||
|
.GroupBy(item => item.MaterialTypeId)
|
||||||
|
.Select(group => group.First())
|
||||||
|
.ToList() ?? new List<OnboardingTemplateMaterialInput>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(input.TemplateName))
|
||||||
|
throw Oops.Bah("请填写入职模板名称");
|
||||||
|
if (input.CompanyId <= 0)
|
||||||
|
throw Oops.Bah("请选择所属公司");
|
||||||
|
if (input.DepartmentId <= 0)
|
||||||
|
throw Oops.Bah("请选择所属部门");
|
||||||
|
if (input.PositionIds.Count == 0)
|
||||||
|
throw Oops.Bah("请至少选择一个适用岗位");
|
||||||
|
if (input.PositionIds.Any(positionId => positionId <= 0))
|
||||||
|
throw Oops.Bah("适用岗位数据不正确");
|
||||||
|
if (input.Contracts.Count == 0)
|
||||||
|
throw Oops.Bah("请至少选择一份合同文件");
|
||||||
|
if (input.Contracts.Any(item => item.ContractTemplateId <= 0
|
||||||
|
|| item.ContractTemplateVersionId <= 0))
|
||||||
|
{
|
||||||
|
throw Oops.Bah("合同模板或版本数据不正确");
|
||||||
|
}
|
||||||
|
if (input.Status != "enable" && input.Status != "disable")
|
||||||
|
throw Oops.Bah("入职模板状态不正确");
|
||||||
|
if (input.Contracts.Any(item => item.PrintCopies <= 0))
|
||||||
|
throw Oops.Bah("合同打印份数必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateReferences(OnboardingTemplateSaveInput input)
|
||||||
|
{
|
||||||
|
var companyEnabled = await _db.Queryable<ObCompany>()
|
||||||
|
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
|
||||||
|
if (!companyEnabled)
|
||||||
|
throw Oops.Bah("所属公司不存在或未启用");
|
||||||
|
|
||||||
|
var departmentEnabled = await _db.Queryable<ObDepartment>()
|
||||||
|
.AnyAsync(item => item.Id == input.DepartmentId
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.Status == "enable");
|
||||||
|
if (!departmentEnabled)
|
||||||
|
throw Oops.Bah("所属部门不存在、未启用或不属于该公司");
|
||||||
|
|
||||||
|
var positionCount = await _db.Queryable<ObPosition>()
|
||||||
|
.CountAsync(item => input.PositionIds.Contains(item.Id)
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.DepartmentId == input.DepartmentId
|
||||||
|
&& item.Status == "enable");
|
||||||
|
if (positionCount != input.PositionIds.Count)
|
||||||
|
throw Oops.Bah("存在未启用或不属于该公司、部门的岗位");
|
||||||
|
|
||||||
|
var versionIds = input.Contracts.Select(item => item.ContractTemplateVersionId).ToList();
|
||||||
|
var versions = await _db.Queryable<ObContractTemplateVersion>()
|
||||||
|
.Where(item => versionIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (versions.Count != versionIds.Count
|
||||||
|
|| input.Contracts.Any(contract => versions.All(version => version.Id != contract.ContractTemplateVersionId
|
||||||
|
|| version.TemplateId != contract.ContractTemplateId)))
|
||||||
|
{
|
||||||
|
throw Oops.Bah("合同模板与版本不匹配");
|
||||||
|
}
|
||||||
|
var now = DateTime.Now;
|
||||||
|
if (versions.Any(version => !ContractTemplateVersionValidity.IsAvailable(version, now)))
|
||||||
|
throw Oops.Bah("入职模板只能选择当前已生效且尚未失效的启用签署材料版本");
|
||||||
|
|
||||||
|
var contractTemplateIds = input.Contracts
|
||||||
|
.Select(item => item.ContractTemplateId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var contractTemplates = await _db.Queryable<ObContractTemplate>()
|
||||||
|
.Where(item => contractTemplateIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
if (contractTemplates.Count != contractTemplateIds.Count
|
||||||
|
|| contractTemplates.Any(item => item.CompanyId != input.CompanyId
|
||||||
|
|| item.DepartmentId != input.DepartmentId))
|
||||||
|
{
|
||||||
|
throw Oops.Bah("存在不属于当前公司、部门的合同模板");
|
||||||
|
}
|
||||||
|
|
||||||
|
var contractPositionRelations = await _db.Queryable<ObContractTemplatePosition>()
|
||||||
|
.Where(item => contractTemplateIds.Contains(item.ContractTemplateId))
|
||||||
|
.ToListAsync();
|
||||||
|
var unsupportedContract = contractTemplateIds.FirstOrDefault(contractTemplateId =>
|
||||||
|
{
|
||||||
|
var supportedPositionIds = contractPositionRelations
|
||||||
|
.Where(item => item.ContractTemplateId == contractTemplateId)
|
||||||
|
.Select(item => item.PositionId)
|
||||||
|
.ToHashSet();
|
||||||
|
return input.PositionIds.Any(positionId => !supportedPositionIds.Contains(positionId));
|
||||||
|
});
|
||||||
|
if (unsupportedContract > 0)
|
||||||
|
{
|
||||||
|
var contractName = contractTemplates
|
||||||
|
.First(item => item.Id == unsupportedContract)
|
||||||
|
.TemplateName;
|
||||||
|
throw Oops.Bah($"合同模板“{contractName}”不能覆盖当前选择的全部岗位");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Status == "enable")
|
||||||
|
{
|
||||||
|
if (contractTemplates.Any(item => !string.Equals(
|
||||||
|
item.Status,
|
||||||
|
"enable",
|
||||||
|
global::System.StringComparison.OrdinalIgnoreCase)))
|
||||||
|
throw Oops.Bah("启用入职模板前,请先启用全部合同模板");
|
||||||
|
}
|
||||||
|
|
||||||
|
var materialIds = input.Materials.Select(item => item.MaterialTypeId).ToList();
|
||||||
|
if (materialIds.Count > 0)
|
||||||
|
{
|
||||||
|
var materialCount = await _db.Queryable<ObMaterialType>()
|
||||||
|
.CountAsync(item => materialIds.Contains(item.Id) && item.Status == "enable");
|
||||||
|
if (materialCount != materialIds.Count)
|
||||||
|
throw Oops.Bah("存在未启用或不存在的上传材料类型");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InsertRelations(long templateId, OnboardingTemplateSaveInput input)
|
||||||
|
{
|
||||||
|
var positions = input.PositionIds.Select(positionId => new ObOnboardingTemplatePosition
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
OnboardingTemplateId = templateId,
|
||||||
|
PositionId = positionId
|
||||||
|
}).ToList();
|
||||||
|
await _db.Insertable(positions).ExecuteCommandAsync();
|
||||||
|
|
||||||
|
var contracts = input.Contracts.Select(item => new ObOnboardingTemplateContract
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
OnboardingTemplateId = templateId,
|
||||||
|
ContractTemplateId = item.ContractTemplateId,
|
||||||
|
ContractTemplateVersionId = item.ContractTemplateVersionId,
|
||||||
|
Required = item.Required,
|
||||||
|
PrintCopies = item.PrintCopies,
|
||||||
|
SortCode = item.SortCode
|
||||||
|
}).ToList();
|
||||||
|
await _db.Insertable(contracts).ExecuteCommandAsync();
|
||||||
|
|
||||||
|
if (input.Materials != null && input.Materials.Count > 0)
|
||||||
|
{
|
||||||
|
var materials = input.Materials.Select(item => new ObOnboardingTemplateMaterial
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
OnboardingTemplateId = templateId,
|
||||||
|
MaterialTypeId = item.MaterialTypeId,
|
||||||
|
Required = item.Required,
|
||||||
|
SortCode = item.SortCode
|
||||||
|
}).ToList();
|
||||||
|
await _db.Insertable(materials).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteRelations(long templateId)
|
||||||
|
{
|
||||||
|
await _db.Deleteable<ObOnboardingTemplatePosition>()
|
||||||
|
.Where(item => item.OnboardingTemplateId == templateId)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObOnboardingTemplateContract>()
|
||||||
|
.Where(item => item.OnboardingTemplateId == templateId)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
await _db.Deleteable<ObOnboardingTemplateMaterial>()
|
||||||
|
.Where(item => item.OnboardingTemplateId == templateId)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<OnboardingTemplateOutput>> BuildOutputs(List<ObOnboardingTemplate> templates)
|
||||||
|
{
|
||||||
|
if (templates.Count == 0)
|
||||||
|
return new List<OnboardingTemplateOutput>();
|
||||||
|
|
||||||
|
var templateIds = templates.Select(item => item.Id).ToList();
|
||||||
|
var companyIds = templates.Select(item => item.CompanyId).Distinct().ToList();
|
||||||
|
var departmentIds = templates
|
||||||
|
.Where(item => item.DepartmentId.HasValue)
|
||||||
|
.Select(item => item.DepartmentId.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var companies = await _db.Queryable<ObCompany>()
|
||||||
|
.Where(item => companyIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var departments = departmentIds.Count == 0
|
||||||
|
? new List<ObDepartment>()
|
||||||
|
: await _db.Queryable<ObDepartment>()
|
||||||
|
.Where(item => departmentIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var relations = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||||
|
.Where(item => templateIds.Contains(item.OnboardingTemplateId))
|
||||||
|
.ToListAsync();
|
||||||
|
var positionIds = relations.Select(item => item.PositionId).Distinct().ToList();
|
||||||
|
var positions = await _db.Queryable<ObPosition>()
|
||||||
|
.Where(item => positionIds.Contains(item.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
var contracts = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||||
|
.Where(item => templateIds.Contains(item.OnboardingTemplateId))
|
||||||
|
.ToListAsync();
|
||||||
|
var materials = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||||
|
.Where(item => templateIds.Contains(item.OnboardingTemplateId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return templates.Select(template =>
|
||||||
|
{
|
||||||
|
var templatePositions = relations.Where(item => item.OnboardingTemplateId == template.Id).ToList();
|
||||||
|
return new OnboardingTemplateOutput
|
||||||
|
{
|
||||||
|
Id = template.Id,
|
||||||
|
CreateUserId = template.CreateUserId,
|
||||||
|
CreateUser = template.CreateUser,
|
||||||
|
TemplateCode = template.TemplateCode,
|
||||||
|
TemplateName = template.TemplateName,
|
||||||
|
CompanyId = template.CompanyId,
|
||||||
|
CompanyName = companies.FirstOrDefault(item => item.Id == template.CompanyId)?.CompanyName,
|
||||||
|
DepartmentId = template.DepartmentId,
|
||||||
|
DepartmentName = departments
|
||||||
|
.FirstOrDefault(item => item.Id == template.DepartmentId)?.DepartmentName,
|
||||||
|
Description = template.Description,
|
||||||
|
Status = template.Status,
|
||||||
|
CreateTime = template.CreateTime,
|
||||||
|
UpdateTime = template.UpdateTime,
|
||||||
|
PositionIds = templatePositions.Select(item => item.PositionId).ToList(),
|
||||||
|
PositionNames = templatePositions
|
||||||
|
.Select(relation => positions.FirstOrDefault(position => position.Id == relation.PositionId)?.PositionName)
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.ToList(),
|
||||||
|
ContractCount = contracts.Count(item => item.OnboardingTemplateId == template.Id),
|
||||||
|
FormFieldCount = 0,
|
||||||
|
MaterialCount = materials.Count(item => item.OnboardingTemplateId == template.Id)
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,82 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务岗位分页查询参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingPositionPageInput : BasePageInput
|
public class OnboardingPositionPageInput : BasePageInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
public long? CompanyId { get; set; }
|
public long? CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位编码。
|
||||||
|
/// </summary>
|
||||||
|
public virtual string PositionCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位名称。
|
||||||
|
/// </summary>
|
||||||
public virtual string PositionName { get; set; }
|
public virtual string PositionName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态。
|
||||||
|
/// </summary>
|
||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增入职业务岗位参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingPositionAddInput : ObPosition
|
public class OnboardingPositionAddInput : ObPosition
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属公司不能为空")]
|
||||||
|
public new long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属部门不能为空")]
|
||||||
|
public new long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位业务编码。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "岗位编码不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "岗位编码不能为空")]
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(50, ErrorMessage = "岗位编码不能超过50个字符")]
|
||||||
public override string PositionCode { get; set; }
|
public override string PositionCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 岗位名称。
|
||||||
|
/// </summary>
|
||||||
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "岗位名称不能为空")]
|
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "岗位名称不能为空")]
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(50, ErrorMessage = "岗位名称不能超过50个字符")]
|
||||||
public override string PositionName { get; set; }
|
public override string PositionName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务备注。
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.DataAnnotations.MaxLength(200, ErrorMessage = "备注不能超过200个字符")]
|
||||||
|
public override string Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑入职业务岗位参数。
|
||||||
|
/// </summary>
|
||||||
public class OnboardingPositionEditInput : OnboardingPositionAddInput
|
public class OnboardingPositionEditInput : OnboardingPositionAddInput
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据 Id。
|
||||||
|
/// </summary>
|
||||||
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
|
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
|
||||||
public override long Id { get; set; }
|
public override long Id { get; set; }
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user