Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ae5e0bc54 | ||
|
|
be20a31332 | ||
|
|
38801533e9 | ||
|
|
085bf5e2ab | ||
|
|
4679bd6c13 | ||
|
|
0931e1f343 |
@@ -37,6 +37,11 @@ public class ObOnboardingTask : BaseEntity
|
||||
/// </summary>
|
||||
public long PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时锁定的入职模板 Id。
|
||||
/// </summary>
|
||||
public long OnboardingTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时指定的审核人用户 Id。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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>
|
||||
/// 模板用途和适用范围说明。
|
||||
/// </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; }
|
||||
}
|
||||
@@ -47,6 +47,11 @@ public class ObTaskContract : BaseEntity
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务创建时冻结的合同打印份数。
|
||||
/// </summary>
|
||||
public int PrintCopies { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 任务合同当前状态。
|
||||
/// </summary>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 候选人在入职任务中提交的固定人员基础信息。
|
||||
/// 候选人在入职任务中提交的《入职登记表》标准员工档案信息。
|
||||
/// </summary>
|
||||
[SugarTable("ob_task_employee_info", TableDescription = "候选人提交的固定基础信息")]
|
||||
[SugarTable("ob_task_employee_info", TableDescription = "候选人填写的入职登记表")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObTaskEmployeeInfo : BaseEntity
|
||||
{
|
||||
@@ -23,45 +23,57 @@ public class ObTaskEmployeeInfo : BaseEntity
|
||||
public long TaskId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工编号。
|
||||
/// 员工编号(选填/人事分配)。
|
||||
/// </summary>
|
||||
public string EmployeeNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内部人员编号。
|
||||
/// </summary>
|
||||
public string InternalNo { get; set; }
|
||||
|
||||
// ==================== 1. 基本个人信息 ====================
|
||||
/// <summary>
|
||||
/// 员工姓名。
|
||||
/// </summary>
|
||||
public string EmployeeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属部门名称。
|
||||
/// </summary>
|
||||
public string DepartmentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 性别。
|
||||
/// </summary>
|
||||
public string Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机号码。
|
||||
/// 民族。
|
||||
/// </summary>
|
||||
public string Phone { get; set; }
|
||||
public string Nation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 身份证号码。
|
||||
/// 出生年月日。
|
||||
/// </summary>
|
||||
public string IdCardNumber { get; set; }
|
||||
public string Birthday { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 毕业院校。
|
||||
/// 户籍所在地。
|
||||
/// </summary>
|
||||
public string College { get; set; }
|
||||
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>
|
||||
@@ -73,82 +85,125 @@ public class ObTaskEmployeeInfo : BaseEntity
|
||||
public string Major { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出生日期。
|
||||
/// 毕业院校。
|
||||
/// </summary>
|
||||
public DateTime? BirthDate { get; set; }
|
||||
public string GraduatedSchool { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 户籍所在地。
|
||||
/// 职业资格。
|
||||
/// </summary>
|
||||
public string RegisteredResidence { get; set; }
|
||||
public string VocationalQualification { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 民族。
|
||||
/// 资格等级。
|
||||
/// </summary>
|
||||
public string Nation { get; set; }
|
||||
public string QualificationLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 现居住或联系地址。
|
||||
/// 有何技术特长。
|
||||
/// </summary>
|
||||
public string ContactAddress { get; set; }
|
||||
public string TechnicalSpecialty { get; set; }
|
||||
|
||||
// ==================== 3. 联系方式与薪酬账户 ====================
|
||||
/// <summary>
|
||||
/// 联系电话/手机号。
|
||||
/// </summary>
|
||||
public string Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入职日期。
|
||||
/// 电子邮箱。
|
||||
/// </summary>
|
||||
public DateTime? EntryDate { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同开始日期。
|
||||
/// 身份证号码。
|
||||
/// </summary>
|
||||
public DateTime? ContractStartDate { get; set; }
|
||||
public string IdCardNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同结束日期。
|
||||
/// 暂住地址 / 现居住地址。
|
||||
/// </summary>
|
||||
public DateTime? ContractEndDate { get; set; }
|
||||
public string CurrentAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 试用期开始日期。
|
||||
/// 房东联系方式。
|
||||
/// </summary>
|
||||
public DateTime? ProbationStartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 试用期结束日期。
|
||||
/// </summary>
|
||||
public DateTime? ProbationEndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 转正日期。
|
||||
/// </summary>
|
||||
public DateTime? RegularizationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 社保缴纳公司。
|
||||
/// </summary>
|
||||
public string SocialInsuranceCompany { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同性质。
|
||||
/// </summary>
|
||||
public string ContractNature { get; set; }
|
||||
public string LandlordContact { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工资银行卡号。
|
||||
/// </summary>
|
||||
public string BankCardNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 银行卡开户名。
|
||||
/// </summary>
|
||||
public string BankAccountName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开户银行名称。
|
||||
/// </summary>
|
||||
public string BankName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务备注。
|
||||
/// 银行卡开户人姓名。
|
||||
/// </summary>
|
||||
public string Remark { get; set; }
|
||||
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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -139,6 +139,10 @@ public static class TaskContractStatus
|
||||
/// </summary>
|
||||
public static class ContractVariableType
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入模板时尚未确认来源的变量。
|
||||
/// </summary>
|
||||
public const string Unresolved = "unresolved";
|
||||
/// <summary>
|
||||
/// 值来源于候选人或正式员工信息。
|
||||
/// </summary>
|
||||
|
||||
@@ -28,12 +28,12 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
.FirstAsync(item => item.TaskId == task.Id)
|
||||
?? throw Oops.Bah("候选人资料不存在");
|
||||
|
||||
var hasMaterialNeedSupplement = await _db.Queryable<ObTaskMaterial>()
|
||||
var hasUnapprovedMaterial = await _db.Queryable<ObTaskMaterial>()
|
||||
.AnyAsync(item => item.TaskId == task.Id
|
||||
&& item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
|
||||
if (hasMaterialNeedSupplement)
|
||||
&& item.ReviewStatus != TaskMaterialReviewStatus.Passed);
|
||||
if (hasUnapprovedMaterial)
|
||||
{
|
||||
throw Oops.Bah("仍有资料需要补充,不能审核通过");
|
||||
throw Oops.Bah("仍有资料未审核通过,不能审核通过入职任务");
|
||||
}
|
||||
|
||||
// 审核通过是创建正式员工的唯一入口,因此必须在入库前拦截身份证号或手机号重复。
|
||||
@@ -44,14 +44,25 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
throw Oops.Bah("该身份证号或手机号已存在正式员工");
|
||||
}
|
||||
|
||||
var extraValues = await _db.Queryable<ObTaskFormValue>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync();
|
||||
var employee = employeeInfo.Adapt<ObEmployee>();
|
||||
employee.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
employee.CompanyId = task.CompanyId;
|
||||
employee.PositionId = task.PositionId;
|
||||
employee.ExtraInfoJson = global::System.Text.Json.JsonSerializer.Serialize(extraValues);
|
||||
employee.DepartmentName = employeeInfo.OnboardingDepartment;
|
||||
employee.College = employeeInfo.GraduatedSchool;
|
||||
employee.BirthDate = ParseDate(employeeInfo.Birthday);
|
||||
employee.ContactAddress = employeeInfo.CurrentAddress;
|
||||
employee.EntryDate = ParseDate(employeeInfo.OnboardingDate);
|
||||
employee.Remark = employeeInfo.OtherRemarks;
|
||||
employee.ExtraInfoJson = global::System.Text.Json.JsonSerializer.Serialize(employeeInfo);
|
||||
employee.Status = CommonStatusConst.ENABLE;
|
||||
employee.CreateTime = DateTime.Now;
|
||||
employee.CreateUserId = UserManager.UserId;
|
||||
employee.CreateUser = UserManager.Name;
|
||||
employee.UpdateTime = null;
|
||||
employee.UpdateUserId = null;
|
||||
employee.UpdateUser = null;
|
||||
employee.IsDelete = false;
|
||||
|
||||
// 员工档案、任务状态和审核记录必须同时成功,避免出现“已通过但没有员工档案”的中间状态。
|
||||
await _db.Ado.BeginTranAsync();
|
||||
@@ -124,6 +135,16 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
return task;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return _db.Insertable(new ObTaskAudit
|
||||
|
||||
@@ -5,6 +5,11 @@ namespace SimpleAdmin.Application;
|
||||
/// </summary>
|
||||
public class OnboardingCompanyPageInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 公司业务编码。
|
||||
/// </summary>
|
||||
public virtual string CompanyCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公司名称。
|
||||
/// </summary>
|
||||
|
||||
@@ -11,9 +11,8 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
|
||||
public async Task<SqlSugarPagedList<ObCompany>> Page(OnboardingCompanyPageInput input)
|
||||
{
|
||||
return await Context.Queryable<ObCompany>()
|
||||
.WhereIF(
|
||||
!string.IsNullOrWhiteSpace(input.CompanyName),
|
||||
it => it.CompanyName.Contains(input.CompanyName) || it.CompanyCode.Contains(input.CompanyName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.CompanyCode), it => it.CompanyCode.Contains(input.CompanyCode))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.CompanyName), it => it.CompanyName.Contains(input.CompanyName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
||||
.OrderBy(it => it.CreateTime)
|
||||
|
||||
+32
-59
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职合同文件生成业务能力。
|
||||
/// 实现入职合同预览和归档 DOCX 文件生成能力。
|
||||
/// </summary>
|
||||
public class OnboardingContractDocumentService : IOnboardingContractDocumentService
|
||||
{
|
||||
@@ -15,7 +15,7 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据已锁定的模板版本和变量快照生成任务合同 DOCX 文件。
|
||||
/// 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
|
||||
/// </summary>
|
||||
public async Task<long> Generate(OnboardingGenerateContractInput input)
|
||||
{
|
||||
@@ -23,10 +23,12 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
||||
?? throw Oops.Bah("任务合同不存在");
|
||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(contract.TaskId)
|
||||
?? throw Oops.Bah("入职任务不存在");
|
||||
|
||||
if (task.TaskStatus != OnboardingTaskStatus.Approved)
|
||||
throw Oops.Bah("仅审核通过的任务可生成归档合同");
|
||||
|
||||
var statusAllowsGeneration = task.TaskStatus == OnboardingTaskStatus.PendingFill
|
||||
|| task.TaskStatus == OnboardingTaskStatus.Rejected
|
||||
|| task.TaskStatus == OnboardingTaskStatus.Reviewing
|
||||
|| task.TaskStatus == OnboardingTaskStatus.Approved;
|
||||
if (!statusAllowsGeneration)
|
||||
throw Oops.Bah("当前任务状态不能生成合同");
|
||||
if (contract.GeneratedDocxFileId.HasValue)
|
||||
return contract.GeneratedDocxFileId.Value;
|
||||
|
||||
@@ -34,72 +36,43 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
||||
?? throw Oops.Bah("合同版本不存在");
|
||||
var source = await _db.Queryable<SysFile>().InSingleAsync(version.SourceDocxFileId)
|
||||
?? throw Oops.Bah("合同源文件不存在");
|
||||
var variables = await _db.Queryable<ObTaskContractVariable>()
|
||||
.Where(x => x.TaskContractId == contract.Id)
|
||||
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||
.Where(item => item.TemplateVersionId == version.Id)
|
||||
.ToListAsync();
|
||||
var variables = await _db.Queryable<ObTaskContractVariable>()
|
||||
.Where(item => item.TaskContractId == contract.Id)
|
||||
.ToListAsync();
|
||||
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($"合同变量“{missing.VariableName}”尚未填写,暂时不能预览合同");
|
||||
|
||||
var sourceResult = await _fileService.Download(new BaseIdInput { Id = source.Id })
|
||||
?? throw Oops.Bah("无法读取合同源文件");
|
||||
|
||||
await using var sourceStream = sourceResult.FileStream;
|
||||
using var sourceBytes = new MemoryStream();
|
||||
await sourceStream.CopyToAsync(sourceBytes);
|
||||
|
||||
// 合同只读取任务变量快照,确保模板或员工档案后续修改不会影响已生成文件。
|
||||
var rendered = ReplaceDocxText(
|
||||
var rendered = DocxTemplateProcessor.Render(
|
||||
sourceBytes.ToArray(),
|
||||
variables.ToDictionary(x => x.VariableCode, x => x.VariableValue ?? string.Empty));
|
||||
variables.ToDictionary(
|
||||
item => item.VariableCode,
|
||||
item => item.VariableValue ?? string.Empty,
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
var name = $"{task.TaskNo}_{contract.ContractName}.docx";
|
||||
var fileId = await _fileService.StoreGeneratedFile(source.Engine, name, rendered);
|
||||
var fileId = await _fileService.StoreGeneratedFile(
|
||||
SysDictConst.FILE_ENGINE_ALIYUN_OSS,
|
||||
name,
|
||||
rendered);
|
||||
|
||||
contract.GeneratedDocxFileId = fileId;
|
||||
contract.GeneratedAt = DateTime.Now;
|
||||
contract.ContractStatus = task.TaskStatus == OnboardingTaskStatus.Approved
|
||||
? TaskContractStatus.OfflinePendingSign
|
||||
: TaskContractStatus.PendingConfirm;
|
||||
await _db.Updateable(contract).ExecuteCommandAsync();
|
||||
|
||||
return fileId;
|
||||
}
|
||||
|
||||
private static byte[] ReplaceDocxText(byte[] docx, Dictionary<string, string> values)
|
||||
{
|
||||
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);
|
||||
|
||||
// DOCX 本质是 ZIP 包,仅替换正文、页眉和页脚 XML 中的文本节点,其他资源原样复制。
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var next = target.CreateEntry(entry.FullName, global::System.IO.Compression.CompressionLevel.Optimal);
|
||||
using var reader = entry.Open();
|
||||
using var writer = next.Open();
|
||||
|
||||
var isWordTextXml = entry.FullName == "word/document.xml"
|
||||
|| ((entry.FullName.StartsWith("word/header") || entry.FullName.StartsWith("word/footer"))
|
||||
&& entry.FullName.EndsWith(".xml"));
|
||||
if (!isWordTextXml)
|
||||
{
|
||||
reader.CopyTo(writer);
|
||||
continue;
|
||||
}
|
||||
|
||||
using var textReader = new StreamReader(reader);
|
||||
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"))
|
||||
{
|
||||
foreach (var item in values)
|
||||
node.Value = node.Value.Replace("{{" + item.Key + "}}", item.Value ?? string.Empty);
|
||||
}
|
||||
|
||||
using var textWriter = new StreamWriter(writer);
|
||||
textWriter.Write(document.ToString(global::System.Xml.Linq.SaveOptions.DisableFormatting));
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -9,5 +9,5 @@ public interface IOnboardingContractSnapshotService : ITransient
|
||||
/// <summary>
|
||||
/// 根据任务、公司和候选人资料构建或刷新合同变量快照。
|
||||
/// </summary>
|
||||
Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null);
|
||||
Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null, bool invalidateGeneratedPreview = false);
|
||||
}
|
||||
|
||||
+161
-43
@@ -1,32 +1,49 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>冻结任务合同变量;DOCX 生成阶段只读取本快照,不再读取模板实时值。</summary>
|
||||
/// <summary>
|
||||
/// 实现任务合同变量快照业务能力。
|
||||
/// 构建任务合同变量快照,合同生成阶段只读取本快照。
|
||||
/// </summary>
|
||||
public class OnboardingContractSnapshotService : IOnboardingContractSnapshotService
|
||||
{
|
||||
private readonly SqlSugarScope _db;
|
||||
public OnboardingContractSnapshotService() => _db = DbContext.DB;
|
||||
|
||||
public OnboardingContractSnapshotService()
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据任务、公司和候选人资料构建或刷新合同变量快照。
|
||||
/// 根据任务、公司、员工固定信息和动态补充字段构建合同变量快照。
|
||||
/// </summary>
|
||||
public async Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null)
|
||||
public async Task Build(
|
||||
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(x => x.TaskId == taskId);
|
||||
var contracts = await _db.Queryable<ObTaskContract>().Where(x => x.TaskId == taskId).ToListAsync();
|
||||
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)
|
||||
{
|
||||
if (contract.GeneratedDocxFileId.HasValue || contract.GeneratedPdfFileId.HasValue) throw Oops.Bah("合同已生成,不能重建变量快照");
|
||||
var definitions = await _db.Queryable<ObTemplateVariable>().Where(x => x.TemplateVersionId == contract.TemplateVersionId).OrderBy(x => x.SortCode).ToListAsync();
|
||||
var oldRows = await _db.Queryable<ObTaskContractVariable>().Where(x => x.TaskContractId == contract.Id).ToListAsync();
|
||||
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||
.Where(item => item.TemplateVersionId == contract.TemplateVersionId)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
var oldRows = await _db.Queryable<ObTaskContractVariable>()
|
||||
.Where(item => item.TaskContractId == contract.Id)
|
||||
.ToListAsync();
|
||||
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;
|
||||
return new ObTaskContractVariable
|
||||
{
|
||||
@@ -36,54 +53,155 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
|
||||
VariableName = definition.VariableName,
|
||||
VariableType = definition.VariableType,
|
||||
DataType = definition.DataType,
|
||||
VariableValue = preserveManual ? old.VariableValue : ResolveValue(definition, employeeInfo, company, task),
|
||||
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
|
||||
};
|
||||
}).ToList();
|
||||
await _db.Deleteable<ObTaskContractVariable>().Where(x => x.TaskContractId == contract.Id).ExecuteCommandAsync();
|
||||
if (rows.Count > 0) await _db.Insertable(rows).ExecuteCommandAsync();
|
||||
|
||||
await _db.Deleteable<ObTaskContractVariable>()
|
||||
.Where(item => item.TaskContractId == contract.Id)
|
||||
.ExecuteCommandAsync();
|
||||
if (rows.Count > 0)
|
||||
await _db.Insertable(rows).ExecuteCommandAsync();
|
||||
|
||||
if (invalidateGeneratedPreview && 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,
|
||||
ContractVariableType.Company => ContractVariableSourceType.Company,
|
||||
ContractVariableType.System => ContractVariableSourceType.System,
|
||||
_ => ContractVariableSourceType.HrInput
|
||||
};
|
||||
return variableType switch
|
||||
{
|
||||
ContractVariableType.Employee => ContractVariableSourceType.Employee,
|
||||
ContractVariableType.Company => ContractVariableSourceType.Company,
|
||||
ContractVariableType.System => ContractVariableSourceType.System,
|
||||
_ => ContractVariableSourceType.HrInput
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveValue(ObTemplateVariable definition, ObTaskEmployeeInfo info, ObCompany company, ObOnboardingTask task)
|
||||
private static string ResolveValue(
|
||||
ObTemplateVariable definition,
|
||||
ObTaskEmployeeInfo employeeInfo,
|
||||
IReadOnlyDictionary<string, string> dynamicValues,
|
||||
ObCompany company,
|
||||
ObOnboardingTask task)
|
||||
{
|
||||
var sourceField = GetSourceField(definition);
|
||||
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.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(str)) return str;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return dynamicValues.TryGetValue(sourceField, out var dynamicValue)
|
||||
? dynamicValue
|
||||
: definition.DefaultValue;
|
||||
}
|
||||
|
||||
if (definition.VariableType == ContractVariableType.Company)
|
||||
return GetPropertyValue(company, ResolveCompanyField(definition)) ?? definition.DefaultValue;
|
||||
if (definition.VariableType == ContractVariableType.System && string.Equals(definition.VariableCode, "TASK_NO", StringComparison.OrdinalIgnoreCase))
|
||||
return task.TaskNo;
|
||||
return GetPropertyValue(company, ResolveCompanyField(sourceField), definition.FormatRule) ?? definition.DefaultValue;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static string GetPropertyValue(object source, string propertyName)
|
||||
private static string GetSourceField(ObTemplateVariable definition)
|
||||
{
|
||||
if (source == null || string.IsNullOrWhiteSpace(propertyName)) return null;
|
||||
return source.GetType().GetProperty(propertyName, global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.IgnoreCase)?.GetValue(source)?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(definition.EmployeeField))
|
||||
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;
|
||||
return definition.VariableCode.ToUpperInvariant() switch
|
||||
if (source == null || string.IsNullOrWhiteSpace(propertyName))
|
||||
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),
|
||||
"COMPANY_NAME" => nameof(ObCompany.CompanyName),
|
||||
"LEGAL_NAME" or "COMPANY_LEGAL_NAME" => nameof(ObCompany.LegalName),
|
||||
"UNIFIED_SOCIAL_CREDIT_CODE" => nameof(ObCompany.UnifiedSocialCreditCode),
|
||||
"LEGAL_REPRESENTATIVE" => nameof(ObCompany.LegalRepresentative),
|
||||
"REGISTERED_ADDRESS" => nameof(ObCompany.RegisteredAddress),
|
||||
"CONTACT_PHONE" => nameof(ObCompany.ContactPhone),
|
||||
_ => definition.VariableCode
|
||||
"FULLNAME" or "FULL_NAME" or "NAME" => nameof(ObTaskEmployeeInfo.EmployeeName),
|
||||
"MOBILE" or "MOBILEPHONE" or "MOBILE_PHONE" => nameof(ObTaskEmployeeInfo.Phone),
|
||||
"IDNUMBER" or "ID_NUMBER" or "IDENTITYNUMBER" => nameof(ObTaskEmployeeInfo.IdCardNumber),
|
||||
_ => sourceField
|
||||
};
|
||||
}
|
||||
private static string ResolveCompanyField(string sourceField)
|
||||
{
|
||||
return sourceField.ToUpperInvariant() switch
|
||||
{
|
||||
"COMPANYCODE" or "COMPANY_CODE" => nameof(ObCompany.CompanyCode),
|
||||
"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
|
||||
};
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
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)
|
||||
{
|
||||
using var input = new MemoryStream(docx);
|
||||
using var archive = new global::System.IO.Compression.ZipArchive(
|
||||
input,
|
||||
global::System.IO.Compression.ZipArchiveMode.Read);
|
||||
var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(entry => IsWordTextXml(entry.FullName)))
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
var document = global::System.Xml.Linq.XDocument.Load(stream);
|
||||
foreach (var paragraph in document.Descendants(WordNamespace + "p"))
|
||||
{
|
||||
var text = string.Concat(paragraph.Descendants(WordNamespace + "t").Select(node => node.Value));
|
||||
foreach (global::System.Text.RegularExpressions.Match match in PlaceholderRegex.Matches(text))
|
||||
codes.Add(match.Groups["code"].Value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return codes.OrderBy(code => code).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用任务变量快照替换 DOCX 中的占位符,支持占位符跨多个 Word 文本节点。
|
||||
/// </summary>
|
||||
public static byte[] Render(byte[] docx, IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 合同适用范围分页查询参数。
|
||||
/// </summary>
|
||||
public class OnboardingContractScopePageInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职业务公司 Id。
|
||||
/// </summary>
|
||||
public long? CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位 Id;为空时表示公司级配置。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同模板 Id。
|
||||
/// </summary>
|
||||
public long? TemplateId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增合同适用范围参数。
|
||||
/// </summary>
|
||||
public class OnboardingContractScopeAddInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职业务公司 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||
public long CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位 Id;为空时表示公司级配置。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同模板 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
||||
public long TemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同模板版本 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "模板版本Id不能为空")]
|
||||
public long TemplateVersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填、必传或必签。
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑合同适用范围参数。
|
||||
/// </summary>
|
||||
public class OnboardingContractScopeEditInput : OnboardingContractScopeAddInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务数据 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "适用范围Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+20
-25
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 定义合同模板、版本、变量和适用范围业务能力。
|
||||
/// 定义合同模板、DOCX 版本和自动识别变量的管理能力。
|
||||
/// </summary>
|
||||
public interface IOnboardingContractTemplateService : ITransient
|
||||
{
|
||||
@@ -9,59 +9,54 @@ public interface IOnboardingContractTemplateService : ITransient
|
||||
/// 分页查询合同模板主档案。
|
||||
/// </summary>
|
||||
Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 查询合同模板详情。
|
||||
/// </summary>
|
||||
Task<ObContractTemplate> Detail(BaseIdInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 新增合同模板主档案。
|
||||
/// </summary>
|
||||
Task Add(OnboardingContractTemplateAddInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 编辑合同模板主档案。
|
||||
/// </summary>
|
||||
Task Edit(OnboardingContractTemplateEditInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被版本、适用范围或任务合同引用的模板。
|
||||
/// 删除未被版本、入职模板或任务合同引用的模板。
|
||||
/// </summary>
|
||||
Task Delete(BaseIdListInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询指定合同模板的版本。
|
||||
/// </summary>
|
||||
Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 新增合同模板 DOCX 版本。
|
||||
/// 新增合同模板 DOCX 版本并自动识别变量。
|
||||
/// </summary>
|
||||
Task AddVersion(OnboardingContractVersionAddInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 启用或停用合同模板版本。
|
||||
/// </summary>
|
||||
Task SetVersionStatus(OnboardingContractVersionStatusInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询指定模板版本的变量定义。
|
||||
/// 删除未被入职模板或任务使用的合同版本。
|
||||
/// </summary>
|
||||
Task DeleteVersion(BaseIdInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询指定模板版本自动识别出的变量。
|
||||
/// </summary>
|
||||
Task<SqlSugarPagedList<ObTemplateVariable>> VariablePage(OnboardingTemplateVariablePageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 为草稿模板版本新增变量。
|
||||
/// </summary>
|
||||
Task AddVariable(OnboardingTemplateVariableAddInput input);
|
||||
/// <summary>
|
||||
/// 编辑草稿模板版本中的变量。
|
||||
/// 编辑并确认模板版本中的变量定义。
|
||||
/// </summary>
|
||||
Task EditVariable(OnboardingTemplateVariableEditInput input);
|
||||
/// <summary>
|
||||
/// 删除草稿模板版本中的变量。
|
||||
/// </summary>
|
||||
Task DeleteVariable(BaseIdListInput input); Task<SqlSugarPagedList<ObContractScope>> ScopePage(OnboardingContractScopePageInput input);
|
||||
/// <summary>
|
||||
/// 新增公司或岗位的合同适用范围。
|
||||
/// </summary>
|
||||
Task AddScope(OnboardingContractScopeAddInput input);
|
||||
/// <summary>
|
||||
/// 编辑合同适用范围。
|
||||
/// </summary>
|
||||
Task EditScope(OnboardingContractScopeEditInput input);
|
||||
/// <summary>
|
||||
/// 删除合同适用范围。
|
||||
/// </summary>
|
||||
Task DeleteScope(BaseIdListInput input);}
|
||||
}
|
||||
+187
-142
@@ -5,6 +5,13 @@ namespace SimpleAdmin.Application;
|
||||
/// </summary>
|
||||
public class OnboardingContractTemplateService : DbRepository<ObContractTemplate>, IOnboardingContractTemplateService
|
||||
{
|
||||
private readonly IFileService _fileService;
|
||||
|
||||
public OnboardingContractTemplateService(IFileService fileService)
|
||||
{
|
||||
_fileService = fileService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询合同模板主档案。
|
||||
/// </summary>
|
||||
@@ -49,29 +56,92 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被版本、适用范围或任务合同引用的模板。
|
||||
/// 删除未被入职模板或任务合同引用的模板(级联删除未使用的版本和变量)。
|
||||
/// </summary>
|
||||
public async Task Delete(BaseIdListInput input)
|
||||
{
|
||||
if (input.Ids.Count == 0)
|
||||
return;
|
||||
|
||||
var hasVersions = await Context.Queryable<ObContractTemplateVersion>()
|
||||
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
||||
if (hasVersions)
|
||||
throw Oops.Bah("模板已有版本,不能删除");
|
||||
|
||||
var hasScopes = await Context.Queryable<ObContractScope>()
|
||||
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
||||
if (hasScopes)
|
||||
throw Oops.Bah("模板已配置适用范围,不能删除");
|
||||
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplateContract>()
|
||||
.AnyAsync(it => input.Ids.Contains(it.ContractTemplateId));
|
||||
if (hasOnboardingTemplates)
|
||||
throw Oops.Bah("模板已被入职模板引用,不能删除,请先在入职模板中移除关联");
|
||||
|
||||
var hasContracts = await Context.Queryable<ObTaskContract>()
|
||||
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
||||
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<ObContractScope>()
|
||||
.Where(it => input.Ids.Contains(it.TemplateId))
|
||||
.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("合同版本不存在");
|
||||
|
||||
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<ObContractScope>()
|
||||
.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>
|
||||
@@ -92,18 +162,60 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
public async Task AddVersion(OnboardingContractVersionAddInput input)
|
||||
{
|
||||
await Detail(new BaseIdInput { Id = input.TemplateId });
|
||||
|
||||
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))
|
||||
throw Oops.Bah("合同模板仅支持DOCX文件");
|
||||
throw Oops.Bah("合同模板仅支持 DOCX 文件");
|
||||
|
||||
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)
|
||||
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 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>
|
||||
@@ -122,10 +234,11 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
|
||||
if (input.VersionStatus == ContractTemplateVersionStatus.Enabled)
|
||||
{
|
||||
var hasVariables = await Context.Queryable<ObTemplateVariable>()
|
||||
.AnyAsync(it => it.TemplateVersionId == input.Id);
|
||||
if (!hasVariables)
|
||||
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;
|
||||
@@ -143,19 +256,8 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
.OrderBy(it => it.VariableCode)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为草稿模板版本新增变量。
|
||||
/// </summary>
|
||||
public async Task AddVariable(OnboardingTemplateVariableAddInput input)
|
||||
{
|
||||
await EnsureVersionDraft(input.TemplateVersionId);
|
||||
await EnsureVariableUnique(input.TemplateVersionId, input.VariableCode, 0);
|
||||
await Context.Insertable(input.Adapt<ObTemplateVariable>()).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑草稿模板版本中的变量。
|
||||
/// 编辑模板版本中自动识别出的变量,不允许修改 DOCX 占位符编码。
|
||||
/// </summary>
|
||||
public async Task EditVariable(OnboardingTemplateVariableEditInput input)
|
||||
{
|
||||
@@ -163,77 +265,68 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
?? throw Oops.Bah("模板变量不存在");
|
||||
if (variable.TemplateVersionId != input.TemplateVersionId)
|
||||
throw Oops.Bah("不允许变更变量所属版本");
|
||||
if (!string.Equals(variable.VariableCode, input.VariableCode, StringComparison.Ordinal))
|
||||
throw Oops.Bah("DOCX 占位符编码由系统识别,不能修改");
|
||||
|
||||
await EnsureVersionDraft(input.TemplateVersionId);
|
||||
await EnsureVariableUnique(input.TemplateVersionId, input.VariableCode, input.Id);
|
||||
await Context.Updateable(input.Adapt<ObTemplateVariable>()).ExecuteCommandAsync();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除草稿模板版本中的变量。
|
||||
/// </summary>
|
||||
public async Task DeleteVariable(BaseIdListInput input)
|
||||
private static ObTemplateVariable BuildDetectedVariable(long versionId, string code, ObVariableDefinition def, int index)
|
||||
{
|
||||
if (input.Ids.Count == 0)
|
||||
return;
|
||||
var separatorIndex = code.IndexOf('.');
|
||||
var prefix = separatorIndex > 0 ? code[..separatorIndex].ToLowerInvariant() : string.Empty;
|
||||
var sourceField = separatorIndex > 0 ? code[(separatorIndex + 1)..] : code;
|
||||
var variableType = prefix switch
|
||||
{
|
||||
"employee" => ContractVariableType.Employee,
|
||||
"company" => ContractVariableType.Company,
|
||||
"hr" => ContractVariableType.Contract,
|
||||
"system" => ContractVariableType.System,
|
||||
_ => ContractVariableType.Unresolved
|
||||
};
|
||||
|
||||
var versionIds = await Context.Queryable<ObTemplateVariable>()
|
||||
.Where(it => input.Ids.Contains(it.Id))
|
||||
.Select(it => it.TemplateVersionId)
|
||||
.ToListAsync();
|
||||
var varName = def != null && !string.IsNullOrWhiteSpace(def.VariableName) ? def.VariableName : sourceField;
|
||||
var dataType = def != null && !string.IsNullOrWhiteSpace(def.DataType) ? def.DataType : InferDataType(sourceField);
|
||||
|
||||
foreach (var versionId in versionIds.Distinct())
|
||||
await EnsureVersionDraft(versionId);
|
||||
|
||||
await Context.Deleteable<ObTemplateVariable>().In(input.Ids).ExecuteCommandAsync();
|
||||
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,
|
||||
Required = def?.Required ?? true,
|
||||
HrEditable = variableType == ContractVariableType.Contract,
|
||||
SortCode = (index + 1) * 10,
|
||||
Remark = def?.Remark
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询合同适用范围。
|
||||
/// </summary>
|
||||
public async Task<SqlSugarPagedList<ObContractScope>> ScopePage(OnboardingContractScopePageInput input)
|
||||
private static string InferDataType(string sourceField)
|
||||
{
|
||||
return await Context.Queryable<ObContractScope>()
|
||||
.WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value)
|
||||
.WhereIF(input.PositionId.HasValue, it => it.PositionId == input.PositionId.Value)
|
||||
.WhereIF(input.TemplateId.HasValue, it => it.TemplateId == input.TemplateId.Value)
|
||||
.OrderBy(it => it.SortCode)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
}
|
||||
var normalized = sourceField.ToLowerInvariant();
|
||||
if (normalized.Contains("date") || normalized.EndsWith("at"))
|
||||
return "date";
|
||||
if (normalized.Contains("salary")
|
||||
|| normalized.Contains("amount")
|
||||
|| normalized.Contains("wage")
|
||||
|| normalized.Contains("copies"))
|
||||
{
|
||||
return "decimal";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增公司或岗位的合同适用范围。
|
||||
/// </summary>
|
||||
public async Task AddScope(OnboardingContractScopeAddInput input)
|
||||
{
|
||||
await EnsureScopeValid(input.CompanyId, input.PositionId, input.TemplateId, input.TemplateVersionId);
|
||||
await EnsureScopeUnique(input.CompanyId, input.PositionId, input.TemplateVersionId, 0);
|
||||
await Context.Insertable(input.Adapt<ObContractScope>()).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑合同适用范围。
|
||||
/// </summary>
|
||||
public async Task EditScope(OnboardingContractScopeEditInput input)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除合同适用范围。
|
||||
/// </summary>
|
||||
public async Task DeleteScope(BaseIdListInput input)
|
||||
{
|
||||
if (input.Ids.Count == 0)
|
||||
return;
|
||||
|
||||
await Context.Deleteable<ObContractScope>().In(input.Ids).ExecuteCommandAsync();
|
||||
return "string";
|
||||
}
|
||||
|
||||
private async Task EnsureCodeUnique(string code, long id)
|
||||
@@ -243,52 +336,4 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
if (exists)
|
||||
throw Oops.Bah("模板编码已存在");
|
||||
}
|
||||
|
||||
private async Task EnsureVersionDraft(long versionId)
|
||||
{
|
||||
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(versionId)
|
||||
?? throw Oops.Bah("模板版本不存在");
|
||||
if (version.VersionStatus != ContractTemplateVersionStatus.Draft)
|
||||
throw Oops.Bah("只有草稿版本可维护变量");
|
||||
}
|
||||
|
||||
private async Task EnsureVariableUnique(long versionId, string code, long id)
|
||||
{
|
||||
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>()
|
||||
.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)
|
||||
?? throw Oops.Bah("合同版本不存在");
|
||||
if (version.TemplateId != templateId || version.VersionStatus != ContractTemplateVersionStatus.Enabled)
|
||||
throw Oops.Bah("只能配置该模板的已启用合同版本");
|
||||
}
|
||||
|
||||
private async Task EnsureScopeUnique(long companyId, long? positionId, long versionId, long id)
|
||||
{
|
||||
var exists = await Context.Queryable<ObContractScope>()
|
||||
.AnyAsync(it => it.CompanyId == companyId
|
||||
&& it.PositionId == positionId
|
||||
&& it.TemplateVersionId == versionId
|
||||
&& it.Id != id);
|
||||
if (exists)
|
||||
throw Oops.Bah("该公司、岗位和合同版本的适用范围已存在");
|
||||
}
|
||||
}
|
||||
|
||||
-76
@@ -91,79 +91,3 @@ public class OnboardingMaterialTypeSaveInput
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "enable";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存公司岗位动态字段要求参数。
|
||||
/// </summary>
|
||||
public class OnboardingFormRequirementSaveInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务数据 Id。
|
||||
/// </summary>
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入职业务公司 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||
public long CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位 Id;为空时表示公司级配置。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 动态字段 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "字段Id不能为空")]
|
||||
public long FieldId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填、必传或必签。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存公司岗位材料要求参数。
|
||||
/// </summary>
|
||||
public class OnboardingMaterialRequirementSaveInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务数据 Id。
|
||||
/// </summary>
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入职业务公司 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||
public long CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位 Id;为空时表示公司级配置。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料类型 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "资料类型Id不能为空")]
|
||||
public long MaterialTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填、必传或必签。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
+8
-27
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 定义动态字段、材料类型及公司岗位要求配置业务能力。
|
||||
/// 定义员工动态补充字段与上传材料类型的基础配置能力。
|
||||
/// </summary>
|
||||
public interface IOnboardingEntryConfigService : ITransient
|
||||
{
|
||||
@@ -9,48 +9,29 @@ public interface IOnboardingEntryConfigService : ITransient
|
||||
/// 查询全部动态入职字段。
|
||||
/// </summary>
|
||||
Task<List<ObFormField>> ListFormFields();
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑动态入职字段。
|
||||
/// </summary>
|
||||
Task SaveFormField(OnboardingFormFieldSaveInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被填写要求引用的动态字段。
|
||||
/// 删除未被入职模板引用的动态字段。
|
||||
/// </summary>
|
||||
Task DeleteFormFields(BaseIdListInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 查询全部入职材料类型。
|
||||
/// </summary>
|
||||
Task<List<ObMaterialType>> ListMaterialTypes();
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑入职材料类型。
|
||||
/// </summary>
|
||||
Task SaveMaterialType(OnboardingMaterialTypeSaveInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被要求或上传记录引用的材料类型。
|
||||
/// 删除未被入职模板或上传记录引用的材料类型。
|
||||
/// </summary>
|
||||
Task DeleteMaterialTypes(BaseIdListInput input);
|
||||
/// <summary>
|
||||
/// 查询指定公司的动态字段填写要求。
|
||||
/// </summary>
|
||||
Task<List<ObFormRequirement>> ListFormRequirements(long companyId);
|
||||
/// <summary>
|
||||
/// 新增或编辑公司岗位动态字段要求。
|
||||
/// </summary>
|
||||
Task SaveFormRequirement(OnboardingFormRequirementSaveInput input);
|
||||
/// <summary>
|
||||
/// 删除动态字段填写要求。
|
||||
/// </summary>
|
||||
Task DeleteFormRequirements(BaseIdListInput input);
|
||||
/// <summary>
|
||||
/// 查询指定公司的材料上传要求。
|
||||
/// </summary>
|
||||
Task<List<ObMaterialRequirement>> ListMaterialRequirements(long companyId);
|
||||
/// <summary>
|
||||
/// 新增或编辑公司岗位材料要求。
|
||||
/// </summary>
|
||||
Task SaveMaterialRequirement(OnboardingMaterialRequirementSaveInput input);
|
||||
/// <summary>
|
||||
/// 删除材料上传要求。
|
||||
/// </summary>
|
||||
Task DeleteMaterialRequirements(BaseIdListInput input);
|
||||
}
|
||||
+37
-146
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 维护员工端动态字段、材料类型及公司岗位要求。
|
||||
/// 维护员工端动态补充字段和材料类型;与公司、岗位的组合关系由入职模板统一管理。
|
||||
/// </summary>
|
||||
public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
{
|
||||
@@ -27,8 +27,12 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
/// </summary>
|
||||
public async Task SaveFormField(OnboardingFormFieldSaveInput input)
|
||||
{
|
||||
var fieldCodeExists = await _db.Queryable<ObFormField>()
|
||||
.AnyAsync(x => x.FieldCode == input.FieldCode && x.Id != input.Id);
|
||||
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)
|
||||
throw Oops.Bah("字段编码已存在");
|
||||
|
||||
@@ -38,7 +42,7 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
FieldCode = input.FieldCode,
|
||||
FieldName = input.FieldName,
|
||||
DataType = input.DataType,
|
||||
OptionsJson = input.OptionsJson,
|
||||
OptionsJson = NormalizeOptionsJson(input.OptionsJson),
|
||||
ValidationRule = input.ValidationRule,
|
||||
Placeholder = input.Placeholder,
|
||||
Status = input.Status
|
||||
@@ -57,6 +61,27 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
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>
|
||||
@@ -67,8 +92,10 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
|
||||
var isReferenced = await _db.Queryable<ObFormRequirement>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.FieldId));
|
||||
if (isReferenced)
|
||||
throw Oops.Bah("字段已被填写要求引用,不能删除");
|
||||
var isReferencedByTemplate = await _db.Queryable<ObOnboardingTemplateFormField>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.FieldId));
|
||||
if (isReferenced || isReferencedByTemplate)
|
||||
throw Oops.Bah("字段已被入职模板引用,不能删除");
|
||||
|
||||
await _db.Deleteable<ObFormField>().In(input.Ids).ExecuteCommandAsync();
|
||||
}
|
||||
@@ -130,8 +157,10 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
|
||||
var isReferenced = await _db.Queryable<ObMaterialRequirement>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||
if (isReferenced)
|
||||
throw Oops.Bah("资料类型已被资料要求引用,不能删除");
|
||||
var isReferencedByTemplate = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||
if (isReferenced || isReferencedByTemplate)
|
||||
throw Oops.Bah("资料类型已被入职模板引用,不能删除");
|
||||
|
||||
var hasUploads = await _db.Queryable<ObTaskMaterial>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||
@@ -140,142 +169,4 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
|
||||
await _db.Deleteable<ObMaterialType>().In(input.Ids).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定公司的动态字段填写要求。
|
||||
/// </summary>
|
||||
public Task<List<ObFormRequirement>> ListFormRequirements(long companyId)
|
||||
{
|
||||
return _db.Queryable<ObFormRequirement>()
|
||||
.Where(x => x.CompanyId == companyId)
|
||||
.OrderBy(x => x.SortCode)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑公司岗位动态字段要求。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除动态字段填写要求。
|
||||
/// </summary>
|
||||
public Task DeleteFormRequirements(BaseIdListInput input)
|
||||
{
|
||||
if (input.Ids.Count == 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return _db.Deleteable<ObFormRequirement>().In(input.Ids).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定公司的材料上传要求。
|
||||
/// </summary>
|
||||
public Task<List<ObMaterialRequirement>> ListMaterialRequirements(long companyId)
|
||||
{
|
||||
return _db.Queryable<ObMaterialRequirement>()
|
||||
.Where(x => x.CompanyId == companyId)
|
||||
.OrderBy(x => x.SortCode)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑公司岗位材料要求。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除材料上传要求。
|
||||
/// </summary>
|
||||
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("岗位不存在、未启用或不属于该公司");
|
||||
}
|
||||
}
|
||||
+5
@@ -14,4 +14,9 @@ public interface IOnboardingTaskLifecycleService : ITransient
|
||||
/// 撤销尚未归档的入职任务并使员工链接失效。
|
||||
/// </summary>
|
||||
Task Revoke(OnboardingAuditActionInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除已撤销的入职任务及其全部关联快照数据。
|
||||
/// </summary>
|
||||
Task Delete(BaseIdInput input);
|
||||
}
|
||||
|
||||
+42
@@ -55,6 +55,48 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
|
||||
await Log(task, OnboardingAuditAction.Revoke, input.AuditOpinion);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除已撤销的入职任务及其全部关联快照数据。
|
||||
/// </summary>
|
||||
public async Task Delete(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();
|
||||
|
||||
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<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)
|
||||
{
|
||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId)
|
||||
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
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? 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>
|
||||
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>
|
||||
/// 适用岗位 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();
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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 positionId);
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职模板聚合配置及公司岗位匹配能力。
|
||||
/// </summary>
|
||||
public class OnboardingTemplateService : IOnboardingTemplateService
|
||||
{
|
||||
private readonly SqlSugarScope _db;
|
||||
|
||||
public OnboardingTemplateService()
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
}
|
||||
|
||||
/// <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(!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,
|
||||
TemplateCode = summary.TemplateCode,
|
||||
TemplateName = summary.TemplateName,
|
||||
CompanyId = summary.CompanyId,
|
||||
CompanyName = summary.CompanyName,
|
||||
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 templateId = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
var codeExists = await _db.Queryable<ObOnboardingTemplate>()
|
||||
.AnyAsync(item => item.TemplateCode == input.TemplateCode && item.Id != templateId);
|
||||
if (codeExists)
|
||||
throw Oops.Bah("入职模板编码已存在");
|
||||
|
||||
var entity = new ObOnboardingTemplate
|
||||
{
|
||||
Id = templateId,
|
||||
TemplateCode = input.TemplateCode.Trim(),
|
||||
TemplateName = input.TemplateName.Trim(),
|
||||
CompanyId = input.CompanyId,
|
||||
Description = input.Description,
|
||||
Status = input.Status
|
||||
};
|
||||
|
||||
await _db.Ado.BeginTranAsync();
|
||||
try
|
||||
{
|
||||
if (input.Id.HasValue)
|
||||
{
|
||||
var exists = 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 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.Deleteable<ObOnboardingTemplate>()
|
||||
.Where(it => input.Ids.Contains(it.Id))
|
||||
.ExecuteCommandAsync();
|
||||
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 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.Status == "enable"
|
||||
&& templateIds.Contains(item.Id))
|
||||
.OrderBy(item => item.TemplateName)
|
||||
.ToListAsync();
|
||||
return await BuildOutputs(templates);
|
||||
}
|
||||
|
||||
private static void NormalizeAndValidateInput(OnboardingTemplateSaveInput input)
|
||||
{
|
||||
input.TemplateCode = input.TemplateCode?.Trim();
|
||||
input.TemplateName = input.TemplateName?.Trim();
|
||||
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
|
||||
input.Contracts = input.Contracts?
|
||||
.GroupBy(item => item.ContractTemplateVersionId)
|
||||
.Select(group => group.First())
|
||||
.ToList() ?? new List<OnboardingTemplateContractInput>();
|
||||
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.TemplateCode) || string.IsNullOrWhiteSpace(input.TemplateName))
|
||||
throw Oops.Bah("请填写入职模板编码和名称");
|
||||
if (input.CompanyId <= 0)
|
||||
throw Oops.Bah("请选择所属公司");
|
||||
if (input.PositionIds.Count == 0)
|
||||
throw Oops.Bah("请至少选择一个适用岗位");
|
||||
if (input.Contracts.Count == 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 positionCount = await _db.Queryable<ObPosition>()
|
||||
.CountAsync(item => input.PositionIds.Contains(item.Id)
|
||||
&& item.CompanyId == input.CompanyId
|
||||
&& 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("合同模板与版本不匹配");
|
||||
}
|
||||
|
||||
if (input.Status == "enable")
|
||||
{
|
||||
if (versions.Any(item => item.VersionStatus != ContractTemplateVersionStatus.Enabled))
|
||||
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 companies = await _db.Queryable<ObCompany>()
|
||||
.Where(item => companyIds.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,
|
||||
TemplateCode = template.TemplateCode,
|
||||
TemplateName = template.TemplateName,
|
||||
CompanyId = template.CompanyId,
|
||||
CompanyName = companies.FirstOrDefault(item => item.Id == template.CompanyId)?.CompanyName,
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,11 @@ public class OnboardingPositionPageInput : BasePageInput
|
||||
/// </summary>
|
||||
public long? CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位编码。
|
||||
/// </summary>
|
||||
public virtual string PositionCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位名称。
|
||||
/// </summary>
|
||||
|
||||
@@ -12,9 +12,8 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
|
||||
{
|
||||
return await Context.Queryable<ObPosition>()
|
||||
.WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value)
|
||||
.WhereIF(
|
||||
!string.IsNullOrWhiteSpace(input.PositionName),
|
||||
it => it.PositionName.Contains(input.PositionName) || it.PositionCode.Contains(input.PositionName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.PositionCode), it => it.PositionCode.Contains(input.PositionCode))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.PositionName), it => it.PositionName.Contains(input.PositionName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
||||
.OrderBy(it => it.PositionName)
|
||||
|
||||
+17
@@ -55,6 +55,23 @@ public class OnboardingSaveMaterialInput
|
||||
public long FileId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除员工在当前可编辑任务中上传的一条材料。
|
||||
/// </summary>
|
||||
public class OnboardingDeleteMaterialInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 员工填写链接中的明文 Token。
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.DataAnnotations.Required]
|
||||
public string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务材料记录 Id。
|
||||
/// </summary>
|
||||
public long TaskMaterialId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工确认阅读合同参数。
|
||||
/// </summary>
|
||||
|
||||
+9
@@ -9,18 +9,27 @@ public interface IOnboardingPublicSubmissionService : ITransient
|
||||
/// 保存候选人填写的动态字段值。
|
||||
/// </summary>
|
||||
Task SaveDynamicValues(OnboardingSaveDynamicValuesInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 关联候选人已经上传的系统文件与材料类型。
|
||||
/// </summary>
|
||||
Task SaveMaterial(OnboardingSaveMaterialInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除可编辑任务中的一条已上传材料关联。
|
||||
/// </summary>
|
||||
Task DeleteMaterial(OnboardingDeleteMaterialInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 记录候选人已阅读并确认指定任务合同。
|
||||
/// </summary>
|
||||
Task ConfirmContract(OnboardingConfirmContractInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 校验全部必填资料后提交入职任务进入审核。
|
||||
/// </summary>
|
||||
Task Submit(OnboardingSubmitInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 在保存文件前校验材料适用范围、扩展名、大小和数量。
|
||||
/// </summary>
|
||||
|
||||
+81
-115
@@ -1,92 +1,66 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现员工端入职资料提交业务能力。
|
||||
/// 实现员工端动态资料、上传材料、合同确认和最终提交能力。
|
||||
/// </summary>
|
||||
public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionService
|
||||
{
|
||||
private readonly SqlSugarScope _db;
|
||||
private readonly IOnboardingContractSnapshotService _snapshotService;
|
||||
|
||||
public OnboardingPublicSubmissionService(
|
||||
IOnboardingContractSnapshotService snapshotService)
|
||||
public OnboardingPublicSubmissionService(IOnboardingContractSnapshotService snapshotService)
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
_snapshotService = snapshotService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存候选人填写的动态字段值。
|
||||
/// 保存候选人填写的动态补充字段值。
|
||||
/// </summary>
|
||||
public async Task SaveDynamicValues(OnboardingSaveDynamicValuesInput input)
|
||||
{
|
||||
var task = await GetEditableTask(input.Token);
|
||||
var requirements = await GetEffectiveFormRequirements(task);
|
||||
var fieldIds = input.Values.Select(item => item.FieldId).Distinct().ToList();
|
||||
var hasInapplicableField = fieldIds.Any(id => requirements.All(requirement => requirement.FieldId != id));
|
||||
if (hasInapplicableField)
|
||||
throw Oops.Bah("填写字段不适用于当前公司或岗位");
|
||||
|
||||
var fields = await _db.Queryable<ObFormField>()
|
||||
.Where(field => fieldIds.Contains(field.Id) && field.Status == "enable")
|
||||
.ToListAsync();
|
||||
foreach (var value in input.Values)
|
||||
{
|
||||
var field = fields.FirstOrDefault(item => item.Id == value.FieldId)
|
||||
?? throw Oops.Bah("填写字段不存在或已停用");
|
||||
var old = await _db.Queryable<ObTaskFormValue>()
|
||||
.FirstAsync(item => item.TaskId == task.Id && item.FieldId == field.Id);
|
||||
var row = new ObTaskFormValue
|
||||
{
|
||||
Id = old?.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||
TaskId = task.Id,
|
||||
FieldId = field.Id,
|
||||
FieldCode = field.FieldCode,
|
||||
FieldName = field.FieldName,
|
||||
FieldValue = value.FieldValue,
|
||||
SubmitRound = task.SubmitCount + 1
|
||||
};
|
||||
|
||||
if (old == null)
|
||||
{
|
||||
await _db.Insertable(row).ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _db.Updateable(row).ExecuteCommandAsync();
|
||||
}
|
||||
}
|
||||
await GetEditableTask(input.Token);
|
||||
throw Oops.Bah("新版入职登记表不再使用旧动态字段接口,请通过员工基础信息接口保存补充变量");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关联候选人已经上传的系统文件与材料类型。
|
||||
/// 关联已上传 OSS 文件与任务材料要求。
|
||||
/// </summary>
|
||||
public async Task SaveMaterial(OnboardingSaveMaterialInput input)
|
||||
{
|
||||
var task = await GetEditableTask(input.Token);
|
||||
var requirements = await GetEffectiveMaterialRequirements(task);
|
||||
var materialIsApplicable = requirements.Any(requirement => requirement.MaterialTypeId == input.MaterialTypeId);
|
||||
if (!materialIsApplicable)
|
||||
throw Oops.Bah("资料类型不适用于当前公司或岗位");
|
||||
|
||||
var material = await _db.Queryable<ObMaterialType>()
|
||||
.FirstAsync(item => item.Id == input.MaterialTypeId && item.Status == "enable")
|
||||
?? throw Oops.Bah("资料类型不存在或已停用");
|
||||
var requirement = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||
.FirstAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
||||
?? throw Oops.Bah("材料类型不属于当前入职模板");
|
||||
var fileExists = await _db.Queryable<SysFile>().AnyAsync(item => item.Id == input.FileId);
|
||||
if (!fileExists)
|
||||
throw Oops.Bah("上传文件不存在");
|
||||
|
||||
var materialCount = await _db.Queryable<ObTaskMaterial>()
|
||||
.CountAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId);
|
||||
if (materialCount >= material.MaxFileCount)
|
||||
throw Oops.Bah("该资料已达到最大上传数量");
|
||||
var existingMaterials = await _db.Queryable<ObTaskMaterial>()
|
||||
.Where(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
||||
.ToListAsync();
|
||||
var supplementMaterial = existingMaterials.FirstOrDefault(
|
||||
item => item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
|
||||
if (supplementMaterial != null)
|
||||
{
|
||||
supplementMaterial.FileId = input.FileId;
|
||||
supplementMaterial.SubmitRound = task.SubmitCount + 1;
|
||||
supplementMaterial.UploadedAt = DateTime.Now;
|
||||
supplementMaterial.ReviewStatus = TaskMaterialReviewStatus.Pending;
|
||||
supplementMaterial.ReviewRemark = null;
|
||||
await _db.Updateable(supplementMaterial).ExecuteCommandAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingMaterials.Count >= requirement.MaxFileCount)
|
||||
throw Oops.Bah("该材料已达到最大上传数量");
|
||||
|
||||
await _db.Insertable(new ObTaskMaterial
|
||||
{
|
||||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||
TaskId = task.Id,
|
||||
MaterialTypeId = material.Id,
|
||||
MaterialName = material.MaterialName,
|
||||
MaterialTypeId = requirement.MaterialTypeId,
|
||||
MaterialName = requirement.MaterialName,
|
||||
FileId = input.FileId,
|
||||
SubmitRound = task.SubmitCount + 1,
|
||||
UploadedAt = DateTime.Now
|
||||
@@ -94,7 +68,26 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录候选人已阅读并确认指定任务合同。
|
||||
/// 任务仍可编辑时删除一条已上传材料。
|
||||
/// </summary>
|
||||
public async Task DeleteMaterial(
|
||||
OnboardingDeleteMaterialInput input)
|
||||
{
|
||||
var task = await GetEditableTask(input.Token);
|
||||
var material = await _db.Queryable<ObTaskMaterial>()
|
||||
.FirstAsync(item =>
|
||||
item.Id == input.TaskMaterialId
|
||||
&& item.TaskId == task.Id);
|
||||
if (material == null)
|
||||
throw Oops.Bah("上传材料不存在");
|
||||
|
||||
await _db.Deleteable<ObTaskMaterial>()
|
||||
.In(material.Id)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录候选人已经查看并确认动态生成的合同。
|
||||
/// </summary>
|
||||
public async Task ConfirmContract(OnboardingConfirmContractInput input)
|
||||
{
|
||||
@@ -102,15 +95,16 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
.FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == task.Id)
|
||||
?? throw Oops.Bah("合同不存在");
|
||||
if (!contract.GeneratedDocxFileId.HasValue || !contract.ReadAt.HasValue)
|
||||
throw Oops.Bah("请先查看已经生成的合同内容");
|
||||
|
||||
contract.ReadAt ??= DateTime.Now;
|
||||
contract.ConfirmedAt = DateTime.Now;
|
||||
contract.ContractStatus = TaskContractStatus.Confirmed;
|
||||
await _db.Updateable(contract).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验全部必填资料后提交入职任务进入审核。
|
||||
/// 校验固定信息、动态字段、材料、合同变量和合同确认后提交审核。
|
||||
/// </summary>
|
||||
public async Task Submit(OnboardingSubmitInput input)
|
||||
{
|
||||
@@ -120,26 +114,15 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
var basicInfoIncomplete = info == null
|
||||
|| string.IsNullOrWhiteSpace(info.EmployeeName)
|
||||
|| string.IsNullOrWhiteSpace(info.Phone)
|
||||
|| string.IsNullOrWhiteSpace(info.IdCardNumber);
|
||||
|| string.IsNullOrWhiteSpace(info.IdCardNumber)
|
||||
|| string.IsNullOrWhiteSpace(info.BankCardNumber)
|
||||
|| string.IsNullOrWhiteSpace(info.BankName);
|
||||
if (basicInfoIncomplete)
|
||||
throw Oops.Bah("请先完整填写基本信息");
|
||||
|
||||
await _snapshotService.Build(task.Id, info);
|
||||
|
||||
// 提交时统一完成动态字段、材料、合同变量和合同确认校验,保存阶段只负责暂存。
|
||||
var requiredFields = (await GetEffectiveFormRequirements(task))
|
||||
.Where(item => item.Required)
|
||||
.Select(item => item.FieldId)
|
||||
.ToList();
|
||||
var values = await _db.Queryable<ObTaskFormValue>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync();
|
||||
var hasMissingRequiredField = requiredFields.Any(
|
||||
id => values.All(item => item.FieldId != id || string.IsNullOrWhiteSpace(item.FieldValue)));
|
||||
if (hasMissingRequiredField)
|
||||
throw Oops.Bah("仍有必填动态信息未填写");
|
||||
|
||||
var requiredMaterials = (await GetEffectiveMaterialRequirements(task))
|
||||
var requiredMaterials = (await GetMaterialRequirements(task.Id))
|
||||
.Where(item => item.Required)
|
||||
.Select(item => item.MaterialTypeId)
|
||||
.ToList();
|
||||
@@ -149,14 +132,14 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
var hasMissingRequiredMaterial = requiredMaterials.Any(
|
||||
id => materials.All(item => item.MaterialTypeId != id));
|
||||
if (hasMissingRequiredMaterial)
|
||||
throw Oops.Bah("仍有必传资料未上传");
|
||||
throw Oops.Bah("仍有必传材料未上传");
|
||||
|
||||
await EnsureRequiredContractVariables(task);
|
||||
|
||||
var hasUnconfirmedRequiredContract = await _db.Queryable<ObTaskContract>()
|
||||
.AnyAsync(item => item.TaskId == task.Id && item.Required && item.ConfirmedAt == null);
|
||||
if (hasUnconfirmedRequiredContract)
|
||||
throw Oops.Bah("请确认阅读全部必签合同");
|
||||
throw Oops.Bah("请查看并确认全部必签合同");
|
||||
|
||||
task.TaskStatus = OnboardingTaskStatus.Reviewing;
|
||||
task.SubmitCount++;
|
||||
@@ -171,42 +154,40 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
AuditRound = task.SubmitCount,
|
||||
AuditAction = OnboardingAuditAction.Submit,
|
||||
AuditOpinion = "员工提交入职资料",
|
||||
SnapshotJson = global::System.Text.Json.JsonSerializer.Serialize(new { info, values, materials }),
|
||||
SnapshotJson = global::System.Text.Json.JsonSerializer.Serialize(new { info, materials }),
|
||||
AuditedAt = DateTime.Now
|
||||
}).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在保存文件前校验材料适用范围、扩展名、大小和数量。
|
||||
/// 上传 OSS 前按照任务材料快照校验扩展名、文件大小和数量。
|
||||
/// </summary>
|
||||
public async Task ValidateMaterialUpload(OnboardingMaterialUploadValidationInput input)
|
||||
{
|
||||
var task = await GetEditableTask(input.Token);
|
||||
var requirements = await GetEffectiveMaterialRequirements(task);
|
||||
var materialIsApplicable = requirements.Any(requirement => requirement.MaterialTypeId == input.MaterialTypeId);
|
||||
if (!materialIsApplicable)
|
||||
throw Oops.Bah("资料类型不适用于当前公司或岗位");
|
||||
|
||||
var material = await _db.Queryable<ObMaterialType>()
|
||||
.FirstAsync(item => item.Id == input.MaterialTypeId && item.Status == "enable")
|
||||
?? throw Oops.Bah("资料类型不存在或已停用");
|
||||
var requirement = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||
.FirstAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
||||
?? throw Oops.Bah("材料类型不属于当前入职模板");
|
||||
var suffix = Path.GetExtension(input.FileName).TrimStart('.').ToLowerInvariant();
|
||||
var allowedExtensions = (material.AllowedExtensions ?? string.Empty)
|
||||
var allowedExtensions = (requirement.AllowedExtensions ?? string.Empty)
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(item => item.TrimStart('.').ToLowerInvariant());
|
||||
var suffixNotAllowed = allowedExtensions.Any() && !allowedExtensions.Contains(suffix);
|
||||
if (suffixNotAllowed)
|
||||
throw Oops.Bah("该资料不允许上传此文件类型");
|
||||
throw Oops.Bah("该材料不允许上传此文件类型");
|
||||
|
||||
var fileTooLarge = material.MaxFileSizeMb.HasValue
|
||||
&& input.FileLength > material.MaxFileSizeMb.Value * 1024L * 1024L;
|
||||
var fileTooLarge = requirement.MaxFileSizeMb.HasValue
|
||||
&& input.FileLength > requirement.MaxFileSizeMb.Value * 1024L * 1024L;
|
||||
if (fileTooLarge)
|
||||
throw Oops.Bah("文件超过允许的大小");
|
||||
|
||||
var materialCount = await _db.Queryable<ObTaskMaterial>()
|
||||
.CountAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId);
|
||||
if (materialCount >= material.MaxFileCount)
|
||||
throw Oops.Bah("该资料已达到最大上传数量");
|
||||
var materials = await _db.Queryable<ObTaskMaterial>()
|
||||
.Where(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
||||
.ToListAsync();
|
||||
var canReplaceSupplement = materials.Any(
|
||||
item => item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
|
||||
if (materials.Count >= requirement.MaxFileCount && !canReplaceSupplement)
|
||||
throw Oops.Bah("该材料已达到最大上传数量");
|
||||
}
|
||||
|
||||
private async Task EnsureRequiredContractVariables(ObOnboardingTask task)
|
||||
@@ -234,34 +215,19 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
throw Oops.Bah($"合同“{missing.contract.ContractName}”的必填变量“{missing.definition.VariableName}”尚未填写");
|
||||
}
|
||||
|
||||
private async Task<List<ObFormRequirement>> GetEffectiveFormRequirements(ObOnboardingTask task)
|
||||
private Task<List<ObTaskMaterialRequirement>> GetMaterialRequirements(long taskId)
|
||||
{
|
||||
var requirements = await _db.Queryable<ObFormRequirement>()
|
||||
.Where(item => item.CompanyId == task.CompanyId
|
||||
&& (item.PositionId == null || item.PositionId == task.PositionId))
|
||||
return _db.Queryable<ObTaskMaterialRequirement>()
|
||||
.Where(item => item.TaskId == taskId)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
|
||||
return requirements
|
||||
.GroupBy(item => item.FieldId)
|
||||
.Select(group => group.FirstOrDefault(item => item.PositionId == task.PositionId) ?? group.First())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<ObMaterialRequirement>> GetEffectiveMaterialRequirements(ObOnboardingTask task)
|
||||
{
|
||||
var requirements = await _db.Queryable<ObMaterialRequirement>()
|
||||
.Where(item => item.CompanyId == task.CompanyId
|
||||
&& (item.PositionId == null || item.PositionId == task.PositionId))
|
||||
.ToListAsync();
|
||||
|
||||
return requirements
|
||||
.GroupBy(item => item.MaterialTypeId)
|
||||
.Select(group => group.FirstOrDefault(item => item.PositionId == task.PositionId) ?? group.First())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<ObOnboardingTask> GetEditableTask(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
throw Oops.Bah("入职链接 Token 不能为空");
|
||||
|
||||
var hash = Convert.ToHexString(
|
||||
global::System.Security.Cryptography.SHA256.HashData(
|
||||
global::System.Text.Encoding.UTF8.GetBytes(token)));
|
||||
|
||||
+88
-80
@@ -1,22 +1,25 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 员工手机端入口;仅凭一次性任务链接访问,绝不创建正式员工。
|
||||
/// 员工手机端公开入口,仅凭一次性任务链接访问入职资料。
|
||||
/// </summary>
|
||||
public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
{
|
||||
private readonly SqlSugarScope _db;
|
||||
private readonly IOnboardingContractSnapshotService _snapshotService;
|
||||
private readonly IOnboardingContractDocumentService _contractDocumentService;
|
||||
|
||||
public OnboardingPublicTaskService(
|
||||
IOnboardingContractSnapshotService snapshotService)
|
||||
IOnboardingContractSnapshotService snapshotService,
|
||||
IOnboardingContractDocumentService contractDocumentService)
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
_snapshotService = snapshotService;
|
||||
_contractDocumentService = contractDocumentService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过有效 Token 获取员工端任务、字段、材料和合同信息。
|
||||
/// 通过有效 Token 获取任务创建时冻结的材料、合同和新版入职登记表信息。
|
||||
/// </summary>
|
||||
public async Task<OnboardingTaskPublicDetailOutput> Detail(OnboardingTaskPublicAccessInput input)
|
||||
{
|
||||
@@ -27,135 +30,140 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
link.AccessCount++;
|
||||
link.LastAccessedAt = DateTime.Now;
|
||||
await _db.Updateable(link).ExecuteCommandAsync();
|
||||
var materialRequirements = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
|
||||
var formRequirements = await GetEffectiveFormRequirements(task);
|
||||
var materialRequirements = await GetEffectiveMaterialRequirements(task);
|
||||
var fields = await _db.Queryable<ObFormField>()
|
||||
.Where(x => formRequirements.Select(r => r.FieldId).Contains(x.Id) && x.Status == "enable")
|
||||
.ToListAsync();
|
||||
var materialTypes = await _db.Queryable<ObMaterialType>()
|
||||
.Where(x => materialRequirements.Select(r => r.MaterialTypeId).Contains(x.Id) && x.Status == "enable")
|
||||
var contracts = await _db.Queryable<ObTaskContract>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync();
|
||||
var templateVersionIds = contracts
|
||||
.Select(item => item.TemplateVersionId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var contractVariables = new List<OnboardingPublicContractVariableOutput>();
|
||||
if (templateVersionIds.Count > 0)
|
||||
{
|
||||
var employeeVariables = await _db.Queryable<ObTemplateVariable>()
|
||||
.Where(item => templateVersionIds.Contains(item.TemplateVersionId)
|
||||
&& item.VariableType == ContractVariableType.Employee)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
contractVariables = employeeVariables.Select(item => new OnboardingPublicContractVariableOutput
|
||||
{
|
||||
VariableCode = item.VariableCode,
|
||||
VariableName = item.VariableName,
|
||||
VariableType = item.VariableType,
|
||||
DataType = item.DataType,
|
||||
DefaultValue = item.DefaultValue,
|
||||
OptionsJson = item.OptionsJson,
|
||||
Required = item.Required
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return new OnboardingTaskPublicDetailOutput
|
||||
{
|
||||
Task = task,
|
||||
EmployeeInfo = await _db.Queryable<ObTaskEmployeeInfo>().FirstAsync(it => it.TaskId == task.Id),
|
||||
Contracts = await _db.Queryable<ObTaskContract>()
|
||||
.Where(it => it.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
DynamicValues = await _db.Queryable<ObTaskFormValue>()
|
||||
.Where(x => x.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
EmployeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
.FirstAsync(item => item.TaskId == task.Id),
|
||||
Contracts = contracts,
|
||||
DynamicValues = new List<ObTaskFormValue>(),
|
||||
Materials = await _db.Queryable<ObTaskMaterial>()
|
||||
.Where(x => x.TaskId == task.Id)
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
FormFields = formRequirements
|
||||
.OrderBy(x => x.SortCode)
|
||||
.Select(requirement => fields.FirstOrDefault(field => field.Id == requirement.FieldId))
|
||||
.Where(field => field != null)
|
||||
.Select(field => new OnboardingPublicFormFieldOutput
|
||||
{
|
||||
FieldId = field.Id,
|
||||
FieldCode = field.FieldCode,
|
||||
FieldName = field.FieldName,
|
||||
DataType = field.DataType,
|
||||
OptionsJson = field.OptionsJson,
|
||||
Required = formRequirements.First(requirement => requirement.FieldId == field.Id).Required
|
||||
})
|
||||
.ToList(),
|
||||
MaterialRequirements = materialRequirements
|
||||
.OrderBy(x => x.SortCode)
|
||||
.Select(requirement => materialTypes.FirstOrDefault(material => material.Id == requirement.MaterialTypeId))
|
||||
.Where(material => material != null)
|
||||
.Select(material => new OnboardingPublicMaterialOutput
|
||||
{
|
||||
MaterialTypeId = material.Id,
|
||||
MaterialName = material.MaterialName,
|
||||
AllowedExtensions = material.AllowedExtensions,
|
||||
MaxFileCount = material.MaxFileCount,
|
||||
Required = materialRequirements.First(requirement => requirement.MaterialTypeId == material.Id).Required
|
||||
})
|
||||
.ToList()
|
||||
FormFields = new List<OnboardingPublicFormFieldOutput>(),
|
||||
MaterialRequirements = materialRequirements.Select(requirement => new OnboardingPublicMaterialOutput
|
||||
{
|
||||
MaterialTypeId = requirement.MaterialTypeId,
|
||||
MaterialName = requirement.MaterialName,
|
||||
AllowedExtensions = requirement.AllowedExtensions,
|
||||
MaxFileCount = requirement.MaxFileCount,
|
||||
Required = requirement.Required
|
||||
}).ToList(),
|
||||
ContractVariables = contractVariables
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存候选人固定基础信息并刷新合同变量快照。
|
||||
/// 保存候选人的固定基础信息并刷新尚未确认的合同变量快照。
|
||||
/// </summary>
|
||||
public async Task SaveEmployeeInfo(OnboardingTaskPublicSaveInput input)
|
||||
{
|
||||
var link = await GetActiveLink(input.Token);
|
||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(link.TaskId)
|
||||
?? throw Oops.Bah("入职任务不存在");
|
||||
if (task.TaskStatus != OnboardingTaskStatus.PendingFill && task.TaskStatus != OnboardingTaskStatus.Rejected)
|
||||
if (task.TaskStatus != OnboardingTaskStatus.PendingFill
|
||||
&& task.TaskStatus != OnboardingTaskStatus.Rejected)
|
||||
{
|
||||
throw Oops.Bah("当前任务不可编辑");
|
||||
}
|
||||
|
||||
var old = await _db.Queryable<ObTaskEmployeeInfo>().FirstAsync(it => it.TaskId == task.Id);
|
||||
var old = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
.FirstAsync(item => item.TaskId == task.Id);
|
||||
var entity = input.Adapt<ObTaskEmployeeInfo>();
|
||||
entity.TaskId = task.Id;
|
||||
entity.IsDelete = false;
|
||||
|
||||
if (old == null)
|
||||
{
|
||||
entity.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
entity.Status = CommonStatusConst.ENABLE;
|
||||
entity.CreateTime = DateTime.Now;
|
||||
entity.CreateUserId = null;
|
||||
entity.CreateUser = null;
|
||||
await _db.Insertable(entity).ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Id = old.Id;
|
||||
entity.Status = old.Status;
|
||||
entity.CreateTime = old.CreateTime;
|
||||
entity.CreateUserId = old.CreateUserId;
|
||||
entity.CreateUser = old.CreateUser;
|
||||
entity.ExtJson = old.ExtJson;
|
||||
entity.IsDelete = old.IsDelete;
|
||||
entity.UpdateTime = DateTime.Now;
|
||||
entity.UpdateUserId = null;
|
||||
entity.UpdateUser = null;
|
||||
await _db.Updateable(entity).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
await _snapshotService.Build(task.Id, entity);
|
||||
await _snapshotService.Build(task.Id, entity, invalidateGeneratedPreview: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验任务归属后返回合同模板源文件 Id。
|
||||
/// 根据员工当前已填写信息生成合同预览,并返回已替换变量的 DOCX 文件 Id。
|
||||
/// </summary>
|
||||
public async Task<long> GetContractSourceFileId(OnboardingPublicContractSourceInput input)
|
||||
{
|
||||
var link = await GetActiveLink(input.Token);
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
.FirstAsync(x => x.Id == input.TaskContractId && x.TaskId == link.TaskId)
|
||||
.FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == link.TaskId)
|
||||
?? throw Oops.Bah("合同不存在");
|
||||
var version = await _db.Queryable<ObContractTemplateVersion>().InSingleAsync(contract.TemplateVersionId)
|
||||
?? throw Oops.Bah("合同模板版本不存在");
|
||||
|
||||
return version.SourceDocxFileId;
|
||||
}
|
||||
await _snapshotService.Build(link.TaskId);
|
||||
var fileId = await _contractDocumentService.Generate(new OnboardingGenerateContractInput
|
||||
{
|
||||
TaskContractId = contract.Id
|
||||
});
|
||||
|
||||
private async Task<List<ObFormRequirement>> GetEffectiveFormRequirements(ObOnboardingTask task)
|
||||
{
|
||||
var requirements = await _db.Queryable<ObFormRequirement>()
|
||||
.Where(x => x.CompanyId == task.CompanyId && (x.PositionId == null || x.PositionId == task.PositionId))
|
||||
.ToListAsync();
|
||||
|
||||
// 同一字段同时存在公司级和岗位级配置时,岗位配置覆盖公司默认配置。
|
||||
return requirements
|
||||
.GroupBy(x => x.FieldId)
|
||||
.Select(group => group.FirstOrDefault(x => x.PositionId == task.PositionId) ?? group.First())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<ObMaterialRequirement>> GetEffectiveMaterialRequirements(ObOnboardingTask task)
|
||||
{
|
||||
var requirements = await _db.Queryable<ObMaterialRequirement>()
|
||||
.Where(x => x.CompanyId == task.CompanyId && (x.PositionId == null || x.PositionId == task.PositionId))
|
||||
.ToListAsync();
|
||||
|
||||
// 同一材料同时存在公司级和岗位级配置时,岗位配置覆盖公司默认配置。
|
||||
return requirements
|
||||
.GroupBy(x => x.MaterialTypeId)
|
||||
.Select(group => group.FirstOrDefault(x => x.PositionId == task.PositionId) ?? group.First())
|
||||
.ToList();
|
||||
contract.ReadAt ??= DateTime.Now;
|
||||
if (contract.ContractStatus == TaskContractStatus.PendingGenerate)
|
||||
contract.ContractStatus = TaskContractStatus.PendingConfirm;
|
||||
await _db.Updateable(contract).ExecuteCommandAsync();
|
||||
return fileId;
|
||||
}
|
||||
|
||||
private async Task<ObTaskLink> GetActiveLink(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
throw Oops.Bah("入职链接 Token 不能为空");
|
||||
|
||||
var hash = Convert.ToHexString(
|
||||
global::System.Security.Cryptography.SHA256.HashData(
|
||||
global::System.Text.Encoding.UTF8.GetBytes(token)));
|
||||
var link = await _db.Queryable<ObTaskLink>().FirstAsync(it => it.TokenHash == hash)
|
||||
var link = await _db.Queryable<ObTaskLink>().FirstAsync(item => item.TokenHash == hash)
|
||||
?? throw Oops.Bah("链接不存在");
|
||||
|
||||
if (link.LinkStatus != OnboardingLinkStatus.Active || link.ExpiresAt <= DateTime.Now)
|
||||
|
||||
@@ -16,6 +16,16 @@ public class OnboardingTaskCreateInput
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
|
||||
public long PositionId { get; set; }
|
||||
/// <summary>
|
||||
/// 本次任务使用的入职模板 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "入职模板Id不能为空")]
|
||||
public long OnboardingTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时由人事预先填写的合同变量。
|
||||
/// </summary>
|
||||
public List<OnboardingInitialContractVariableInput> ContractVariables { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时指定的审核人用户 Id。
|
||||
@@ -33,6 +43,28 @@ public class OnboardingTaskCreateInput
|
||||
public string Remark { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时填写的单个合同变量。
|
||||
/// </summary>
|
||||
public class OnboardingInitialContractVariableInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 变量所属合同模板版本 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "合同模板版本Id不能为空")]
|
||||
public long ContractTemplateVersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DOCX 中的完整变量编码,例如 hr.MonthlySalary。
|
||||
/// </summary>
|
||||
public string VariableCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 人事录入的变量值。
|
||||
/// </summary>
|
||||
public string VariableValue { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入职任务创建结果。
|
||||
/// </summary>
|
||||
|
||||
@@ -96,6 +96,47 @@ public class OnboardingPublicFormFieldOutput
|
||||
public bool Required { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工端需要填写的合同员工变量。
|
||||
/// </summary>
|
||||
public class OnboardingPublicContractVariableOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 合同变量编码。
|
||||
/// </summary>
|
||||
public string VariableCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同变量显示名称。
|
||||
/// </summary>
|
||||
public string VariableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 变量来源类型。
|
||||
/// </summary>
|
||||
public string VariableType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 变量数据类型。
|
||||
/// </summary>
|
||||
public string DataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 变量默认值。
|
||||
/// </summary>
|
||||
public string DefaultValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 选择型变量的可选项 JSON。
|
||||
/// </summary>
|
||||
public string OptionsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工端材料上传要求。
|
||||
/// </summary>
|
||||
@@ -166,4 +207,9 @@ public class OnboardingTaskPublicDetailOutput
|
||||
/// 候选人已上传材料列表。
|
||||
/// </summary>
|
||||
public List<ObTaskMaterial> Materials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同中由员工填写的扩展变量。
|
||||
/// </summary>
|
||||
public List<OnboardingPublicContractVariableOutput> ContractVariables { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职任务创建业务能力。
|
||||
/// 实现基于入职模板创建任务及冻结配置快照的业务能力。
|
||||
/// </summary>
|
||||
public class OnboardingTaskService : IOnboardingTaskService
|
||||
{
|
||||
@@ -19,42 +19,63 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验公司、岗位和合同范围后创建入职任务、链接及任务合同。
|
||||
/// 校验公司、岗位和入职模板后创建任务,并冻结合同及材料要求。
|
||||
/// </summary>
|
||||
public async Task<OnboardingTaskCreatedOutput> Create(OnboardingTaskCreateInput input)
|
||||
{
|
||||
var companyEnabled = await _db.Queryable<ObCompany>()
|
||||
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
|
||||
if (!companyEnabled)
|
||||
{
|
||||
throw Oops.Bah("公司不存在或未启用");
|
||||
}
|
||||
|
||||
var positionEnabled = await _db.Queryable<ObPosition>()
|
||||
.AnyAsync(item => item.Id == input.PositionId
|
||||
&& item.CompanyId == input.CompanyId
|
||||
&& item.Status == "enable");
|
||||
if (!positionEnabled)
|
||||
{
|
||||
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
|
||||
}
|
||||
|
||||
var scopes = await _db.Queryable<ObContractScope>()
|
||||
.Where(item => item.CompanyId == input.CompanyId
|
||||
&& (item.PositionId == null || item.PositionId == input.PositionId))
|
||||
var onboardingTemplate = await _db.Queryable<ObOnboardingTemplate>()
|
||||
.FirstAsync(item => item.Id == input.OnboardingTemplateId
|
||||
&& item.CompanyId == input.CompanyId
|
||||
&& item.Status == "enable")
|
||||
?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司");
|
||||
var templateSupportsPosition = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||
.AnyAsync(item => item.OnboardingTemplateId == onboardingTemplate.Id
|
||||
&& item.PositionId == input.PositionId);
|
||||
if (!templateSupportsPosition)
|
||||
throw Oops.Bah("所选入职模板不适用于该岗位");
|
||||
|
||||
var contractConfigs = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
if (contractConfigs.Count == 0)
|
||||
throw Oops.Bah("入职模板尚未配置合同文件");
|
||||
|
||||
var materialConfigs = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
|
||||
// 同一版本同时存在“全公司”和“指定岗位”范围时,岗位专属配置优先,避免同一合同生成两次。
|
||||
scopes = scopes
|
||||
.GroupBy(item => item.TemplateVersionId)
|
||||
.Select(group => group.FirstOrDefault(item => item.PositionId == input.PositionId) ?? group.First())
|
||||
.ToList();
|
||||
if (scopes.Count == 0)
|
||||
{
|
||||
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();
|
||||
var versionIds = contractConfigs.Select(item => item.ContractTemplateVersionId).Distinct().ToList();
|
||||
var enabledVersionCount = await _db.Queryable<ObContractTemplateVersion>()
|
||||
.CountAsync(item => versionIds.Contains(item.Id)
|
||||
&& item.VersionStatus == ContractTemplateVersionStatus.Enabled);
|
||||
if (enabledVersionCount != versionIds.Count)
|
||||
throw Oops.Bah("入职模板包含未启用的合同版本,请先调整模板配置");
|
||||
|
||||
var materialIds = materialConfigs.Select(item => item.MaterialTypeId).Distinct().ToList();
|
||||
var materialTypes = materialIds.Count == 0
|
||||
? new List<ObMaterialType>()
|
||||
: await _db.Queryable<ObMaterialType>().Where(item => materialIds.Contains(item.Id)).ToListAsync();
|
||||
if (materialTypes.Count != materialIds.Count)
|
||||
throw Oops.Bah("入职模板包含不存在的材料类型,请先调整模板配置");
|
||||
|
||||
var taskId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
var expiryConfig = await _configService.GetByConfigKey(
|
||||
CateGoryConst.CONFIG_BIZ_DEFINE,
|
||||
"onboard.link.expireHours");
|
||||
@@ -65,6 +86,7 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
throw Oops.Bah("请在系统配置中设置 onboard.link.expireHours(正整数小时)");
|
||||
}
|
||||
|
||||
var taskId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
var expiresAt = DateTime.Now.AddHours(expiryHours);
|
||||
var token = Guid.NewGuid().ToString("N");
|
||||
var task = new ObOnboardingTask
|
||||
@@ -73,13 +95,15 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
TaskNo = $"OB{DateTime.Now:yyyyMMddHHmmss}{taskId.ToString()[^4..]}",
|
||||
CompanyId = input.CompanyId,
|
||||
PositionId = input.PositionId,
|
||||
OnboardingTemplateId = onboardingTemplate.Id,
|
||||
AssignedReviewerUserId = input.AssignedReviewerUserId,
|
||||
LinkExpireAt = expiresAt,
|
||||
TaskStatus = OnboardingTaskStatus.PendingFill,
|
||||
Remark = input.Remark
|
||||
};
|
||||
|
||||
// 任务、链接、合同实例和变量快照属于同一创建动作,必须保证事务一致性。
|
||||
ValidateInitialContractDateRanges(input.ContractVariables);
|
||||
|
||||
await _db.Ado.BeginTranAsync();
|
||||
try
|
||||
{
|
||||
@@ -93,27 +117,56 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
ExpiresAt = expiresAt
|
||||
}).ExecuteCommandAsync();
|
||||
|
||||
var templateIds = scopes.Select(item => item.TemplateId).Distinct().ToList();
|
||||
var templates = await _db.Queryable<ObContractTemplate>()
|
||||
.Where(item => templateIds.Contains(item.Id))
|
||||
.ToListAsync();
|
||||
var contracts = scopes.Select(scope =>
|
||||
// 初始化空白的员工入职登记表
|
||||
await _db.Insertable(new ObTaskEmployeeInfo
|
||||
{
|
||||
var template = templates.First(item => item.Id == scope.TemplateId);
|
||||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||
TaskId = taskId,
|
||||
Status = CommonStatusConst.ENABLE,
|
||||
CreateTime = DateTime.Now
|
||||
}).ExecuteCommandAsync();
|
||||
|
||||
var contracts = contractConfigs.Select(config =>
|
||||
{
|
||||
var template = contractTemplates.First(item => item.Id == config.ContractTemplateId);
|
||||
return new ObTaskContract
|
||||
{
|
||||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||
TaskId = taskId,
|
||||
TemplateId = scope.TemplateId,
|
||||
TemplateVersionId = scope.TemplateVersionId,
|
||||
TemplateId = config.ContractTemplateId,
|
||||
TemplateVersionId = config.ContractTemplateVersionId,
|
||||
ContractName = template.TemplateName,
|
||||
ContractType = template.ContractType,
|
||||
Required = scope.Required
|
||||
Required = config.Required,
|
||||
PrintCopies = config.PrintCopies
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
await _db.Insertable(contracts).ExecuteCommandAsync();
|
||||
|
||||
if (materialConfigs.Count > 0)
|
||||
{
|
||||
var materialSnapshots = materialConfigs.Select(config =>
|
||||
{
|
||||
var material = materialTypes.First(item => item.Id == config.MaterialTypeId);
|
||||
return new ObTaskMaterialRequirement
|
||||
{
|
||||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||
TaskId = taskId,
|
||||
MaterialTypeId = material.Id,
|
||||
MaterialCode = material.MaterialCode,
|
||||
MaterialName = material.MaterialName,
|
||||
AllowedExtensions = material.AllowedExtensions,
|
||||
MaxFileCount = material.MaxFileCount,
|
||||
MaxFileSizeMb = material.MaxFileSizeMb,
|
||||
Required = config.Required,
|
||||
SortCode = config.SortCode
|
||||
};
|
||||
}).ToList();
|
||||
await _db.Insertable(materialSnapshots).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
await _snapshotService.Build(taskId);
|
||||
await ApplyInitialContractVariables(input.ContractVariables, contracts);
|
||||
await _db.Ado.CommitTranAsync();
|
||||
}
|
||||
catch
|
||||
@@ -130,4 +183,115 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
ExpiresAt = expiresAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验并保存任务创建时填写的人事合同变量。
|
||||
/// </summary>
|
||||
private async Task ApplyInitialContractVariables(
|
||||
List<OnboardingInitialContractVariableInput> inputs,
|
||||
List<ObTaskContract> contracts)
|
||||
{
|
||||
inputs ??= new List<OnboardingInitialContractVariableInput>();
|
||||
var versionIds = contracts.Select(item => item.TemplateVersionId).Distinct().ToList();
|
||||
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||
.Where(item => versionIds.Contains(item.TemplateVersionId)
|
||||
&& item.HrEditable
|
||||
&& (item.VariableType == "hr"
|
||||
|| item.VariableType == "contract"
|
||||
|| item.VariableType == "company"
|
||||
|| item.VariableType == ContractVariableType.Contract
|
||||
|| item.VariableType == ContractVariableType.Company
|
||||
|| item.VariableCode.StartsWith("hr.")))
|
||||
.ToListAsync();
|
||||
|
||||
var duplicateInput = inputs
|
||||
.GroupBy(item => new { item.ContractTemplateVersionId, item.VariableCode })
|
||||
.FirstOrDefault(group => group.Count() > 1);
|
||||
if (duplicateInput != null)
|
||||
throw Oops.Bah($"合同变量“{duplicateInput.Key.VariableCode}”重复提交");
|
||||
|
||||
foreach (var input in inputs)
|
||||
{
|
||||
var definitionExists = definitions.Any(item =>
|
||||
item.TemplateVersionId == input.ContractTemplateVersionId
|
||||
&& item.VariableCode == input.VariableCode);
|
||||
if (!definitionExists)
|
||||
throw Oops.Bah($"合同变量“{input.VariableCode}”不存在或不允许人事填写");
|
||||
}
|
||||
|
||||
var missingRequired = definitions.FirstOrDefault(definition =>
|
||||
{
|
||||
if (!definition.Required)
|
||||
return false;
|
||||
|
||||
var input = inputs.FirstOrDefault(item =>
|
||||
item.ContractTemplateVersionId == definition.TemplateVersionId
|
||||
&& item.VariableCode == definition.VariableCode);
|
||||
return string.IsNullOrWhiteSpace(input?.VariableValue ?? definition.DefaultValue);
|
||||
});
|
||||
if (missingRequired != null)
|
||||
throw Oops.Bah($"请填写合同变量“{missingRequired.VariableName}”");
|
||||
|
||||
if (inputs.Count == 0)
|
||||
return;
|
||||
|
||||
var contractByVersion = contracts.ToDictionary(item => item.TemplateVersionId);
|
||||
var contractIds = contracts.Select(item => item.Id).ToList();
|
||||
var rows = await _db.Queryable<ObTaskContractVariable>()
|
||||
.Where(item => contractIds.Contains(item.TaskContractId))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var input in inputs)
|
||||
{
|
||||
var contract = contractByVersion[input.ContractTemplateVersionId];
|
||||
var row = rows.FirstOrDefault(item =>
|
||||
item.TaskContractId == contract.Id && item.VariableCode == input.VariableCode)
|
||||
?? throw Oops.Bah($"合同变量快照“{input.VariableCode}”不存在");
|
||||
row.VariableValue = input.VariableValue;
|
||||
row.SourceType = ContractVariableSourceType.HrInput;
|
||||
row.IsManualOverride = true;
|
||||
}
|
||||
|
||||
var changedRows = rows.Where(item => item.IsManualOverride).ToList();
|
||||
if (changedRows.Count > 0)
|
||||
await _db.Updateable(changedRows).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验人事创建任务时填写的合同日期先后关系。
|
||||
/// </summary>
|
||||
private static void ValidateInitialContractDateRanges(
|
||||
List<OnboardingInitialContractVariableInput> inputs)
|
||||
{
|
||||
inputs ??= new List<OnboardingInitialContractVariableInput>();
|
||||
foreach (var group in inputs.GroupBy(item => item.ContractTemplateVersionId))
|
||||
{
|
||||
ValidateDateRange(group, "hr.ContractStartDate", "hr.ContractEndDate", "合同");
|
||||
ValidateDateRange(group, "hr.ProbationStartDate", "hr.ProbationEndDate", "试用期");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验同一合同版本内一组开始、结束日期。
|
||||
/// </summary>
|
||||
private static void ValidateDateRange(
|
||||
IEnumerable<OnboardingInitialContractVariableInput> inputs,
|
||||
string startCode,
|
||||
string endCode,
|
||||
string rangeName)
|
||||
{
|
||||
var startValue = inputs.FirstOrDefault(item =>
|
||||
string.Equals(item.VariableCode, startCode, StringComparison.OrdinalIgnoreCase))?.VariableValue;
|
||||
var endValue = inputs.FirstOrDefault(item =>
|
||||
string.Equals(item.VariableCode, endCode, StringComparison.OrdinalIgnoreCase))?.VariableValue;
|
||||
if (string.IsNullOrWhiteSpace(startValue) || string.IsNullOrWhiteSpace(endValue))
|
||||
return;
|
||||
|
||||
if (!DateTime.TryParse(startValue, out var startDate))
|
||||
throw Oops.Bah($"{rangeName}开始日期格式不正确");
|
||||
if (!DateTime.TryParse(endValue, out var endDate))
|
||||
throw Oops.Bah($"{rangeName}结束日期格式不正确");
|
||||
if (endDate.Date < startDate.Date)
|
||||
throw Oops.Bah($"{rangeName}结束日期不能早于开始日期");
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -141,6 +141,15 @@ public class OnboardingTaskContractVariableService : IOnboardingTaskContractVari
|
||||
{
|
||||
await _db.Updateable(rows)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 人事参数变化后,原合同文件和员工确认必须失效,避免员工确认旧内容。
|
||||
contract.GeneratedDocxFileId = null;
|
||||
contract.GeneratedPdfFileId = null;
|
||||
contract.GeneratedAt = null;
|
||||
contract.ReadAt = null;
|
||||
contract.ConfirmedAt = null;
|
||||
contract.ContractStatus = TaskContractStatus.PendingGenerate;
|
||||
await _db.Updateable(contract).ExecuteCommandAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-6
@@ -44,12 +44,7 @@ public class OnboardingTaskDetailOutput
|
||||
/// <summary>
|
||||
/// 候选人已上传材料列表。
|
||||
/// </summary>
|
||||
public List<ObTaskMaterial> Materials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 候选人动态字段值列表。
|
||||
/// </summary>
|
||||
public List<ObTaskFormValue> FormValues { get; set; }
|
||||
public List<OnboardingTaskMaterialDetailOutput> Materials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务当前有效链接信息。
|
||||
@@ -61,3 +56,19 @@ public class OnboardingTaskDetailOutput
|
||||
/// </summary>
|
||||
public List<ObTaskAudit> Audits { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台入职任务材料详情。
|
||||
/// </summary>
|
||||
public class OnboardingTaskMaterialDetailOutput : ObTaskMaterial
|
||||
{
|
||||
/// <summary>
|
||||
/// 材料原始文件名,用于下载时保留文件名称。
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料文件后缀,用于判断是否支持图片预览。
|
||||
/// </summary>
|
||||
public string FileSuffix { get; set; }
|
||||
}
|
||||
|
||||
+41
-19
@@ -33,28 +33,50 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
|
||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(input.Id)
|
||||
?? throw Oops.Bah("入职任务不存在");
|
||||
|
||||
var employeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
.FirstAsync(item => item.TaskId == task.Id);
|
||||
var contracts = await _db.Queryable<ObTaskContract>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync();
|
||||
var materials = await _db.Queryable<ObTaskMaterial>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync();
|
||||
var materialFileIds = materials
|
||||
.Select(item => item.FileId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var materialFiles = await _db.Queryable<SysFile>()
|
||||
.Where(item => materialFileIds.Contains(item.Id))
|
||||
.ToListAsync();
|
||||
var materialFileMap = materialFiles.ToDictionary(item => item.Id);
|
||||
var materialOutputs = materials.Select(material =>
|
||||
{
|
||||
var output = material.Adapt<OnboardingTaskMaterialDetailOutput>();
|
||||
if (materialFileMap.TryGetValue(material.FileId, out var file))
|
||||
{
|
||||
output.FileName = file.Name;
|
||||
output.FileSuffix = file.Suffix;
|
||||
}
|
||||
|
||||
return output;
|
||||
}).ToList();
|
||||
var link = await _db.Queryable<ObTaskLink>()
|
||||
.Where(item => item.TaskId == task.Id && item.LinkStatus == "active")
|
||||
.OrderByDescending(item => item.CreateTime)
|
||||
.FirstAsync();
|
||||
var audits = await _db.Queryable<ObTaskAudit>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.OrderByDescending(item => item.AuditedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return new OnboardingTaskDetailOutput
|
||||
{
|
||||
Task = task,
|
||||
EmployeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
.FirstAsync(item => item.TaskId == task.Id),
|
||||
Contracts = await _db.Queryable<ObTaskContract>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
Materials = await _db.Queryable<ObTaskMaterial>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
FormValues = await _db.Queryable<ObTaskFormValue>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
Link = await _db.Queryable<ObTaskLink>()
|
||||
.Where(item => item.TaskId == task.Id && item.LinkStatus == "active")
|
||||
.OrderByDescending(item => item.CreateTime)
|
||||
.FirstAsync(),
|
||||
Audits = await _db.Queryable<ObTaskAudit>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.OrderByDescending(item => item.AuditedAt)
|
||||
.ToListAsync()
|
||||
EmployeeInfo = employeeInfo,
|
||||
Contracts = contracts,
|
||||
Materials = materialOutputs,
|
||||
Link = link,
|
||||
Audits = audits
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
public interface IOnboardingVariableDefinitionService : ITransient
|
||||
{
|
||||
Task<SqlSugarPagedList<ObVariableDefinition>> Page(OnboardingVariableDefinitionPageInput input);
|
||||
Task<List<ObVariableDefinition>> ListAll();
|
||||
Task Add(OnboardingVariableDefinitionAddInput input);
|
||||
Task Edit(OnboardingVariableDefinitionEditInput input);
|
||||
Task Delete(BaseIdListInput input);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
public class OnboardingVariableDefinitionPageInput : BasePageInput
|
||||
{
|
||||
public string Keyword { get; set; }
|
||||
public string VariableCategory { get; set; }
|
||||
}
|
||||
|
||||
public class OnboardingVariableDefinitionAddInput
|
||||
{
|
||||
[Required(ErrorMessage = "变量编码不能为空")]
|
||||
public string VariableCode { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "变量名称不能为空")]
|
||||
public string VariableName { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "变量分类不能为空")]
|
||||
public string VariableCategory { get; set; }
|
||||
|
||||
public string DataType { get; set; } = "string";
|
||||
public string DefaultValue { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public int SortCode { get; set; } = 100;
|
||||
public string Remark { get; set; }
|
||||
}
|
||||
|
||||
public class OnboardingVariableDefinitionEditInput : OnboardingVariableDefinitionAddInput
|
||||
{
|
||||
[IdNotNull(ErrorMessage = "Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
public class OnboardingVariableDefinitionService : IOnboardingVariableDefinitionService
|
||||
{
|
||||
private readonly SqlSugarScope _db;
|
||||
private static bool _initialized = false;
|
||||
private static readonly object _lock = new();
|
||||
|
||||
public OnboardingVariableDefinitionService()
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
}
|
||||
|
||||
public async Task<SqlSugarPagedList<ObVariableDefinition>> Page(OnboardingVariableDefinitionPageInput input)
|
||||
{
|
||||
return await _db.Queryable<ObVariableDefinition>()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), it => it.VariableCode.Contains(input.Keyword) || it.VariableName.Contains(input.Keyword))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.VariableCategory), it => it.VariableCategory == input.VariableCategory)
|
||||
.OrderBy(it => it.SortCode)
|
||||
.OrderBy(it => it.Id)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
}
|
||||
|
||||
public async Task<List<ObVariableDefinition>> ListAll()
|
||||
{
|
||||
return await _db.Queryable<ObVariableDefinition>()
|
||||
.OrderBy(it => it.SortCode)
|
||||
.OrderBy(it => it.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task Add(OnboardingVariableDefinitionAddInput input)
|
||||
{
|
||||
var code = input.VariableCode.Trim();
|
||||
var exists = await _db.Queryable<ObVariableDefinition>().AnyAsync(it => it.VariableCode == code);
|
||||
if (exists)
|
||||
throw Oops.Bah($"变量编码“{code}”已存在");
|
||||
|
||||
var entity = input.Adapt<ObVariableDefinition>();
|
||||
entity.VariableCode = code;
|
||||
entity.IsBuiltIn = false;
|
||||
await _db.Insertable(entity).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
public async Task Edit(OnboardingVariableDefinitionEditInput input)
|
||||
{
|
||||
var entity = await _db.Queryable<ObVariableDefinition>().InSingleAsync(input.Id)
|
||||
?? throw Oops.Bah("变量不存在");
|
||||
|
||||
entity.VariableName = input.VariableName;
|
||||
entity.DataType = input.DataType ?? "string";
|
||||
entity.DefaultValue = input.DefaultValue;
|
||||
entity.Required = input.Required;
|
||||
entity.SortCode = input.SortCode;
|
||||
entity.Remark = input.Remark;
|
||||
if (!entity.IsBuiltIn)
|
||||
{
|
||||
entity.VariableCategory = input.VariableCategory;
|
||||
}
|
||||
|
||||
await _db.Updateable(entity).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
public async Task Delete(BaseIdListInput input)
|
||||
{
|
||||
if (input.Ids.Count == 0) return;
|
||||
|
||||
var hasBuiltIn = await _db.Queryable<ObVariableDefinition>()
|
||||
.Where(it => input.Ids.Contains(it.Id) && it.IsBuiltIn)
|
||||
.AnyAsync();
|
||||
if (hasBuiltIn)
|
||||
throw Oops.Bah("系统内置的标准入职登记表变量不能删除");
|
||||
|
||||
await _db.Deleteable<ObVariableDefinition>()
|
||||
.Where(it => input.Ids.Contains(it.Id))
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
|
||||
// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
|
||||
//
|
||||
// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
|
||||
// 1.请不要删除和修改根目录下的LICENSE文件。
|
||||
@@ -68,6 +68,11 @@ public class SysDictConst
|
||||
/// </summary>
|
||||
public const string FILE_ENGINE_MINIO = "MINIO";
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云 OSS,当前仅供入职业务文件使用。
|
||||
/// </summary>
|
||||
public const string FILE_ENGINE_ALIYUN_OSS = "ALIYUN_OSS";
|
||||
|
||||
#endregion 上传文件引擎
|
||||
|
||||
#region 系统字典分类
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace SimpleAdmin.System;
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云 OSS 及文档预览服务配置。
|
||||
/// </summary>
|
||||
public class OssSettingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// OSS 访问密钥 Id。
|
||||
/// </summary>
|
||||
public string AccessKeyID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OSS 访问密钥。
|
||||
/// </summary>
|
||||
public string AccessKeySecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于换取临时凭证的 RAM 角色 ARN。
|
||||
/// </summary>
|
||||
public string RoleArn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// STS 角色会话名称。
|
||||
/// </summary>
|
||||
public string RoleSessionName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// STS 服务端点。
|
||||
/// </summary>
|
||||
public string StsEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OSS Bucket 名称。
|
||||
/// </summary>
|
||||
public string Bucket { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OSS 服务端点。
|
||||
/// </summary>
|
||||
public string EndPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IMM 服务端点。
|
||||
/// </summary>
|
||||
public string IMMEndpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IMM 项目名称。
|
||||
/// </summary>
|
||||
public string IMMProject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OSS 自定义访问域名。
|
||||
/// </summary>
|
||||
public string OSSUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云地域 Id。
|
||||
/// </summary>
|
||||
public string RegionId { get; set; }
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
// 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为。
|
||||
// 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关。
|
||||
|
||||
using Aliyun.OSS;
|
||||
|
||||
namespace SimpleAdmin.System;
|
||||
|
||||
/// <summary>
|
||||
@@ -95,6 +97,8 @@ public class FileService : DbRepository<SysFile>, IFileService
|
||||
return GetFileStreamResult(devFile.StoragePath, devFile.Name);
|
||||
if (devFile.Engine == SysDictConst.FILE_ENGINE_MINIO)
|
||||
return await GetFileStreamResultFromMinio(devFile.ObjName, devFile.Name);
|
||||
if (devFile.Engine == SysDictConst.FILE_ENGINE_ALIYUN_OSS)
|
||||
return await GetFileStreamResultFromAliyunOss(devFile);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
@@ -142,6 +146,9 @@ public class FileService : DbRepository<SysFile>, IFileService
|
||||
}
|
||||
break;
|
||||
|
||||
case SysDictConst.FILE_ENGINE_ALIYUN_OSS:
|
||||
(bucketName, objName, storageUrl) = await StorageAliyunOss(objectId, file);
|
||||
break;
|
||||
default:
|
||||
|
||||
throw Oops.Bah("不支持的文件引擎");
|
||||
@@ -154,13 +161,13 @@ public class FileService : DbRepository<SysFile>, IFileService
|
||||
Engine = engine,
|
||||
Bucket = bucketName,
|
||||
Name = file.FileName,
|
||||
Suffix = fileSuffix.Split(".")[1],
|
||||
Suffix = fileSuffix.TrimStart('.'),
|
||||
ObjName = objName,
|
||||
SizeKb = fileSizeKb,
|
||||
SizeInfo = GetSizeInfo(fileSizeKb),
|
||||
StoragePath = storageUrl
|
||||
};
|
||||
if (engine != CateGoryConst.CONFIG_FILE_LOCAL)//如果不是本地,设置下载地址
|
||||
if (engine != SysDictConst.FILE_ENGINE_LOCAL)//如果不是本地,设置下载地址
|
||||
{
|
||||
devFile.DownloadPath = storageUrl;
|
||||
}
|
||||
@@ -236,6 +243,111 @@ public class FileService : DbRepository<SysFile>, IFileService
|
||||
return (fileObjectName, downloadUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将入职业务文件存储到阿里云 OSS。
|
||||
/// </summary>
|
||||
private async Task<(string bucketName, string objName, string downloadUrl)> StorageAliyunOss(
|
||||
long fileId,
|
||||
IFormFile file)
|
||||
{
|
||||
var setting = GetOssSetting();
|
||||
var suffix = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
var objectName = $"onboarding/{DateTime.Now:yyyy/MM/dd}/{fileId}{suffix}";
|
||||
var metadata = new ObjectMetadata();
|
||||
if (!string.IsNullOrWhiteSpace(file.ContentType))
|
||||
metadata.ContentType = file.ContentType;
|
||||
|
||||
await using var stream = file.OpenReadStream();
|
||||
var client = CreateOssClient(setting);
|
||||
client.PutObject(setting.Bucket, objectName, stream, metadata);
|
||||
|
||||
var downloadUrl = string.IsNullOrWhiteSpace(setting.OSSUrl)
|
||||
? $"https://{setting.Bucket}.{setting.EndPoint.Trim().TrimEnd('/')}/{objectName}"
|
||||
: $"{setting.OSSUrl.Trim().TrimEnd('/')}/{objectName}";
|
||||
|
||||
return (setting.Bucket, objectName, downloadUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从阿里云 OSS 读取业务文件并通过统一下载接口返回。
|
||||
/// </summary>
|
||||
private async Task<FileStreamResult> GetFileStreamResultFromAliyunOss(SysFile file)
|
||||
{
|
||||
var setting = GetOssSetting();
|
||||
var client = CreateOssClient(setting);
|
||||
var ossObject = client.GetObject(file.Bucket, file.ObjName);
|
||||
var memory = new MemoryStream();
|
||||
await using (ossObject.Content)
|
||||
{
|
||||
await ossObject.Content.CopyToAsync(memory);
|
||||
}
|
||||
|
||||
memory.Position = 0;
|
||||
var fileName = HttpUtility.UrlEncode(file.Name, Encoding.GetEncoding("UTF-8"));
|
||||
return new FileStreamResult(memory, "application/octet-stream")
|
||||
{
|
||||
FileDownloadName = fileName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取并校验阿里云 OSS 配置。
|
||||
/// </summary>
|
||||
private static OssSettingOptions GetOssSetting()
|
||||
{
|
||||
var setting = App.GetConfig<OssSettingOptions>("OssSetting", true);
|
||||
var configurationIncomplete = setting == null
|
||||
|| string.IsNullOrWhiteSpace(setting.AccessKeyID)
|
||||
|| string.IsNullOrWhiteSpace(setting.AccessKeySecret)
|
||||
|| string.IsNullOrWhiteSpace(setting.Bucket)
|
||||
|| string.IsNullOrWhiteSpace(setting.EndPoint);
|
||||
if (configurationIncomplete)
|
||||
throw Oops.Bah("阿里云 OSS 配置不完整");
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 STS 临时凭证创建阿里云 OSS 客户端。
|
||||
/// </summary>
|
||||
private static OssClient CreateOssClient(OssSettingOptions setting)
|
||||
{
|
||||
var endpoint = setting.EndPoint.Trim();
|
||||
if (!endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
endpoint = "https://" + endpoint;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(setting.RoleArn))
|
||||
return new OssClient(endpoint, setting.AccessKeyID, setting.AccessKeySecret);
|
||||
|
||||
var stsConfig = new AlibabaCloud.OpenApiClient.Models.Config
|
||||
{
|
||||
AccessKeyId = setting.AccessKeyID,
|
||||
AccessKeySecret = setting.AccessKeySecret,
|
||||
Endpoint = setting.StsEndpoint
|
||||
};
|
||||
var stsClient = new AlibabaCloud.SDK.Sts20150401.Client(stsConfig);
|
||||
var request = new AlibabaCloud.SDK.Sts20150401.Models.AssumeRoleRequest
|
||||
{
|
||||
RoleArn = setting.RoleArn,
|
||||
RoleSessionName = string.IsNullOrWhiteSpace(setting.RoleSessionName)
|
||||
? "onboardx-server"
|
||||
: setting.RoleSessionName,
|
||||
DurationSeconds = 3600
|
||||
};
|
||||
var credentials = stsClient.AssumeRole(request).Body.Credentials;
|
||||
if (credentials == null)
|
||||
throw Oops.Bah("获取阿里云 OSS 临时凭证失败");
|
||||
|
||||
return new OssClient(
|
||||
endpoint,
|
||||
credentials.AccessKeyId,
|
||||
credentials.AccessKeySecret,
|
||||
credentials.SecurityToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件大小
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AlibabaCloud.SDK.Sts20150401" Version="1.2.0"/>
|
||||
<PackageReference Include="Aliyun.OSS.SDK.NetCore" Version="2.14.0"/>
|
||||
<PackageReference Include="IP2Region.Net" Version="2.0.2"/>
|
||||
<PackageReference Include="UAParser" Version="3.1.47"/>
|
||||
<PackageReference Include="Magicodes.IE.Excel" Version="2.7.5.1"/>
|
||||
|
||||
@@ -506,6 +506,11 @@
|
||||
MINIO
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:SimpleAdmin.System.SysDictConst.FILE_ENGINE_ALIYUN_OSS">
|
||||
<summary>
|
||||
阿里云 OSS,当前仅供入职业务文件使用。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:SimpleAdmin.System.SysDictConst.DICT_CATEGORY_FRM">
|
||||
<summary>
|
||||
系统
|
||||
@@ -2670,6 +2675,66 @@
|
||||
对象映射
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.System.OssSettingOptions">
|
||||
<summary>
|
||||
阿里云 OSS 及文档预览服务配置。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.AccessKeyID">
|
||||
<summary>
|
||||
OSS 访问密钥 Id。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.AccessKeySecret">
|
||||
<summary>
|
||||
OSS 访问密钥。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.RoleArn">
|
||||
<summary>
|
||||
用于换取临时凭证的 RAM 角色 ARN。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.RoleSessionName">
|
||||
<summary>
|
||||
STS 角色会话名称。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.StsEndpoint">
|
||||
<summary>
|
||||
STS 服务端点。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.Bucket">
|
||||
<summary>
|
||||
OSS Bucket 名称。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.EndPoint">
|
||||
<summary>
|
||||
OSS 服务端点。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.IMMEndpoint">
|
||||
<summary>
|
||||
IMM 服务端点。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.IMMProject">
|
||||
<summary>
|
||||
IMM 项目名称。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.OSSUrl">
|
||||
<summary>
|
||||
OSS 自定义访问域名。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:SimpleAdmin.System.OssSettingOptions.RegionId">
|
||||
<summary>
|
||||
阿里云地域 Id。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.System.SystemSettingsOptions">
|
||||
<summary>
|
||||
系统配置
|
||||
@@ -3588,6 +3653,26 @@
|
||||
<param name="file"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.System.FileService.StorageAliyunOss(System.Int64,Microsoft.AspNetCore.Http.IFormFile)">
|
||||
<summary>
|
||||
将入职业务文件存储到阿里云 OSS。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.System.FileService.GetFileStreamResultFromAliyunOss(SimpleAdmin.System.SysFile)">
|
||||
<summary>
|
||||
从阿里云 OSS 读取业务文件并通过统一下载接口返回。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.System.FileService.GetOssSetting">
|
||||
<summary>
|
||||
读取并校验阿里云 OSS 配置。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.System.FileService.CreateOssClient(SimpleAdmin.System.OssSettingOptions)">
|
||||
<summary>
|
||||
通过 STS 临时凭证创建阿里云 OSS 客户端。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.System.FileService.GetSizeInfo(System.Int64)">
|
||||
<summary>
|
||||
获取文件大小
|
||||
|
||||
@@ -24,5 +24,18 @@
|
||||
"InitSeedData": false, //是否初始化种子数据
|
||||
"SuperAdminViewAllData": true, //是否超级管理员可以查看所有数据
|
||||
"UseMessageCenter": false //是否启用通知中心
|
||||
}
|
||||
},
|
||||
"OssSetting": {
|
||||
"AccessKeyID": "LTAI5t9VQvLERXtMHC9BK31f",
|
||||
"AccessKeySecret": "YomhludvaOmLZo1K0vUDoeBhWRjxWL",
|
||||
"RoleArn": "acs:ram::1436008104961462:role/onboardx-sts",
|
||||
"RoleSessionName": "onboardx-server",
|
||||
"StsEndpoint": "sts.cn-beijing.aliyuncs.com",
|
||||
"Bucket": "onboardx",
|
||||
"EndPoint": "oss-cn-beijing.aliyuncs.com",
|
||||
"IMMEndpoint": "",
|
||||
"IMMProject": "",
|
||||
"OSSUrl": "https://onboardx.oss-cn-beijing.aliyuncs.com",
|
||||
"RegionId": "oss-cn-beijing"
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,18 @@
|
||||
"InitSeedData": false, //是否初始化种子数据
|
||||
"SuperAdminViewAllData": true, //是否超级管理员可以查看所有数据
|
||||
"UseMessageCenter": false //是否启用通知中心
|
||||
}
|
||||
},
|
||||
"OssSetting": {
|
||||
"AccessKeyID": "LTAI5t9VQvLERXtMHC9BK31f",
|
||||
"AccessKeySecret": "YomhludvaOmLZo1K0vUDoeBhWRjxWL",
|
||||
"RoleArn": "acs:ram::1436008104961462:role/onboardx-sts",
|
||||
"RoleSessionName": "onboardx-server",
|
||||
"StsEndpoint": "sts.cn-beijing.aliyuncs.com",
|
||||
"Bucket": "onboardx",
|
||||
"EndPoint": "oss-cn-beijing.aliyuncs.com",
|
||||
"IMMEndpoint": "",
|
||||
"IMMProject": "",
|
||||
"OSSUrl": "https://onboardx.oss-cn-beijing.aliyuncs.com",
|
||||
"RegionId": "oss-cn-beijing"
|
||||
}
|
||||
}
|
||||
+33
-56
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Web.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 提供合同模板、版本、变量和适用范围相关 HTTP 接口。
|
||||
/// 提供合同模板、DOCX 版本和自动识别变量配置接口。
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("Application", Tag = "入职合同模板")]
|
||||
[Route("biz/onboarding/contract-template")]
|
||||
@@ -9,10 +9,32 @@ namespace SimpleAdmin.Web.Core;
|
||||
public class OnboardingContractTemplateController
|
||||
{
|
||||
private readonly IOnboardingContractTemplateService _service;
|
||||
private readonly IFileService _fileService;
|
||||
|
||||
public OnboardingContractTemplateController(IOnboardingContractTemplateService service)
|
||||
public OnboardingContractTemplateController(
|
||||
IOnboardingContractTemplateService service,
|
||||
IFileService fileService)
|
||||
{
|
||||
_service = service;
|
||||
_fileService = fileService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将入职合同 DOCX 模板上传到阿里云 OSS。
|
||||
/// </summary>
|
||||
[HttpPost("file/upload-docx")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[RequestSizeLimit(20 * 1024 * 1024)]
|
||||
public async Task<long> UploadDocx([FromForm] IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
throw Oops.Bah("请选择需要上传的 DOCX 文件");
|
||||
|
||||
var suffix = Path.GetExtension(file.FileName);
|
||||
if (!string.Equals(suffix, ".docx", StringComparison.OrdinalIgnoreCase))
|
||||
throw Oops.Bah("合同模板仅支持 DOCX 文件");
|
||||
|
||||
return await _fileService.UploadFile(SysDictConst.FILE_ENGINE_ALIYUN_OSS, file);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,6 +109,15 @@ public class OnboardingContractTemplateController
|
||||
await _service.SetVersionStatus(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被入职模板或任务使用的合同模板版本。
|
||||
/// </summary>
|
||||
[HttpPost("version/delete")]
|
||||
public async Task DeleteVersion([FromBody] BaseIdInput input)
|
||||
{
|
||||
await _service.DeleteVersion(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询模板变量。
|
||||
/// </summary>
|
||||
@@ -96,15 +127,6 @@ public class OnboardingContractTemplateController
|
||||
return await _service.VariablePage(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增模板变量。
|
||||
/// </summary>
|
||||
[HttpPost("variable/add")]
|
||||
public async Task AddVariable([FromBody] OnboardingTemplateVariableAddInput input)
|
||||
{
|
||||
await _service.AddVariable(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑模板变量。
|
||||
/// </summary>
|
||||
@@ -113,49 +135,4 @@ public class OnboardingContractTemplateController
|
||||
{
|
||||
await _service.EditVariable(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除模板变量。
|
||||
/// </summary>
|
||||
[HttpPost("variable/delete")]
|
||||
public async Task DeleteVariable([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.DeleteVariable(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询合同适用范围。
|
||||
/// </summary>
|
||||
[HttpGet("scope/page")]
|
||||
public async Task<dynamic> ScopePage([FromQuery] OnboardingContractScopePageInput input)
|
||||
{
|
||||
return await _service.ScopePage(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增合同适用范围。
|
||||
/// </summary>
|
||||
[HttpPost("scope/add")]
|
||||
public async Task AddScope([FromBody] OnboardingContractScopeAddInput input)
|
||||
{
|
||||
await _service.AddScope(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑合同适用范围。
|
||||
/// </summary>
|
||||
[HttpPost("scope/edit")]
|
||||
public async Task EditScope([FromBody] OnboardingContractScopeEditInput input)
|
||||
{
|
||||
await _service.EditScope(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除合同适用范围。
|
||||
/// </summary>
|
||||
[HttpPost("scope/delete")]
|
||||
public async Task DeleteScope([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.DeleteScope(input);
|
||||
}
|
||||
}
|
||||
+1
-55
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Web.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 提供入职字段与材料要求配置相关 HTTP 接口。
|
||||
/// 提供员工动态补充字段与上传材料类型配置接口。
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("Application", Tag = "入职填写与材料配置")]
|
||||
[Route("biz/onboarding/entry-config")]
|
||||
@@ -68,58 +68,4 @@ public class OnboardingEntryConfigController
|
||||
{
|
||||
await _service.DeleteMaterialTypes(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询公司岗位动态字段要求。
|
||||
/// </summary>
|
||||
[HttpGet("form-requirement/list")]
|
||||
public async Task<dynamic> ListFormRequirements([FromQuery] long companyId)
|
||||
{
|
||||
return await _service.ListFormRequirements(companyId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存公司岗位动态字段要求。
|
||||
/// </summary>
|
||||
[HttpPost("form-requirement/save")]
|
||||
public async Task SaveFormRequirement([FromBody] OnboardingFormRequirementSaveInput input)
|
||||
{
|
||||
await _service.SaveFormRequirement(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除动态字段要求。
|
||||
/// </summary>
|
||||
[HttpPost("form-requirement/delete")]
|
||||
public async Task DeleteFormRequirements([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.DeleteFormRequirements(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询公司岗位材料要求。
|
||||
/// </summary>
|
||||
[HttpGet("material-requirement/list")]
|
||||
public async Task<dynamic> ListMaterialRequirements([FromQuery] long companyId)
|
||||
{
|
||||
return await _service.ListMaterialRequirements(companyId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存公司岗位材料要求。
|
||||
/// </summary>
|
||||
[HttpPost("material-requirement/save")]
|
||||
public async Task SaveMaterialRequirement([FromBody] OnboardingMaterialRequirementSaveInput input)
|
||||
{
|
||||
await _service.SaveMaterialRequirement(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除材料要求。
|
||||
/// </summary>
|
||||
[HttpPost("material-requirement/delete")]
|
||||
public async Task DeleteMaterialRequirements([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.DeleteMaterialRequirements(input);
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -35,6 +35,16 @@ public class OnboardingPublicSubmissionController
|
||||
await _service.SaveMaterial(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除当前入职任务中的一条已上传材料。
|
||||
/// </summary>
|
||||
[HttpPost("delete-material")]
|
||||
public async Task DeleteMaterial(
|
||||
[FromBody] OnboardingDeleteMaterialInput input)
|
||||
{
|
||||
await _service.DeleteMaterial(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确认已阅读指定合同。
|
||||
/// </summary>
|
||||
@@ -59,11 +69,14 @@ public class OnboardingPublicSubmissionController
|
||||
[HttpPost("upload-material")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[RequestSizeLimit(20 * 1024 * 1024)]
|
||||
public async Task UploadMaterial([FromForm] string token, [FromForm] long materialTypeId, IFormFile file)
|
||||
public async Task UploadMaterial(
|
||||
[FromQuery] string token,
|
||||
[FromQuery] long materialTypeId,
|
||||
[FromForm] IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("请选择需要上传的文件");
|
||||
throw Oops.Bah("请选择需要上传的文件");
|
||||
}
|
||||
|
||||
await _service.ValidateMaterialUpload(new OnboardingMaterialUploadValidationInput
|
||||
@@ -74,7 +87,7 @@ public class OnboardingPublicSubmissionController
|
||||
FileLength = file.Length
|
||||
});
|
||||
|
||||
var fileId = await _fileService.UploadFile(SysDictConst.FILE_ENGINE_LOCAL, file);
|
||||
var fileId = await _fileService.UploadFile(SysDictConst.FILE_ENGINE_ALIYUN_OSS, file);
|
||||
await _service.SaveMaterial(new OnboardingSaveMaterialInput
|
||||
{
|
||||
Token = token,
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public class OnboardingPublicTaskController
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载当前任务锁定的合同模板源文件。
|
||||
/// 下载根据当前任务资料动态生成并已替换变量的合同文件。
|
||||
/// </summary>
|
||||
[HttpGet("contract-source")]
|
||||
public async Task<FileStreamResult> ContractSource([FromQuery] OnboardingPublicContractSourceInput input)
|
||||
|
||||
+9
@@ -32,4 +32,13 @@ public class OnboardingTaskLifecycleController
|
||||
{
|
||||
await _service.Revoke(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除已撤销的入职任务。
|
||||
/// </summary>
|
||||
[HttpPost("delete")]
|
||||
public async Task Delete([FromBody] BaseIdInput input)
|
||||
{
|
||||
await _service.Delete(input);
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
namespace SimpleAdmin.Web.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 提供入职模板聚合配置接口。
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("Application", Tag = "入职模板")]
|
||||
[Route("biz/onboarding/template")]
|
||||
[RolePermission]
|
||||
public class OnboardingTemplateController
|
||||
{
|
||||
private readonly IOnboardingTemplateService _service;
|
||||
|
||||
public OnboardingTemplateController(IOnboardingTemplateService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询入职模板。
|
||||
/// </summary>
|
||||
[HttpGet("page")]
|
||||
public async Task<dynamic> Page([FromQuery] OnboardingTemplatePageInput input)
|
||||
{
|
||||
return await _service.Page(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询入职模板详情及全部关联配置。
|
||||
/// </summary>
|
||||
[HttpGet("detail")]
|
||||
public async Task<dynamic> Detail([FromQuery] BaseIdInput input)
|
||||
{
|
||||
return await _service.Detail(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑入职模板。
|
||||
/// </summary>
|
||||
[HttpPost("save")]
|
||||
public async Task<long> Save([FromBody] OnboardingTemplateSaveInput input)
|
||||
{
|
||||
return await _service.Save(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被任务使用的入职模板。
|
||||
/// </summary>
|
||||
[HttpPost("delete")]
|
||||
public async Task Delete([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.Delete(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定公司岗位可使用的入职模板。
|
||||
/// </summary>
|
||||
[HttpGet("list-enabled")]
|
||||
public async Task<dynamic> ListEnabled([FromQuery] long companyId, [FromQuery] long positionId)
|
||||
{
|
||||
return await _service.ListEnabled(companyId, positionId);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
namespace SimpleAdmin.Web.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 合同变量字典定义管理。
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("Application", Tag = "合同变量字典")]
|
||||
[Route("biz/onboarding/variable-definition")]
|
||||
[RolePermission]
|
||||
public class OnboardingVariableDefinitionController
|
||||
{
|
||||
private readonly IOnboardingVariableDefinitionService _service;
|
||||
|
||||
public OnboardingVariableDefinitionController(IOnboardingVariableDefinitionService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
[HttpGet("page")]
|
||||
public async Task<dynamic> Page([FromQuery] OnboardingVariableDefinitionPageInput input)
|
||||
{
|
||||
return await _service.Page(input);
|
||||
}
|
||||
|
||||
[HttpGet("list")]
|
||||
public async Task<dynamic> List()
|
||||
{
|
||||
return await _service.ListAll();
|
||||
}
|
||||
|
||||
[HttpPost("add")]
|
||||
public async Task Add([FromBody] OnboardingVariableDefinitionAddInput input)
|
||||
{
|
||||
await _service.Add(input);
|
||||
}
|
||||
|
||||
[HttpPost("edit")]
|
||||
public async Task Edit([FromBody] OnboardingVariableDefinitionEditInput input)
|
||||
{
|
||||
await _service.Edit(input);
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
public async Task Delete([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.Delete(input);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
|
||||
// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛
|
||||
//
|
||||
// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款:
|
||||
// 1.请不要删除和修改根目录下的LICENSE文件。
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
global using MoYu;
|
||||
global using MoYu.DynamicApiController;
|
||||
global using MoYu.FriendlyException;
|
||||
global using MoYu.Logging;
|
||||
global using Microsoft.AspNetCore.Authorization;
|
||||
global using Microsoft.AspNetCore.Builder;
|
||||
|
||||
@@ -88,7 +88,12 @@
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingContractTemplateController">
|
||||
<summary>
|
||||
提供合同模板、版本、变量和适用范围相关 HTTP 接口。
|
||||
提供合同模板、DOCX 版本和自动识别变量配置接口。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.UploadDocx(Microsoft.AspNetCore.Http.IFormFile)">
|
||||
<summary>
|
||||
将入职合同 DOCX 模板上传到阿里云 OSS。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.Page(SimpleAdmin.Application.OnboardingContractTemplatePageInput)">
|
||||
@@ -131,46 +136,21 @@
|
||||
设置合同模板版本状态。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.DeleteVersion(SimpleAdmin.Core.BaseIdInput)">
|
||||
<summary>
|
||||
删除未被入职模板或任务使用的合同模板版本。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.VariablePage(SimpleAdmin.Application.OnboardingTemplateVariablePageInput)">
|
||||
<summary>
|
||||
分页查询模板变量。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.AddVariable(SimpleAdmin.Application.OnboardingTemplateVariableAddInput)">
|
||||
<summary>
|
||||
新增模板变量。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.EditVariable(SimpleAdmin.Application.OnboardingTemplateVariableEditInput)">
|
||||
<summary>
|
||||
编辑模板变量。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.DeleteVariable(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除模板变量。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.ScopePage(SimpleAdmin.Application.OnboardingContractScopePageInput)">
|
||||
<summary>
|
||||
分页查询合同适用范围。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.AddScope(SimpleAdmin.Application.OnboardingContractScopeAddInput)">
|
||||
<summary>
|
||||
新增合同适用范围。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.EditScope(SimpleAdmin.Application.OnboardingContractScopeEditInput)">
|
||||
<summary>
|
||||
编辑合同适用范围。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.DeleteScope(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除合同适用范围。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingEmployeeController">
|
||||
<summary>
|
||||
提供正式员工档案相关 HTTP 接口。
|
||||
@@ -188,7 +168,7 @@
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingEntryConfigController">
|
||||
<summary>
|
||||
提供入职字段与材料要求配置相关 HTTP 接口。
|
||||
提供员工动态补充字段与上传材料类型配置接口。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.ListFormFields">
|
||||
@@ -221,36 +201,6 @@
|
||||
删除入职材料类型。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.ListFormRequirements(System.Int64)">
|
||||
<summary>
|
||||
查询公司岗位动态字段要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.SaveFormRequirement(SimpleAdmin.Application.OnboardingFormRequirementSaveInput)">
|
||||
<summary>
|
||||
保存公司岗位动态字段要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.DeleteFormRequirements(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除动态字段要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.ListMaterialRequirements(System.Int64)">
|
||||
<summary>
|
||||
查询公司岗位材料要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.SaveMaterialRequirement(SimpleAdmin.Application.OnboardingMaterialRequirementSaveInput)">
|
||||
<summary>
|
||||
保存公司岗位材料要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.DeleteMaterialRequirements(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除材料要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingMaterialReviewController">
|
||||
<summary>
|
||||
提供候选人材料审核相关 HTTP 接口。
|
||||
@@ -311,6 +261,11 @@
|
||||
保存员工已上传材料记录。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingPublicSubmissionController.DeleteMaterial(SimpleAdmin.Application.OnboardingDeleteMaterialInput)">
|
||||
<summary>
|
||||
删除当前入职任务中的一条已上传材料。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingPublicSubmissionController.ConfirmContract(SimpleAdmin.Application.OnboardingConfirmContractInput)">
|
||||
<summary>
|
||||
确认已阅读指定合同。
|
||||
@@ -343,7 +298,7 @@
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingPublicTaskController.ContractSource(SimpleAdmin.Application.OnboardingPublicContractSourceInput)">
|
||||
<summary>
|
||||
下载当前任务锁定的合同模板源文件。
|
||||
下载根据当前任务资料动态生成并已替换变量的合同文件。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingTaskContractVariableController">
|
||||
@@ -386,6 +341,11 @@
|
||||
撤销入职任务及员工填写链接。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTaskLifecycleController.Delete(SimpleAdmin.Core.BaseIdInput)">
|
||||
<summary>
|
||||
删除已撤销的入职任务。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingTaskQueryController">
|
||||
<summary>
|
||||
提供入职任务查询相关 HTTP 接口。
|
||||
@@ -401,6 +361,41 @@
|
||||
查询入职任务完整详情。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingTemplateController">
|
||||
<summary>
|
||||
提供入职模板聚合配置接口。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.Page(SimpleAdmin.Application.OnboardingTemplatePageInput)">
|
||||
<summary>
|
||||
分页查询入职模板。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.Detail(SimpleAdmin.Core.BaseIdInput)">
|
||||
<summary>
|
||||
查询入职模板详情及全部关联配置。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.Save(SimpleAdmin.Application.OnboardingTemplateSaveInput)">
|
||||
<summary>
|
||||
新增或编辑入职模板。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.Delete(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除未被任务使用的入职模板。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.ListEnabled(System.Int64,System.Int64)">
|
||||
<summary>
|
||||
查询指定公司岗位可使用的入职模板。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingVariableDefinitionController">
|
||||
<summary>
|
||||
合同变量字典定义管理。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.BizOrgController">
|
||||
<summary>
|
||||
机构管理控制器
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
-- OnboardX 入职模板聚合重构
|
||||
-- 适用数据库:MySQL 8.x
|
||||
-- 执行前请备份 OnboardX 数据库。
|
||||
-- 新逻辑不再读取 ob_form_requirement、ob_material_requirement、ob_contract_scope,
|
||||
-- 三张旧表暂时保留,仅用于迁移和回滚;对应写接口已从业务代码中移除。
|
||||
-- 执行本脚本后请重启后端服务。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ob_onboarding_template` (
|
||||
`Id` BIGINT NOT NULL COMMENT '主键Id',
|
||||
`TemplateCode` VARCHAR(100) NOT NULL COMMENT '入职模板编码',
|
||||
`TemplateName` VARCHAR(200) NOT NULL COMMENT '入职模板名称',
|
||||
`CompanyId` BIGINT NOT NULL COMMENT '所属公司Id',
|
||||
`Description` VARCHAR(1000) NULL COMMENT '模板说明',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '状态',
|
||||
`CreateTime` DATETIME NULL COMMENT '创建时间',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
`CreateUserId` BIGINT NULL COMMENT '创建者Id',
|
||||
`UpdateUserId` BIGINT NULL COMMENT '修改者Id',
|
||||
`CreateUser` VARCHAR(100) NULL COMMENT '创建人',
|
||||
`UpdateUser` VARCHAR(100) NULL COMMENT '更新人',
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '软删除',
|
||||
`ExtJson` LONGTEXT NULL COMMENT '扩展信息',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_ob_onboarding_template_code` (`TemplateCode`),
|
||||
KEY `idx_ob_onboarding_template_company` (`CompanyId`, `Status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ob_onboarding_template_position` (
|
||||
`Id` BIGINT NOT NULL,
|
||||
`OnboardingTemplateId` BIGINT NOT NULL COMMENT '入职模板Id',
|
||||
`PositionId` BIGINT NOT NULL COMMENT '岗位Id',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable',
|
||||
`CreateTime` DATETIME NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
`CreateUserId` BIGINT NULL,
|
||||
`UpdateUserId` BIGINT NULL,
|
||||
`CreateUser` VARCHAR(100) NULL,
|
||||
`UpdateUser` VARCHAR(100) NULL,
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`ExtJson` LONGTEXT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_ob_template_position` (`OnboardingTemplateId`, `PositionId`),
|
||||
KEY `idx_ob_template_position_position` (`PositionId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板适用岗位';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ob_onboarding_template_contract` (
|
||||
`Id` BIGINT NOT NULL,
|
||||
`OnboardingTemplateId` BIGINT NOT NULL COMMENT '入职模板Id',
|
||||
`ContractTemplateId` BIGINT NOT NULL COMMENT '合同模板Id',
|
||||
`ContractTemplateVersionId` BIGINT NOT NULL COMMENT '合同模板版本Id',
|
||||
`Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必须确认',
|
||||
`PrintCopies` INT NOT NULL DEFAULT 1 COMMENT '打印份数',
|
||||
`SortCode` INT NULL COMMENT '排序码',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable',
|
||||
`CreateTime` DATETIME NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
`CreateUserId` BIGINT NULL,
|
||||
`UpdateUserId` BIGINT NULL,
|
||||
`CreateUser` VARCHAR(100) NULL,
|
||||
`UpdateUser` VARCHAR(100) NULL,
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`ExtJson` LONGTEXT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_ob_template_contract_version` (`OnboardingTemplateId`, `ContractTemplateVersionId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板合同配置';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ob_onboarding_template_form_field` (
|
||||
`Id` BIGINT NOT NULL,
|
||||
`OnboardingTemplateId` BIGINT NOT NULL COMMENT '入职模板Id',
|
||||
`FieldId` BIGINT NOT NULL COMMENT '动态字段Id',
|
||||
`Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必填',
|
||||
`SortCode` INT NULL COMMENT '排序码',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable',
|
||||
`CreateTime` DATETIME NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
`CreateUserId` BIGINT NULL,
|
||||
`UpdateUserId` BIGINT NULL,
|
||||
`CreateUser` VARCHAR(100) NULL,
|
||||
`UpdateUser` VARCHAR(100) NULL,
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`ExtJson` LONGTEXT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_ob_template_form_field` (`OnboardingTemplateId`, `FieldId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板补充字段';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ob_onboarding_template_material` (
|
||||
`Id` BIGINT NOT NULL,
|
||||
`OnboardingTemplateId` BIGINT NOT NULL COMMENT '入职模板Id',
|
||||
`MaterialTypeId` BIGINT NOT NULL COMMENT '材料类型Id',
|
||||
`Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必传',
|
||||
`SortCode` INT NULL COMMENT '排序码',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable',
|
||||
`CreateTime` DATETIME NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
`CreateUserId` BIGINT NULL,
|
||||
`UpdateUserId` BIGINT NULL,
|
||||
`CreateUser` VARCHAR(100) NULL,
|
||||
`UpdateUser` VARCHAR(100) NULL,
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`ExtJson` LONGTEXT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_ob_template_material` (`OnboardingTemplateId`, `MaterialTypeId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板上传材料';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ob_task_form_requirement` (
|
||||
`Id` BIGINT NOT NULL,
|
||||
`TaskId` BIGINT NOT NULL COMMENT '入职任务Id',
|
||||
`FieldId` BIGINT NOT NULL COMMENT '原动态字段Id',
|
||||
`FieldCode` VARCHAR(100) NOT NULL COMMENT '冻结字段编码',
|
||||
`FieldName` VARCHAR(200) NOT NULL COMMENT '冻结字段名称',
|
||||
`DataType` VARCHAR(50) NOT NULL COMMENT '冻结数据类型',
|
||||
`OptionsJson` JSON NULL COMMENT '冻结选项JSON',
|
||||
`Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必填',
|
||||
`SortCode` INT NULL COMMENT '排序码',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable',
|
||||
`CreateTime` DATETIME NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
`CreateUserId` BIGINT NULL,
|
||||
`UpdateUserId` BIGINT NULL,
|
||||
`CreateUser` VARCHAR(100) NULL,
|
||||
`UpdateUser` VARCHAR(100) NULL,
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`ExtJson` LONGTEXT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_ob_task_form_requirement` (`TaskId`, `FieldId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务补充字段快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ob_task_material_requirement` (
|
||||
`Id` BIGINT NOT NULL,
|
||||
`TaskId` BIGINT NOT NULL COMMENT '入职任务Id',
|
||||
`MaterialTypeId` BIGINT NOT NULL COMMENT '原材料类型Id',
|
||||
`MaterialCode` VARCHAR(100) NOT NULL COMMENT '冻结材料编码',
|
||||
`MaterialName` VARCHAR(200) NOT NULL COMMENT '冻结材料名称',
|
||||
`AllowedExtensions` VARCHAR(500) NULL COMMENT '冻结允许扩展名',
|
||||
`MaxFileCount` INT NOT NULL DEFAULT 1 COMMENT '冻结最大文件数',
|
||||
`MaxFileSizeMb` INT NULL COMMENT '冻结单文件大小MB',
|
||||
`Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必传',
|
||||
`SortCode` INT NULL COMMENT '排序码',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable',
|
||||
`CreateTime` DATETIME NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
`CreateUserId` BIGINT NULL,
|
||||
`UpdateUserId` BIGINT NULL,
|
||||
`CreateUser` VARCHAR(100) NULL,
|
||||
`UpdateUser` VARCHAR(100) NULL,
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`ExtJson` LONGTEXT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_ob_task_material_requirement` (`TaskId`, `MaterialTypeId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务上传材料快照';
|
||||
|
||||
DROP PROCEDURE IF EXISTS `ob_add_column_if_missing`;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE `ob_add_column_if_missing`(
|
||||
IN p_table_name VARCHAR(128),
|
||||
IN p_column_name VARCHAR(128),
|
||||
IN p_column_definition VARCHAR(1000)
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = p_table_name
|
||||
AND COLUMN_NAME = p_column_name
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `',
|
||||
p_column_name, '` ', p_column_definition);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL `ob_add_column_if_missing`(
|
||||
'ob_onboarding_task',
|
||||
'OnboardingTemplateId',
|
||||
'BIGINT NOT NULL DEFAULT 0 COMMENT ''创建任务时锁定的入职模板Id'' AFTER `PositionId`'
|
||||
);
|
||||
CALL `ob_add_column_if_missing`(
|
||||
'ob_task_contract',
|
||||
'PrintCopies',
|
||||
'INT NOT NULL DEFAULT 1 COMMENT ''合同打印份数'' AFTER `Required`'
|
||||
);
|
||||
DROP PROCEDURE IF EXISTS `ob_add_column_if_missing`;
|
||||
|
||||
-- 将旧的公司/岗位合同范围迁移为“一公司一岗位一入职模板”。
|
||||
-- 公司级合同、字段和材料要求会自动继承到该公司的每个岗位模板。
|
||||
DROP TEMPORARY TABLE IF EXISTS `tmp_ob_template_map`;
|
||||
CREATE TEMPORARY TABLE `tmp_ob_template_map` (
|
||||
`CompanyId` BIGINT NOT NULL,
|
||||
`PositionId` BIGINT NOT NULL,
|
||||
`TemplateCode` VARCHAR(100) NOT NULL,
|
||||
`OnboardingTemplateId` BIGINT NULL,
|
||||
PRIMARY KEY (`CompanyId`, `PositionId`)
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO `tmp_ob_template_map` (`CompanyId`, `PositionId`, `TemplateCode`)
|
||||
SELECT DISTINCT p.`CompanyId`, p.`Id`, CONCAT('MIGRATED_', p.`CompanyId`, '_', p.`Id`)
|
||||
FROM `ob_position` p
|
||||
INNER JOIN (
|
||||
SELECT `CompanyId` FROM `ob_contract_scope` WHERE `IsDelete` = 0
|
||||
UNION
|
||||
SELECT `CompanyId` FROM `ob_form_requirement` WHERE `IsDelete` = 0
|
||||
UNION
|
||||
SELECT `CompanyId` FROM `ob_material_requirement` WHERE `IsDelete` = 0
|
||||
) configured_company ON configured_company.`CompanyId` = p.`CompanyId`
|
||||
WHERE p.`IsDelete` = 0;
|
||||
|
||||
INSERT IGNORE INTO `ob_onboarding_template` (
|
||||
`Id`, `TemplateCode`, `TemplateName`, `CompanyId`, `Description`, `Status`, `CreateTime`, `IsDelete`
|
||||
)
|
||||
SELECT
|
||||
UUID_SHORT(),
|
||||
m.`TemplateCode`,
|
||||
CONCAT(COALESCE(c.`CompanyName`, m.`CompanyId`), '-', COALESCE(p.`PositionName`, m.`PositionId`), '-入职模板'),
|
||||
m.`CompanyId`,
|
||||
'由旧公司岗位配置自动迁移,请管理员检查后使用',
|
||||
'enable',
|
||||
NOW(),
|
||||
0
|
||||
FROM `tmp_ob_template_map` m
|
||||
LEFT JOIN `ob_company` c ON c.`Id` = m.`CompanyId`
|
||||
LEFT JOIN `ob_position` p ON p.`Id` = m.`PositionId`;
|
||||
|
||||
UPDATE `tmp_ob_template_map` m
|
||||
INNER JOIN `ob_onboarding_template` t ON t.`TemplateCode` = m.`TemplateCode`
|
||||
SET m.`OnboardingTemplateId` = t.`Id`;
|
||||
|
||||
INSERT IGNORE INTO `ob_onboarding_template_position` (
|
||||
`Id`, `OnboardingTemplateId`, `PositionId`, `Status`, `CreateTime`, `IsDelete`
|
||||
)
|
||||
SELECT UUID_SHORT(), m.`OnboardingTemplateId`, m.`PositionId`, 'enable', NOW(), 0
|
||||
FROM `tmp_ob_template_map` m;
|
||||
|
||||
INSERT IGNORE INTO `ob_onboarding_template_contract` (
|
||||
`Id`, `OnboardingTemplateId`, `ContractTemplateId`, `ContractTemplateVersionId`,
|
||||
`Required`, `PrintCopies`, `SortCode`, `Status`, `CreateTime`, `IsDelete`
|
||||
)
|
||||
SELECT
|
||||
UUID_SHORT(),
|
||||
m.`OnboardingTemplateId`,
|
||||
MAX(s.`TemplateId`),
|
||||
s.`TemplateVersionId`,
|
||||
COALESCE(
|
||||
MAX(CASE WHEN s.`PositionId` = m.`PositionId` THEN s.`Required` END),
|
||||
MAX(s.`Required`)
|
||||
),
|
||||
1,
|
||||
COALESCE(
|
||||
MAX(CASE WHEN s.`PositionId` = m.`PositionId` THEN s.`SortCode` END),
|
||||
MAX(s.`SortCode`)
|
||||
),
|
||||
'enable',
|
||||
NOW(),
|
||||
0
|
||||
FROM `tmp_ob_template_map` m
|
||||
INNER JOIN `ob_contract_scope` s
|
||||
ON s.`CompanyId` = m.`CompanyId`
|
||||
AND (s.`PositionId` IS NULL OR s.`PositionId` = m.`PositionId`)
|
||||
AND s.`IsDelete` = 0
|
||||
GROUP BY m.`OnboardingTemplateId`, s.`TemplateVersionId`;
|
||||
|
||||
INSERT IGNORE INTO `ob_onboarding_template_form_field` (
|
||||
`Id`, `OnboardingTemplateId`, `FieldId`, `Required`, `SortCode`, `Status`, `CreateTime`, `IsDelete`
|
||||
)
|
||||
SELECT
|
||||
UUID_SHORT(),
|
||||
m.`OnboardingTemplateId`,
|
||||
r.`FieldId`,
|
||||
COALESCE(
|
||||
MAX(CASE WHEN r.`PositionId` = m.`PositionId` THEN r.`Required` END),
|
||||
MAX(r.`Required`)
|
||||
),
|
||||
COALESCE(
|
||||
MAX(CASE WHEN r.`PositionId` = m.`PositionId` THEN r.`SortCode` END),
|
||||
MAX(r.`SortCode`)
|
||||
),
|
||||
'enable',
|
||||
NOW(),
|
||||
0
|
||||
FROM `tmp_ob_template_map` m
|
||||
INNER JOIN `ob_form_requirement` r
|
||||
ON r.`CompanyId` = m.`CompanyId`
|
||||
AND (r.`PositionId` IS NULL OR r.`PositionId` = m.`PositionId`)
|
||||
AND r.`IsDelete` = 0
|
||||
GROUP BY m.`OnboardingTemplateId`, r.`FieldId`;
|
||||
|
||||
INSERT IGNORE INTO `ob_onboarding_template_material` (
|
||||
`Id`, `OnboardingTemplateId`, `MaterialTypeId`, `Required`, `SortCode`, `Status`, `CreateTime`, `IsDelete`
|
||||
)
|
||||
SELECT
|
||||
UUID_SHORT(),
|
||||
m.`OnboardingTemplateId`,
|
||||
r.`MaterialTypeId`,
|
||||
COALESCE(
|
||||
MAX(CASE WHEN r.`PositionId` = m.`PositionId` THEN r.`Required` END),
|
||||
MAX(r.`Required`)
|
||||
),
|
||||
COALESCE(
|
||||
MAX(CASE WHEN r.`PositionId` = m.`PositionId` THEN r.`SortCode` END),
|
||||
MAX(r.`SortCode`)
|
||||
),
|
||||
'enable',
|
||||
NOW(),
|
||||
0
|
||||
FROM `tmp_ob_template_map` m
|
||||
INNER JOIN `ob_material_requirement` r
|
||||
ON r.`CompanyId` = m.`CompanyId`
|
||||
AND (r.`PositionId` IS NULL OR r.`PositionId` = m.`PositionId`)
|
||||
AND r.`IsDelete` = 0
|
||||
GROUP BY m.`OnboardingTemplateId`, r.`MaterialTypeId`;
|
||||
|
||||
-- 为迁移前已创建的入职任务回填入职模板,并冻结字段/材料要求快照。
|
||||
UPDATE `ob_onboarding_task` task
|
||||
INNER JOIN `tmp_ob_template_map` m
|
||||
ON m.`CompanyId` = task.`CompanyId`
|
||||
AND m.`PositionId` = task.`PositionId`
|
||||
SET task.`OnboardingTemplateId` = m.`OnboardingTemplateId`
|
||||
WHERE task.`OnboardingTemplateId` = 0;
|
||||
|
||||
INSERT IGNORE INTO `ob_task_form_requirement` (
|
||||
`Id`, `TaskId`, `FieldId`, `FieldCode`, `FieldName`, `DataType`, `OptionsJson`,
|
||||
`Required`, `SortCode`, `Status`, `CreateTime`, `IsDelete`
|
||||
)
|
||||
SELECT
|
||||
UUID_SHORT(), task.`Id`, config.`FieldId`, field.`FieldCode`, field.`FieldName`,
|
||||
field.`DataType`, field.`OptionsJson`, config.`Required`, config.`SortCode`,
|
||||
'enable', NOW(), 0
|
||||
FROM `ob_onboarding_task` task
|
||||
INNER JOIN `ob_onboarding_template_form_field` config
|
||||
ON config.`OnboardingTemplateId` = task.`OnboardingTemplateId`
|
||||
INNER JOIN `ob_form_field` field ON field.`Id` = config.`FieldId` AND field.`IsDelete` = 0
|
||||
WHERE task.`OnboardingTemplateId` <> 0;
|
||||
|
||||
INSERT IGNORE INTO `ob_task_material_requirement` (
|
||||
`Id`, `TaskId`, `MaterialTypeId`, `MaterialCode`, `MaterialName`,
|
||||
`AllowedExtensions`, `MaxFileCount`, `MaxFileSizeMb`, `Required`, `SortCode`,
|
||||
`Status`, `CreateTime`, `IsDelete`
|
||||
)
|
||||
SELECT
|
||||
UUID_SHORT(), task.`Id`, config.`MaterialTypeId`, material.`MaterialCode`,
|
||||
material.`MaterialName`, material.`AllowedExtensions`, material.`MaxFileCount`,
|
||||
material.`MaxFileSizeMb`, config.`Required`, config.`SortCode`,
|
||||
'enable', NOW(), 0
|
||||
FROM `ob_onboarding_task` task
|
||||
INNER JOIN `ob_onboarding_template_material` config
|
||||
ON config.`OnboardingTemplateId` = task.`OnboardingTemplateId`
|
||||
INNER JOIN `ob_material_type` material ON material.`Id` = config.`MaterialTypeId` AND material.`IsDelete` = 0
|
||||
WHERE task.`OnboardingTemplateId` <> 0;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS `tmp_ob_template_map`;
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- 执行后检查:
|
||||
-- SELECT * FROM ob_onboarding_template;
|
||||
-- SELECT * FROM ob_onboarding_template_position;
|
||||
-- SELECT * FROM ob_onboarding_template_contract;
|
||||
-- SELECT * FROM ob_onboarding_template_form_field;
|
||||
-- SELECT * FROM ob_onboarding_template_material;
|
||||
@@ -0,0 +1,144 @@
|
||||
-- ====================================================================
|
||||
-- OnboardX 数据库全量升级与重建脚本 (2026-08-25)
|
||||
-- 包含标准化《入职登记表》全字段、合同变量字典体系、精简入职模板
|
||||
-- ====================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- 1. 变量字典定义表 (新核心表)
|
||||
CREATE TABLE IF NOT EXISTS `ob_variable_definition` (
|
||||
`Id` BIGINT NOT NULL COMMENT '主键Id',
|
||||
`VariableCode` VARCHAR(128) NOT NULL COMMENT '变量全编码 (如 employee.EmployeeName, hr.BaseSalary)',
|
||||
`VariableName` VARCHAR(128) NOT NULL COMMENT '变量中文显示名称',
|
||||
`VariableCategory` VARCHAR(32) NOT NULL COMMENT '变量分类: employee, hr, company, system',
|
||||
`DataType` VARCHAR(32) NOT NULL DEFAULT 'string' COMMENT '数据类型: string, number, date',
|
||||
`DefaultValue` VARCHAR(512) NULL COMMENT '默认值/占位提示',
|
||||
`IsBuiltIn` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为登记表内置字段 (1=是,0=否)',
|
||||
`Required` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否必填',
|
||||
`SortCode` INT NOT NULL DEFAULT 100 COMMENT '排序码',
|
||||
`Remark` VARCHAR(512) NULL COMMENT '说明备注',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '状态',
|
||||
`CreateTime` DATETIME NULL COMMENT '创建时间',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
`CreateUserId` BIGINT NULL COMMENT '创建者Id',
|
||||
`UpdateUserId` BIGINT NULL COMMENT '修改者Id',
|
||||
`CreateUser` VARCHAR(64) NULL COMMENT '创建人',
|
||||
`UpdateUser` VARCHAR(64) NULL COMMENT '更新人',
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '软删除',
|
||||
`ExtJson` LONGTEXT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_variable_code` (`VariableCode`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同变量字典定义表';
|
||||
|
||||
-- 预置标准变量字典数据
|
||||
TRUNCATE TABLE `ob_variable_definition`;
|
||||
|
||||
INSERT INTO `ob_variable_definition` (`Id`, `VariableCode`, `VariableName`, `VariableCategory`, `DataType`, `IsBuiltIn`, `Required`, `SortCode`, `Remark`, `CreateTime`, `IsDelete`) VALUES
|
||||
-- 员工入职登记表内置字段 (employee.)
|
||||
(841400000000001, 'employee.EmployeeName', '员工姓名', 'employee', 'string', 1, 1, 10, '入职登记表-姓名', NOW(), 0),
|
||||
(841400000000002, 'employee.Gender', '性别', 'employee', 'string', 1, 1, 20, '入职登记表-性别', NOW(), 0),
|
||||
(841400000000003, 'employee.Nation', '民族', 'employee', 'string', 1, 0, 30, '入职登记表-民族', NOW(), 0),
|
||||
(841400000000004, 'employee.Birthday', '出生年月日', 'employee', 'date', 1, 0, 40, '入职登记表-出生日期', NOW(), 0),
|
||||
(841400000000005, 'employee.RegisteredResidence', '户籍所在地', 'employee', 'string', 1, 0, 50, '入职登记表-户籍', NOW(), 0),
|
||||
(841400000000006, 'employee.Height', '身高(cm)', 'employee', 'string', 1, 0, 60, '入职登记表-身高', NOW(), 0),
|
||||
(841400000000007, 'employee.Weight', '体重(kg)', 'employee', 'string', 1, 0, 70, '入职登记表-体重', NOW(), 0),
|
||||
(841400000000008, 'employee.HealthCondition', '健康状况', 'employee', 'string', 1, 0, 80, '入职登记表-健康状况', NOW(), 0),
|
||||
(841400000000009, 'employee.MaritalStatus', '婚育情况', 'employee', 'string', 1, 0, 90, '入职登记表-婚育情况', NOW(), 0),
|
||||
(841400000000010, 'employee.Education', '最高学历', 'employee', 'string', 1, 0, 100, '入职登记表-学历', NOW(), 0),
|
||||
(841400000000011, 'employee.Major', '所学专业', 'employee', 'string', 1, 0, 110, '入职登记表-专业', NOW(), 0),
|
||||
(841400000000012, 'employee.GraduatedSchool', '毕业院校', 'employee', 'string', 1, 0, 120, '入职登记表-毕业院校', NOW(), 0),
|
||||
(841400000000013, 'employee.VocationalQualification', '职业资格', 'employee', 'string', 1, 0, 130, '入职登记表-职业资格', NOW(), 0),
|
||||
(841400000000014, 'employee.QualificationLevel', '资格等级', 'employee', 'string', 1, 0, 140, '入职登记表-等级', NOW(), 0),
|
||||
(841400000000015, 'employee.TechnicalSpecialty', '技术特长', 'employee', 'string', 1, 0, 150, '入职登记表-有何技术特长', NOW(), 0),
|
||||
(841400000000016, 'employee.Phone', '联系电话', 'employee', 'string', 1, 1, 160, '入职登记表-联系电话', NOW(), 0),
|
||||
(841400000000017, 'employee.Email', '电子邮箱', 'employee', 'string', 1, 0, 170, '入职登记表-邮箱', NOW(), 0),
|
||||
(841400000000018, 'employee.IdCardNumber', '身份证号码', 'employee', 'string', 1, 1, 180, '入职登记表-身份证号码', NOW(), 0),
|
||||
(841400000000019, 'employee.CurrentAddress', '现居住/暂住地址', 'employee', 'string', 1, 0, 190, '入职登记表-暂住地址', NOW(), 0),
|
||||
(841400000000020, 'employee.LandlordContact', '房东联系方式', 'employee', 'string', 1, 0, 200, '入职登记表-房东联系方式', NOW(), 0),
|
||||
(841400000000021, 'employee.BankCardNumber', '工资银行卡号', 'employee', 'string', 1, 0, 210, '入职登记表-银行卡号', NOW(), 0),
|
||||
(841400000000022, 'employee.BankName', '开户银行', 'employee', 'string', 1, 0, 220, '入职登记表-开户银行', NOW(), 0),
|
||||
(841400000000023, 'employee.BankAccountName', '开户人姓名', 'employee', 'string', 1, 0, 230, '入职登记表-开户名', NOW(), 0),
|
||||
(841400000000024, 'employee.EmergencyContact', '紧急联系人姓名', 'employee', 'string', 1, 0, 240, '入职登记表-紧急联系人', NOW(), 0),
|
||||
(841400000000025, 'employee.EmergencyPhone', '紧急联系人电话', 'employee', 'string', 1, 0, 250, '入职登记表-紧急联系人电话', NOW(), 0),
|
||||
(841400000000026, 'employee.EmergencyAddress', '紧急联系人地址', 'employee', 'string', 1, 0, 260, '入职登记表-紧急联系人地址', NOW(), 0),
|
||||
|
||||
-- 人事合同参数 (hr.)
|
||||
(841400000000101, 'hr.BaseSalary', '基本工资(元)', 'hr', 'number', 1, 1, 300, '合同约定基本工资', NOW(), 0),
|
||||
(841400000000102, 'hr.PositionSalary', '岗位工资(元)', 'hr', 'number', 1, 0, 310, '合同约定岗位工资', NOW(), 0),
|
||||
(841400000000103, 'hr.PerformanceAllowance', '绩效津贴(元)', 'hr', 'number', 1, 0, 320, '合同绩效工资/津贴', NOW(), 0),
|
||||
(841400000000104, 'hr.AttendanceBonus', '全勤奖(元)', 'hr', 'number', 1, 0, 330, '全勤奖金', NOW(), 0),
|
||||
(841400000000105, 'hr.RegularSalary', '转正后薪资(元)', 'hr', 'number', 1, 0, 340, '转正薪资', NOW(), 0),
|
||||
(841400000000106, 'hr.WorkCity', '工作地点/城市', 'hr', 'string', 1, 0, 350, '合同履行工作地点', NOW(), 0),
|
||||
(841400000000107, 'hr.PositionName', '聘用岗位名称', 'hr', 'string', 1, 0, 360, '入职岗位名称', NOW(), 0),
|
||||
(841400000000108, 'hr.ContractTerm', '合同期限', 'hr', 'string', 1, 0, 370, '固定期限,如:3年', NOW(), 0),
|
||||
(841400000000109, 'hr.ContractStartDate', '合同开始日期', 'hr', 'date', 1, 1, 380, '劳动合同起算日期', NOW(), 0),
|
||||
(841400000000110, 'hr.ContractEndDate', '合同结束日期', 'hr', 'date', 1, 1, 390, '劳动合同终止日期', NOW(), 0),
|
||||
(841400000000111, 'hr.ProbationTerm', '试用期期限', 'hr', 'string', 1, 0, 400, '试用期时长,如:3个月', NOW(), 0),
|
||||
(841400000000112, 'hr.ProbationStartDate', '试用期开始日期', 'hr', 'date', 1, 0, 410, '试用期起算日期', NOW(), 0),
|
||||
(841400000000113, 'hr.ProbationEndDate', '试用期结束日期', 'hr', 'date', 1, 0, 420, '试用期截至日期', NOW(), 0),
|
||||
(841400000000114, 'hr.EmployeeNo', '员工工号', 'hr', 'string', 1, 0, 430, '企业内部员工编号', NOW(), 0),
|
||||
|
||||
-- 公司与系统参数 (company. / system.)
|
||||
(841400000000201, 'company.CompanyName', '公司主体全称', 'company', 'string', 1, 0, 500, '签署方甲方公司全称', NOW(), 0),
|
||||
(841400000000202, 'company.CompanyCode', '公司统一代码', 'company', 'string', 1, 0, 510, '统一社会信用代码', NOW(), 0),
|
||||
(841400000000203, 'company.LegalRepresentative', '法定代表人', 'company', 'string', 1, 0, 520, '甲方企业法定代表人', NOW(), 0),
|
||||
(841400000000204, 'company.Address', '公司注册地址', 'company', 'string', 1, 0, 530, '甲方注册地址', NOW(), 0),
|
||||
(841400000000205, 'company.Phone', '公司联系电话', 'company', 'string', 1, 0, 540, '甲方联系电话', NOW(), 0),
|
||||
(841400000000301, 'system.TaskNo', '入职任务编号', 'system', 'string', 1, 0, 600, '系统入职流水号', NOW(), 0),
|
||||
(841400000000302, 'system.SignDate', '合同签署日期', 'system', 'date', 1, 0, 610, '实际签署生成日期', NOW(), 0);
|
||||
|
||||
-- 2. 入职登记表员工档案表升级重建
|
||||
DROP TABLE IF EXISTS `ob_task_employee_info`;
|
||||
CREATE TABLE `ob_task_employee_info` (
|
||||
`Id` BIGINT NOT NULL COMMENT '主键Id',
|
||||
`TaskId` BIGINT NOT NULL COMMENT '所属入职任务Id',
|
||||
`EmployeeNo` VARCHAR(64) NULL COMMENT '员工工号',
|
||||
`EmployeeName` VARCHAR(64) NULL COMMENT '员工姓名',
|
||||
`Gender` VARCHAR(16) NULL COMMENT '性别',
|
||||
`Nation` VARCHAR(32) NULL COMMENT '民族',
|
||||
`Birthday` VARCHAR(32) NULL COMMENT '出生年月日',
|
||||
`RegisteredResidence` VARCHAR(256) NULL COMMENT '户籍所在地',
|
||||
`Height` VARCHAR(16) NULL COMMENT '身高',
|
||||
`Weight` VARCHAR(16) NULL COMMENT '体重',
|
||||
`HealthCondition` VARCHAR(64) NULL COMMENT '健康状况',
|
||||
`MaritalStatus` VARCHAR(32) NULL COMMENT '婚育情况',
|
||||
`Education` VARCHAR(32) NULL COMMENT '最高学历',
|
||||
`Major` VARCHAR(64) NULL COMMENT '所学专业',
|
||||
`GraduatedSchool` VARCHAR(128) NULL COMMENT '毕业院校',
|
||||
`VocationalQualification` VARCHAR(128) NULL COMMENT '职业资格',
|
||||
`QualificationLevel` VARCHAR(64) NULL COMMENT '资格等级',
|
||||
`TechnicalSpecialty` VARCHAR(256) NULL COMMENT '有何技术特长',
|
||||
`Phone` VARCHAR(32) NULL COMMENT '联系电话',
|
||||
`Email` VARCHAR(64) NULL COMMENT '电子邮箱',
|
||||
`IdCardNumber` VARCHAR(32) NULL COMMENT '身份证号码',
|
||||
`CurrentAddress` VARCHAR(256) NULL COMMENT '暂住/现居住地址',
|
||||
`LandlordContact` VARCHAR(64) NULL COMMENT '房东联系方式',
|
||||
`BankCardNumber` VARCHAR(64) NULL COMMENT '工资银行卡号',
|
||||
`BankName` VARCHAR(128) NULL COMMENT '开户银行名称',
|
||||
`BankAccountName` VARCHAR(64) NULL COMMENT '银行卡开户名',
|
||||
`EmergencyContact` VARCHAR(64) NULL COMMENT '紧急联系人姓名',
|
||||
`EmergencyPhone` VARCHAR(32) NULL COMMENT '紧急联系人电话',
|
||||
`EmergencyAddress` VARCHAR(256) NULL COMMENT '紧急联系人地址',
|
||||
`EducationHistoryJson` LONGTEXT NULL COMMENT '教育学习经历 JSON',
|
||||
`WorkHistoryJson` LONGTEXT NULL COMMENT '主要工作经历 JSON',
|
||||
`FamilyMembersJson` LONGTEXT NULL COMMENT '家庭成员 JSON',
|
||||
`OnboardingDepartment` VARCHAR(64) NULL COMMENT '入职部门',
|
||||
`OnboardingDate` VARCHAR(32) NULL COMMENT '入职时间',
|
||||
`OnboardingPosition` VARCHAR(64) NULL COMMENT '入职岗位',
|
||||
`OtherRemarks` VARCHAR(512) NULL COMMENT '其他说明',
|
||||
`CustomVariablesJson` LONGTEXT NULL COMMENT '特殊合同增量变量键值 JSON',
|
||||
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '状态',
|
||||
`CreateTime` DATETIME NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
`CreateUserId` BIGINT NULL,
|
||||
`UpdateUserId` BIGINT NULL,
|
||||
`CreateUser` VARCHAR(64) NULL,
|
||||
`UpdateUser` VARCHAR(64) NULL,
|
||||
`IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`ExtJson` LONGTEXT NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_task_id` (`TaskId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='候选人填写的入职登记表';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@@ -0,0 +1,68 @@
|
||||
-- OnboardX 新版入职登记表与框架实体字段对齐
|
||||
-- 仅补列,不删除或重建现有业务数据。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `ob_add_column_if_missing`;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE `ob_add_column_if_missing`(
|
||||
IN p_table_name VARCHAR(128),
|
||||
IN p_column_name VARCHAR(128),
|
||||
IN p_column_definition VARCHAR(1000)
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = p_table_name
|
||||
AND COLUMN_NAME = p_column_name
|
||||
) THEN
|
||||
SET @ddl = CONCAT(
|
||||
'ALTER TABLE `', p_table_name, '` ADD COLUMN `',
|
||||
p_column_name, '` ', p_column_definition
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL `ob_add_column_if_missing`(
|
||||
'ob_variable_definition',
|
||||
'Status',
|
||||
'VARCHAR(20) NULL DEFAULT ''enable'' COMMENT ''状态'' AFTER `Remark`'
|
||||
);
|
||||
CALL `ob_add_column_if_missing`(
|
||||
'ob_variable_definition',
|
||||
'CreateUserId',
|
||||
'BIGINT NULL COMMENT ''创建者Id'' AFTER `UpdateTime`'
|
||||
);
|
||||
CALL `ob_add_column_if_missing`(
|
||||
'ob_variable_definition',
|
||||
'UpdateUserId',
|
||||
'BIGINT NULL COMMENT ''修改者Id'' AFTER `CreateUserId`'
|
||||
);
|
||||
|
||||
CALL `ob_add_column_if_missing`(
|
||||
'ob_task_employee_info',
|
||||
'Status',
|
||||
'VARCHAR(20) NULL DEFAULT ''enable'' COMMENT ''状态'' AFTER `CustomVariablesJson`'
|
||||
);
|
||||
CALL `ob_add_column_if_missing`(
|
||||
'ob_task_employee_info',
|
||||
'CreateUserId',
|
||||
'BIGINT NULL COMMENT ''创建者Id'' AFTER `UpdateTime`'
|
||||
);
|
||||
CALL `ob_add_column_if_missing`(
|
||||
'ob_task_employee_info',
|
||||
'UpdateUserId',
|
||||
'BIGINT NULL COMMENT ''修改者Id'' AFTER `CreateUserId`'
|
||||
);
|
||||
|
||||
DROP PROCEDURE IF EXISTS `ob_add_column_if_missing`;
|
||||
|
||||
-- 检查结果:
|
||||
-- SHOW COLUMNS FROM ob_variable_definition;
|
||||
-- SHOW COLUMNS FROM ob_task_employee_info;
|
||||
Reference in New Issue
Block a user