diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObContractTemplate.cs b/SimpleAdmin.Application/Entity/Onboarding/ObContractTemplate.cs
index 331af71..e43ba8a 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/ObContractTemplate.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObContractTemplate.cs
@@ -17,6 +17,18 @@ namespace SimpleAdmin.Application;
[Tenant(SqlSugarConst.DB_DEFAULT)]
public class ObContractTemplate : BaseEntity
{
+ ///
+ /// 合同模板所属入职业务公司 Id。
+ ///
+ [SugarColumn(IsNullable = true)]
+ public long? CompanyId { get; set; }
+
+ ///
+ /// 合同模板所属入职业务部门 Id。
+ ///
+ [SugarColumn(IsNullable = true)]
+ public long? DepartmentId { get; set; }
+
///
/// 合同模板业务编码。
///
@@ -36,4 +48,28 @@ public class ObContractTemplate : BaseEntity
/// 模板用途说明。
///
public string Description { get; set; }
+
+ ///
+ /// 所属公司名称,仅用于接口展示。
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public string CompanyName { get; set; }
+
+ ///
+ /// 所属部门名称,仅用于接口展示。
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public string DepartmentName { get; set; }
+
+ ///
+ /// 适用岗位 Id 列表,仅用于接口展示和编辑回显。
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public List PositionIds { get; set; } = new();
+
+ ///
+ /// 适用岗位名称列表,仅用于接口展示。
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public List PositionNames { get; set; } = new();
}
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObContractTemplatePosition.cs b/SimpleAdmin.Application/Entity/Onboarding/ObContractTemplatePosition.cs
new file mode 100644
index 0000000..719c933
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObContractTemplatePosition.cs
@@ -0,0 +1,19 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 合同模板适用岗位关系,用于限制合同模板可被哪些岗位的入职模板选择。
+///
+[SugarTable("ob_contract_template_position", TableDescription = "合同模板适用岗位")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObContractTemplatePosition : BaseEntity
+{
+ ///
+ /// 合同模板主档案 Id。
+ ///
+ public long ContractTemplateId { get; set; }
+
+ ///
+ /// 适用岗位 Id。
+ ///
+ public long PositionId { get; set; }
+}
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObDepartment.cs b/SimpleAdmin.Application/Entity/Onboarding/ObDepartment.cs
new file mode 100644
index 0000000..fbd3e23
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObDepartment.cs
@@ -0,0 +1,40 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职业务部门档案,属于某家入职公司。
+///
+[SugarTable("ob_department", TableDescription = "入职业务部门")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObDepartment : BaseEntity
+{
+ ///
+ /// 所属入职业务公司 Id。
+ ///
+ public long CompanyId { get; set; }
+
+ ///
+ /// 部门业务编码。
+ ///
+ public virtual string DepartmentCode { get; set; }
+
+ ///
+ /// 部门名称。
+ ///
+ public virtual string DepartmentName { get; set; }
+
+ ///
+ /// 排序码。
+ ///
+ public int SortCode { get; set; } = 99;
+
+ ///
+ /// 业务备注。
+ ///
+ public string Remark { get; set; }
+
+ ///
+ /// 所属公司名称 (展示用)
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public string CompanyName { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObEmployee.cs b/SimpleAdmin.Application/Entity/Onboarding/ObEmployee.cs
index a747cef..b7c347c 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/ObEmployee.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObEmployee.cs
@@ -37,6 +37,12 @@ public class ObEmployee : BaseEntity
///
public long CompanyId { get; set; }
+ ///
+ /// 所属入职业务部门 Id。
+ ///
+ [SugarColumn(IsNullable = true)]
+ public long? DepartmentId { get; set; }
+
///
/// 所属部门名称。
///
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs
index a990480..c8f9c8e 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs
@@ -22,6 +22,12 @@ public class ObOnboardingTask : BaseEntity
///
public long CompanyId { get; set; }
+ ///
+ /// 所属入职业务部门 Id。
+ ///
+ [SugarColumn(IsNullable = true)]
+ public long? DepartmentId { get; set; }
+
///
/// 入职任务所属岗位 Id。
///
@@ -83,6 +89,12 @@ public class ObOnboardingTask : BaseEntity
[SugarColumn(IsIgnore = true)]
public string CompanyName { get; set; }
+ ///
+ /// 部门名称 (展示用)
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public string DepartmentName { get; set; }
+
///
/// 岗位名称 (展示用)
///
@@ -94,5 +106,4 @@ public class ObOnboardingTask : BaseEntity
///
[SugarColumn(IsIgnore = true)]
public string OnboardingTemplateName { get; set; }
-
-}
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplate.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplate.cs
index 082c800..d8d28bf 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplate.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplate.cs
@@ -22,8 +22,14 @@ public class ObOnboardingTemplate : BaseEntity
///
public long CompanyId { get; set; }
+ ///
+ /// 所属入职业务部门 Id。
+ ///
+ [SugarColumn(IsNullable = true)]
+ public long? DepartmentId { get; set; }
+
///
/// 模板用途和适用范围说明。
///
public string Description { get; set; }
-}
\ No newline at end of file
+}
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObPosition.cs b/SimpleAdmin.Application/Entity/Onboarding/ObPosition.cs
index dcedb81..6adaa46 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/ObPosition.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObPosition.cs
@@ -11,7 +11,7 @@
namespace SimpleAdmin.Application;
///
-/// 入职业务岗位档案,保存岗位默认合同期限、试用期及业务参数。
+/// 入职业务岗位档案,保存岗位所属部门、默认合同期限、试用期及业务参数。
///
[SugarTable("ob_position", TableDescription = "入职业务岗位")]
[Tenant(SqlSugarConst.DB_DEFAULT)]
@@ -22,6 +22,12 @@ public class ObPosition : BaseEntity
///
public long CompanyId { get; set; }
+ ///
+ /// 所属入职业务部门 Id。
+ ///
+ [SugarColumn(IsNullable = true)]
+ public long? DepartmentId { get; set; }
+
///
/// 岗位业务编码。
///
@@ -51,4 +57,16 @@ public class ObPosition : BaseEntity
/// 业务备注。
///
public string Remark { get; set; }
+
+ ///
+ /// 所属公司名称 (展示用)
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public string CompanyName { get; set; }
+
+ ///
+ /// 所属部门名称 (展示用)
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public string DepartmentName { get; set; }
}
diff --git a/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs b/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
index a4dd855..3c12cbc 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
@@ -1,7 +1,7 @@
-namespace SimpleAdmin.Application;
+namespace SimpleAdmin.Application;
///
-/// 实现入职审核业务能力。
+/// 实现入职任务审核与退回业务能力。
///
public class OnboardingAuditService : IOnboardingAuditService
{
@@ -47,8 +47,17 @@ public class OnboardingAuditService : IOnboardingAuditService
var employee = employeeInfo.Adapt();
employee.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
employee.CompanyId = task.CompanyId;
+ employee.DepartmentId = task.DepartmentId;
employee.PositionId = task.PositionId;
- employee.DepartmentName = employeeInfo.OnboardingDepartment;
+ if (task.DepartmentId.HasValue)
+ {
+ var dept = await _db.Queryable().InSingleAsync(task.DepartmentId.Value);
+ employee.DepartmentName = dept?.DepartmentName ?? employeeInfo.OnboardingDepartment;
+ }
+ else
+ {
+ employee.DepartmentName = employeeInfo.OnboardingDepartment;
+ }
employee.College = employeeInfo.GraduatedSchool;
employee.BirthDate = ParseDate(employeeInfo.Birthday);
employee.ContactAddress = employeeInfo.CurrentAddress;
@@ -68,13 +77,26 @@ public class OnboardingAuditService : IOnboardingAuditService
await _db.Ado.BeginTranAsync();
try
{
+ var updatedRows = await _db.Updateable()
+ .SetColumns(item => new ObOnboardingTask
+ {
+ EmployeeId = employee.Id,
+ TaskStatus = OnboardingTaskStatus.Approved,
+ AuditedAt = DateTime.Now
+ })
+ .Where(item => item.Id == task.Id
+ && item.TaskStatus == OnboardingTaskStatus.Reviewing
+ && item.EmployeeId == null)
+ .ExecuteCommandAsync();
+ if (updatedRows != 1)
+ throw Oops.Bah("该入职任务已被其他审核人处理,请刷新后查看");
+
await _snapshotService.Build(task.Id, employeeInfo);
await _db.Insertable(employee).ExecuteCommandAsync();
task.EmployeeId = employee.Id;
task.TaskStatus = OnboardingTaskStatus.Approved;
task.AuditedAt = DateTime.Now;
- await _db.Updateable(task).ExecuteCommandAsync();
await WriteAudit(task, OnboardingAuditAction.Approve, input.AuditOpinion);
await _db.Ado.CommitTranAsync();
}
@@ -96,10 +118,34 @@ public class OnboardingAuditService : IOnboardingAuditService
}
var task = await GetAuditableTask(input.TaskId);
- task.TaskStatus = OnboardingTaskStatus.Rejected;
- task.RejectReason = input.AuditOpinion;
- await _db.Updateable(task).ExecuteCommandAsync();
- await WriteAudit(task, OnboardingAuditAction.Reject, input.AuditOpinion);
+ await _db.Ado.BeginTranAsync();
+ try
+ {
+ var auditedAt = DateTime.Now;
+ var updatedRows = await _db.Updateable()
+ .SetColumns(item => new ObOnboardingTask
+ {
+ TaskStatus = OnboardingTaskStatus.Rejected,
+ AuditedAt = auditedAt,
+ RejectReason = input.AuditOpinion
+ })
+ .Where(item => item.Id == task.Id
+ && item.TaskStatus == OnboardingTaskStatus.Reviewing)
+ .ExecuteCommandAsync();
+ if (updatedRows != 1)
+ throw Oops.Bah("该入职任务已被其他审核人处理,请刷新后查看");
+
+ task.TaskStatus = OnboardingTaskStatus.Rejected;
+ task.AuditedAt = auditedAt;
+ task.RejectReason = input.AuditOpinion;
+ await WriteAudit(task, OnboardingAuditAction.Reject, input.AuditOpinion);
+ await _db.Ado.CommitTranAsync();
+ }
+ catch
+ {
+ await _db.Ado.RollbackTranAsync();
+ throw;
+ }
}
private async Task GetAuditableTask(long taskId)
@@ -113,19 +159,8 @@ public class OnboardingAuditService : IOnboardingAuditService
var config = await _config.GetByConfigKey(CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.reviewer.userIds")
?? throw Oops.Bah("请配置入职审核人");
- using var configJson = global::System.Text.Json.JsonDocument.Parse(config.ConfigValue);
// 审核人由全局配置与公司专属配置合并得出,不依赖独立业务表。
- var reviewerIds = new List();
- if (configJson.RootElement.TryGetProperty("all", out var allReviewers))
- {
- reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64()));
- }
-
- if (configJson.RootElement.TryGetProperty("company", out var companyReviewers)
- && companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers))
- {
- reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64()));
- }
+ var reviewerIds = OnboardingReviewerConfigParser.Parse(config.ConfigValue, task.CompanyId);
if (!reviewerIds.Contains(UserManager.UserId))
{
diff --git a/SimpleAdmin.Application/Services/Onboarding/Common/OnboardingReviewerConfigParser.cs b/SimpleAdmin.Application/Services/Onboarding/Common/OnboardingReviewerConfigParser.cs
new file mode 100644
index 0000000..2fe38af
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/Common/OnboardingReviewerConfigParser.cs
@@ -0,0 +1,64 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 解析入职审核人系统配置,统一支持全局审核人和公司专属审核人。
+///
+internal static class OnboardingReviewerConfigParser
+{
+ private const string ConfigKey = "onboard.reviewer.userIds";
+
+ ///
+ /// 获取指定公司的全部有效审核人用户 Id。
+ ///
+ public static HashSet Parse(string configValue, long companyId)
+ {
+ if (string.IsNullOrWhiteSpace(configValue))
+ throw Oops.Bah($"系统配置 {ConfigKey} 不能为空");
+
+ try
+ {
+ using var document = global::System.Text.Json.JsonDocument.Parse(configValue);
+ if (document.RootElement.ValueKind != global::System.Text.Json.JsonValueKind.Object)
+ throw new global::System.Text.Json.JsonException();
+
+ var reviewerIds = new HashSet();
+ if (document.RootElement.TryGetProperty("all", out var allReviewers))
+ AddReviewers(allReviewers, reviewerIds);
+
+ if (document.RootElement.TryGetProperty("company", out var companyReviewers))
+ {
+ if (companyReviewers.ValueKind != global::System.Text.Json.JsonValueKind.Object)
+ throw new global::System.Text.Json.JsonException();
+
+ if (companyReviewers.TryGetProperty(companyId.ToString(), out var reviewers))
+ AddReviewers(reviewers, reviewerIds);
+ }
+
+ return reviewerIds;
+ }
+ catch (global::System.Text.Json.JsonException)
+ {
+ throw Oops.Bah(
+ $"系统配置 {ConfigKey} 格式不正确,应为包含 all 数组和 company 对象的 JSON");
+ }
+ }
+
+ ///
+ /// 将 JSON 数组中的审核人 Id 加入集合,并拒绝非整数配置。
+ ///
+ private static void AddReviewers(
+ global::System.Text.Json.JsonElement element,
+ HashSet reviewerIds)
+ {
+ if (element.ValueKind != global::System.Text.Json.JsonValueKind.Array)
+ throw new global::System.Text.Json.JsonException();
+
+ foreach (var reviewer in element.EnumerateArray())
+ {
+ if (!reviewer.TryGetInt64(out var reviewerId) || reviewerId <= 0)
+ throw new global::System.Text.Json.JsonException();
+
+ reviewerIds.Add(reviewerId);
+ }
+ }
+}
diff --git a/SimpleAdmin.Application/Services/Onboarding/Company/OnboardingCompanyService.cs b/SimpleAdmin.Application/Services/Onboarding/Company/OnboardingCompanyService.cs
index 16bca1b..9c41a7c 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Company/OnboardingCompanyService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Company/OnboardingCompanyService.cs
@@ -70,6 +70,21 @@ public class OnboardingCompanyService : DbRepository, IOnboardingComp
if (hasPositions)
throw Oops.Bah("公司下仍有岗位,不能删除");
+ var hasDepartments = await Context.Queryable()
+ .AnyAsync(item => input.Ids.Contains(item.CompanyId));
+ if (hasDepartments)
+ throw Oops.Bah("公司下仍有部门,不能删除");
+
+ var hasOnboardingTemplates = await Context.Queryable()
+ .AnyAsync(item => input.Ids.Contains(item.CompanyId));
+ if (hasOnboardingTemplates)
+ throw Oops.Bah("公司已被入职模板引用,不能删除");
+
+ var hasContractTemplates = await Context.Queryable()
+ .AnyAsync(item => item.CompanyId.HasValue && input.Ids.Contains(item.CompanyId.Value));
+ if (hasContractTemplates)
+ throw Oops.Bah("公司已被合同模板引用,不能删除");
+
var hasTasks = await Context.Queryable()
.AnyAsync(it => input.Ids.Contains(it.CompanyId));
if (hasTasks)
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs
index 4adc6a1..91cc483 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs
@@ -42,6 +42,15 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
var variables = await _db.Queryable()
.Where(item => item.TaskContractId == contract.Id)
.ToListAsync();
+ var duplicateVariable = variables
+ .GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
+ .FirstOrDefault(group => group.Count() > 1);
+ if (duplicateVariable != null)
+ {
+ throw Oops.Bah(
+ $"合同变量快照“{duplicateVariable.Key}”存在重复数据,请联系管理员修复后重试");
+ }
+
var missing = definitions
.Where(item => item.Required)
.FirstOrDefault(definition => string.IsNullOrWhiteSpace(
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs
index a4611f6..dc37328 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs
@@ -136,7 +136,10 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
if (!string.IsNullOrWhiteSpace(str)) return str;
}
}
- catch { }
+ catch (global::System.Text.Json.JsonException)
+ {
+ throw Oops.Bah("员工补充变量数据格式异常,请重新保存入职登记表后再生成合同");
+ }
}
return dynamicValues.TryGetValue(sourceField, out var dynamicValue)
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractTemplateInput.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractTemplateInput.cs
index dec4d89..29a5a1b 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractTemplateInput.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractTemplateInput.cs
@@ -5,6 +5,21 @@ namespace SimpleAdmin.Application;
///
public class OnboardingContractTemplatePageInput : BasePageInput
{
+ ///
+ /// 所属公司 Id。
+ ///
+ public long? CompanyId { get; set; }
+
+ ///
+ /// 所属部门 Id。
+ ///
+ public long? DepartmentId { get; set; }
+
+ ///
+ /// 适用岗位 Id。
+ ///
+ public long? PositionId { get; set; }
+
///
/// 合同模板名称。
///
@@ -26,6 +41,23 @@ public class OnboardingContractTemplatePageInput : BasePageInput
///
public class OnboardingContractTemplateAddInput
{
+ ///
+ /// 所属公司 Id。
+ ///
+ [IdNotNull(ErrorMessage = "所属公司不能为空")]
+ public long CompanyId { get; set; }
+
+ ///
+ /// 所属部门 Id。
+ ///
+ [IdNotNull(ErrorMessage = "所属部门不能为空")]
+ public long DepartmentId { get; set; }
+
+ ///
+ /// 合同模板适用的岗位 Id 列表。
+ ///
+ public List PositionIds { get; set; } = new();
+
///
/// 合同模板业务编码。
///
@@ -66,3 +98,27 @@ public class OnboardingContractTemplateEditInput : OnboardingContractTemplateAdd
[IdNotNull(ErrorMessage = "模板Id不能为空")]
public long Id { get; set; }
}
+
+///
+/// 查询公司、部门及岗位可选合同模板参数。
+///
+public class OnboardingContractTemplateAvailableInput
+{
+ ///
+ /// 所属公司 Id。
+ ///
+ [IdNotNull(ErrorMessage = "所属公司不能为空")]
+ public long CompanyId { get; set; }
+
+ ///
+ /// 所属部门 Id。
+ ///
+ [IdNotNull(ErrorMessage = "所属部门不能为空")]
+ public long DepartmentId { get; set; }
+
+ ///
+ /// 入职模板已选择的岗位 Id 列表。
+ /// 返回结果必须同时适用于这些岗位。
+ ///
+ public List PositionIds { get; set; } = new();
+}
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/IOnboardingContractTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/IOnboardingContractTemplateService.cs
index 0b457b7..87a2f76 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/IOnboardingContractTemplateService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/IOnboardingContractTemplateService.cs
@@ -15,6 +15,11 @@ public interface IOnboardingContractTemplateService : ITransient
///
Task Detail(BaseIdInput input);
+ ///
+ /// 查询同时适用于指定公司、部门和全部已选岗位的启用合同模板。
+ ///
+ Task> ListAvailable(OnboardingContractTemplateAvailableInput input);
+
///
/// 新增合同模板主档案。
///
@@ -59,4 +64,4 @@ public interface IOnboardingContractTemplateService : ITransient
/// 编辑并确认模板版本中的变量定义。
///
Task EditVariable(OnboardingTemplateVariableEditInput input);
-}
\ No newline at end of file
+}
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs
index 04380ab..4bf1b99 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs
@@ -17,15 +17,30 @@ public class OnboardingContractTemplateService : DbRepository
public async Task> Page(OnboardingContractTemplatePageInput input)
{
- return await Context.Queryable()
+ var query = Context.Queryable()
+ .WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
+ .WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
.WhereIF(
!string.IsNullOrWhiteSpace(input.TemplateName),
it => it.TemplateName.Contains(input.TemplateName) || it.TemplateCode.Contains(input.TemplateName))
.WhereIF(!string.IsNullOrWhiteSpace(input.ContractType), it => it.ContractType == input.ContractType)
- .WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
+ .WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status);
+
+ if (input.PositionId.HasValue)
+ {
+ var templateIds = await Context.Queryable()
+ .Where(item => item.PositionId == input.PositionId.Value)
+ .Select(item => item.ContractTemplateId)
+ .ToListAsync();
+ query = query.Where(item => templateIds.Contains(item.Id));
+ }
+
+ var page = await query
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
.OrderBy(it => it.TemplateName)
.ToPagedListAsync(input.PageNum, input.PageSize);
+ await PopulateScopeDisplay(page.List.ToList());
+ return page;
}
///
@@ -33,7 +48,47 @@ public class OnboardingContractTemplateService : DbRepository
public async Task Detail(BaseIdInput input)
{
- return await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在");
+ var template = await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在");
+ await PopulateScopeDisplay(new List { template });
+ return template;
+ }
+
+ ///
+ /// 查询同时覆盖全部已选岗位的启用合同模板。
+ ///
+ public async Task> ListAvailable(
+ OnboardingContractTemplateAvailableInput input)
+ {
+ input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List();
+ if (input.PositionIds.Count == 0)
+ return new List();
+
+ await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
+
+ var candidates = await Context.Queryable()
+ .Where(item => item.CompanyId == input.CompanyId
+ && item.DepartmentId == input.DepartmentId
+ && item.Status == "enable")
+ .OrderBy(item => item.TemplateName)
+ .ToListAsync();
+ if (candidates.Count == 0)
+ return candidates;
+
+ var candidateIds = candidates.Select(item => item.Id).ToList();
+ var relations = await Context.Queryable()
+ .Where(item => candidateIds.Contains(item.ContractTemplateId))
+ .ToListAsync();
+ var availableIds = relations
+ .GroupBy(item => item.ContractTemplateId)
+ .Where(group => input.PositionIds.All(positionId =>
+ group.Any(item => item.PositionId == positionId)))
+ .Select(group => group.Key)
+ .ToHashSet();
+ var availableTemplates = candidates
+ .Where(item => availableIds.Contains(item.Id))
+ .ToList();
+ await PopulateScopeDisplay(availableTemplates);
+ return availableTemplates;
}
///
@@ -41,8 +96,24 @@ public class OnboardingContractTemplateService : DbRepository
public async Task Add(OnboardingContractTemplateAddInput input)
{
+ NormalizeInput(input);
+ await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
await EnsureCodeUnique(input.TemplateCode, 0);
- await InsertAsync(input.Adapt());
+
+ var templateId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
+ var entity = BuildEntity(templateId, input);
+ await Context.Ado.BeginTranAsync();
+ try
+ {
+ await Context.Insertable(entity).ExecuteCommandAsync();
+ await InsertPositionRelations(templateId, input.PositionIds);
+ await Context.Ado.CommitTranAsync();
+ }
+ catch
+ {
+ await Context.Ado.RollbackTranAsync();
+ throw;
+ }
}
///
@@ -51,8 +122,29 @@ public class OnboardingContractTemplateService : DbRepository());
+
+ var entity = BuildEntity(input.Id, input);
+ await Context.Ado.BeginTranAsync();
+ try
+ {
+ await Context.Updateable(entity)
+ .IgnoreColumns(ignoreAllNullColumns: true)
+ .ExecuteCommandAsync();
+ await Context.Deleteable()
+ .Where(item => item.ContractTemplateId == input.Id)
+ .ExecuteCommandAsync();
+ await InsertPositionRelations(input.Id, input.PositionIds);
+ await Context.Ado.CommitTranAsync();
+ }
+ catch
+ {
+ await Context.Ado.RollbackTranAsync();
+ throw;
+ }
}
///
@@ -95,6 +187,10 @@ public class OnboardingContractTemplateService : DbRepository input.Ids.Contains(it.TemplateId))
.ExecuteCommandAsync();
+ await Context.Deleteable()
+ .Where(item => input.Ids.Contains(item.ContractTemplateId))
+ .ExecuteCommandAsync();
+
await DeleteByIdsAsync(input.Ids.Cast
+ private static void NormalizeInput(OnboardingContractTemplateAddInput input)
+ {
+ input.TemplateCode = input.TemplateCode?.Trim();
+ input.TemplateName = input.TemplateName?.Trim();
+ input.ContractType = input.ContractType?.Trim();
+ input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List();
+
+ if (string.IsNullOrWhiteSpace(input.TemplateCode)
+ || string.IsNullOrWhiteSpace(input.TemplateName)
+ || string.IsNullOrWhiteSpace(input.ContractType))
+ {
+ throw Oops.Bah("请填写模板编码、模板名称和合同类型");
+ }
+
+ if (input.PositionIds.Count == 0)
+ throw Oops.Bah("请至少选择一个适用岗位");
+ if (input.Status != "enable" && input.Status != "disable")
+ throw Oops.Bah("合同模板状态不正确");
+ }
+
+ ///
+ /// 校验公司、部门、岗位属于同一条启用的业务层级。
+ ///
+ private async Task ValidateBusinessScope(
+ long companyId,
+ long departmentId,
+ List positionIds)
+ {
+ var companyEnabled = await Context.Queryable()
+ .AnyAsync(item => item.Id == companyId && item.Status == "enable");
+ if (!companyEnabled)
+ throw Oops.Bah("所属公司不存在或未启用");
+
+ var departmentEnabled = await Context.Queryable()
+ .AnyAsync(item => item.Id == departmentId
+ && item.CompanyId == companyId
+ && item.Status == "enable");
+ if (!departmentEnabled)
+ throw Oops.Bah("所属部门不存在、未启用或不属于该公司");
+
+ var positionCount = await Context.Queryable()
+ .CountAsync(item => positionIds.Contains(item.Id)
+ && item.CompanyId == companyId
+ && item.DepartmentId == departmentId
+ && item.Status == "enable");
+ if (positionCount != positionIds.Count)
+ throw Oops.Bah("存在未启用或不属于该公司、部门的岗位");
+ }
+
+ ///
+ /// 防止修改合同模板归属后破坏已经保存的入职模板配置。
+ ///
+ private async Task ValidateReferencedOnboardingTemplates(
+ OnboardingContractTemplateEditInput input)
+ {
+ var relations = await Context.Queryable()
+ .Where(item => item.ContractTemplateId == input.Id)
+ .ToListAsync();
+ if (relations.Count == 0)
+ return;
+
+ var onboardingTemplateIds = relations
+ .Select(item => item.OnboardingTemplateId)
+ .Distinct()
+ .ToList();
+ var onboardingTemplates = await Context.Queryable()
+ .Where(item => onboardingTemplateIds.Contains(item.Id))
+ .ToListAsync();
+ var positionRelations = await Context.Queryable()
+ .Where(item => onboardingTemplateIds.Contains(item.OnboardingTemplateId))
+ .ToListAsync();
+
+ foreach (var template in onboardingTemplates)
+ {
+ var positionIds = positionRelations
+ .Where(item => item.OnboardingTemplateId == template.Id)
+ .Select(item => item.PositionId)
+ .Distinct()
+ .ToList();
+ var departmentMatches = !template.DepartmentId.HasValue
+ || template.DepartmentId.Value == input.DepartmentId;
+ var scopeMatches = template.CompanyId == input.CompanyId
+ && departmentMatches
+ && positionIds.All(input.PositionIds.Contains);
+ if (!scopeMatches)
+ {
+ throw Oops.Bah(
+ $"合同模板仍被入职模板“{template.TemplateName}”引用,当前归属调整会使其失效,请先调整入职模板");
+ }
+ }
+ }
+
+ ///
+ /// 构建合同模板主档案实体。
+ ///
+ private static ObContractTemplate BuildEntity(
+ long templateId,
+ OnboardingContractTemplateAddInput input)
+ {
+ return new ObContractTemplate
+ {
+ Id = templateId,
+ CompanyId = input.CompanyId,
+ DepartmentId = input.DepartmentId,
+ TemplateCode = input.TemplateCode,
+ TemplateName = input.TemplateName,
+ ContractType = input.ContractType,
+ Description = input.Description,
+ Status = input.Status
+ };
+ }
+
+ ///
+ /// 保存合同模板适用岗位关系。
+ ///
+ private async Task InsertPositionRelations(long templateId, List positionIds)
+ {
+ var relations = positionIds.Select(positionId => new ObContractTemplatePosition
+ {
+ Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
+ ContractTemplateId = templateId,
+ PositionId = positionId
+ }).ToList();
+ await Context.Insertable(relations).ExecuteCommandAsync();
+ }
+
+ ///
+ /// 回填合同模板的公司、部门和适用岗位展示信息。
+ ///
+ private async Task PopulateScopeDisplay(List templates)
+ {
+ if (templates.Count == 0)
+ return;
+
+ var templateIds = templates.Select(item => item.Id).ToList();
+ var companyIds = templates
+ .Where(item => item.CompanyId.HasValue)
+ .Select(item => item.CompanyId.Value)
+ .Distinct()
+ .ToList();
+ var departmentIds = templates
+ .Where(item => item.DepartmentId.HasValue)
+ .Select(item => item.DepartmentId.Value)
+ .Distinct()
+ .ToList();
+ var companies = companyIds.Count == 0
+ ? new List()
+ : await Context.Queryable()
+ .Where(item => companyIds.Contains(item.Id))
+ .ToListAsync();
+ var departments = departmentIds.Count == 0
+ ? new List()
+ : await Context.Queryable()
+ .Where(item => departmentIds.Contains(item.Id))
+ .ToListAsync();
+ var relations = await Context.Queryable()
+ .Where(item => templateIds.Contains(item.ContractTemplateId))
+ .ToListAsync();
+ var positionIds = relations.Select(item => item.PositionId).Distinct().ToList();
+ var positions = positionIds.Count == 0
+ ? new List()
+ : await Context.Queryable()
+ .Where(item => positionIds.Contains(item.Id))
+ .ToListAsync();
+
+ foreach (var template in templates)
+ {
+ template.CompanyName = companies
+ .FirstOrDefault(item => item.Id == template.CompanyId)?.CompanyName;
+ template.DepartmentName = departments
+ .FirstOrDefault(item => item.Id == template.DepartmentId)?.DepartmentName;
+ template.PositionIds = relations
+ .Where(item => item.ContractTemplateId == template.Id)
+ .Select(item => item.PositionId)
+ .ToList();
+ template.PositionNames = template.PositionIds
+ .Select(positionId => positions.FirstOrDefault(item => item.Id == positionId)?.PositionName)
+ .Where(name => !string.IsNullOrWhiteSpace(name))
+ .ToList();
+ }
+ }
}
diff --git a/SimpleAdmin.Application/Services/Onboarding/Department/Dto/OnboardingDepartmentInput.cs b/SimpleAdmin.Application/Services/Onboarding/Department/Dto/OnboardingDepartmentInput.cs
new file mode 100644
index 0000000..378d0bf
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/Department/Dto/OnboardingDepartmentInput.cs
@@ -0,0 +1,63 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职业务部门分页查询参数。
+///
+public class OnboardingDepartmentPageInput : BasePageInput
+{
+ ///
+ /// 入职业务公司 Id。
+ ///
+ public long? CompanyId { get; set; }
+
+ ///
+ /// 部门编码。
+ ///
+ public virtual string DepartmentCode { get; set; }
+
+ ///
+ /// 部门名称。
+ ///
+ public virtual string DepartmentName { get; set; }
+
+ ///
+ /// 启用状态。
+ ///
+ public string Status { get; set; }
+}
+
+///
+/// 新增入职业务部门参数。
+///
+public class OnboardingDepartmentAddInput : ObDepartment
+{
+ ///
+ /// 所属公司 Id。
+ ///
+ [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属公司不能为空")]
+ public new long CompanyId { get; set; }
+
+ ///
+ /// 部门业务编码。
+ ///
+ [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "部门编码不能为空")]
+ public override string DepartmentCode { get; set; }
+
+ ///
+ /// 部门名称。
+ ///
+ [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "部门名称不能为空")]
+ public override string DepartmentName { get; set; }
+}
+
+///
+/// 编辑入职业务部门参数。
+///
+public class OnboardingDepartmentEditInput : OnboardingDepartmentAddInput
+{
+ ///
+ /// 业务数据 Id。
+ ///
+ [IdNotNull(ErrorMessage = "部门Id不能为空")]
+ public override long Id { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/Department/IOnboardingDepartmentService.cs b/SimpleAdmin.Application/Services/Onboarding/Department/IOnboardingDepartmentService.cs
new file mode 100644
index 0000000..fef85d1
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/Department/IOnboardingDepartmentService.cs
@@ -0,0 +1,37 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 定义入职业务部门业务能力。
+///
+public interface IOnboardingDepartmentService : ITransient
+{
+ ///
+ /// 分页查询入职业务部门。
+ ///
+ Task> Page(OnboardingDepartmentPageInput input);
+
+ ///
+ /// 查询指定公司下全部已启用部门列表。
+ ///
+ Task> ListEnabled(long companyId);
+
+ ///
+ /// 查询部门详情。
+ ///
+ Task Detail(BaseIdInput input);
+
+ ///
+ /// 新增入职业务部门。
+ ///
+ Task Add(OnboardingDepartmentAddInput input);
+
+ ///
+ /// 编辑入职业务部门。
+ ///
+ Task Edit(OnboardingDepartmentEditInput input);
+
+ ///
+ /// 批量删除部门。
+ ///
+ Task Delete(BaseIdListInput input);
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/Department/OnboardingDepartmentService.cs b/SimpleAdmin.Application/Services/Onboarding/Department/OnboardingDepartmentService.cs
new file mode 100644
index 0000000..bbb935d
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/Department/OnboardingDepartmentService.cs
@@ -0,0 +1,177 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 实现入职业务部门业务能力。
+///
+public class OnboardingDepartmentService : DbRepository, IOnboardingDepartmentService
+{
+ ///
+ /// 分页查询入职业务部门。
+ ///
+ public async Task> Page(OnboardingDepartmentPageInput input)
+ {
+ var list = await Context.Queryable()
+ .WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value)
+ .WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentCode), it => it.DepartmentCode.Contains(input.DepartmentCode))
+ .WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentName), it => it.DepartmentName.Contains(input.DepartmentName))
+ .WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
+ .OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
+ .OrderBy(it => it.SortCode)
+ .OrderBy(it => it.DepartmentName)
+ .ToPagedListAsync(input.PageNum, input.PageSize);
+
+ if (list.List != null && list.List.Any())
+ {
+ var companyIds = list.List.Select(d => d.CompanyId).Distinct().ToList();
+ var companies = await Context.Queryable()
+ .Where(c => companyIds.Contains(c.Id))
+ .ToListAsync();
+ var companyMap = companies.ToDictionary(c => c.Id, c => c.CompanyName);
+
+ foreach (var item in list.List)
+ {
+ if (companyMap.TryGetValue(item.CompanyId, out var cName))
+ {
+ item.CompanyName = cName;
+ }
+ }
+ }
+
+ return list;
+ }
+
+ ///
+ /// 查询指定公司下全部已启用部门列表。
+ ///
+ public Task> ListEnabled(long companyId)
+ {
+ return Context.Queryable()
+ .Where(it => it.CompanyId == companyId && it.Status == "enable")
+ .OrderBy(it => it.SortCode)
+ .OrderBy(it => it.DepartmentName)
+ .ToListAsync();
+ }
+
+ ///
+ /// 查询部门详情。
+ ///
+ public async Task Detail(BaseIdInput input)
+ {
+ return await GetByIdAsync(input.Id) ?? throw Oops.Bah("部门不存在");
+ }
+
+ ///
+ /// 新增入职业务部门。
+ ///
+ public async Task Add(OnboardingDepartmentAddInput input)
+ {
+ NormalizeInput(input);
+ await EnsureCompanyExists(input.CompanyId);
+ await EnsureCodeUnique(input.CompanyId, input.DepartmentCode, 0);
+ await InsertAsync(input.Adapt());
+ }
+
+ ///
+ /// 编辑入职业务部门。
+ ///
+ public async Task Edit(OnboardingDepartmentEditInput input)
+ {
+ NormalizeInput(input);
+ var department = await Detail(new BaseIdInput { Id = input.Id });
+ await EnsureCompanyCanChange(department, input.CompanyId);
+ await EnsureCompanyExists(input.CompanyId);
+ await EnsureCodeUnique(input.CompanyId, input.DepartmentCode, input.Id);
+ await UpdateAsync(input.Adapt());
+ }
+
+ ///
+ /// 删除未被岗位或任务引用的部门。
+ ///
+ public async Task Delete(BaseIdListInput input)
+ {
+ if (input.Ids.Count == 0)
+ return;
+
+ var hasPositions = await Context.Queryable()
+ .AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
+ if (hasPositions)
+ throw Oops.Bah("部门下已有岗位,不能删除");
+
+ var hasTasks = await Context.Queryable()
+ .AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
+ if (hasTasks)
+ throw Oops.Bah("部门已有入职任务,不能删除");
+
+ var hasEmployees = await Context.Queryable()
+ .AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
+ if (hasEmployees)
+ throw Oops.Bah("部门已有正式员工,不能删除");
+
+ var hasOnboardingTemplates = await Context.Queryable()
+ .AnyAsync(item => item.DepartmentId.HasValue && input.Ids.Contains(item.DepartmentId.Value));
+ if (hasOnboardingTemplates)
+ throw Oops.Bah("部门已被入职模板引用,不能删除");
+
+ var hasContractTemplates = await Context.Queryable()
+ .AnyAsync(item => item.DepartmentId.HasValue && input.Ids.Contains(item.DepartmentId.Value));
+ if (hasContractTemplates)
+ throw Oops.Bah("部门已被合同模板引用,不能删除");
+
+ await DeleteByIdsAsync(input.Ids.Cast
public string CompanyName { get; set; }
+ ///
+ /// 所属部门名称。
+ ///
+ public string DepartmentName { get; set; }
+
///
/// 适用岗位 Id 列表。
///
@@ -210,4 +226,4 @@ public class OnboardingTemplateDetailOutput : OnboardingTemplateOutput
/// 上传材料配置明细。
///
public List Materials { get; set; } = new();
-}
\ No newline at end of file
+}
diff --git a/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/IOnboardingTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/IOnboardingTemplateService.cs
index 24f9c4a..655dd73 100644
--- a/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/IOnboardingTemplateService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/IOnboardingTemplateService.cs
@@ -26,7 +26,10 @@ public interface IOnboardingTemplateService : ITransient
Task Delete(BaseIdListInput input);
///
- /// 查询公司岗位可使用的入职模板。
+ /// 查询公司、部门和岗位可使用的入职模板。
///
- Task> ListEnabled(long companyId, long positionId);
-}
\ No newline at end of file
+ Task> ListEnabled(
+ long companyId,
+ long departmentId,
+ long positionId);
+}
diff --git a/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/OnboardingTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/OnboardingTemplateService.cs
index d332d2a..3f2ca1b 100644
--- a/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/OnboardingTemplateService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/OnboardingTemplateService.cs
@@ -20,6 +20,7 @@ public class OnboardingTemplateService : IOnboardingTemplateService
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword),
item => item.TemplateCode.Contains(input.Keyword) || item.TemplateName.Contains(input.Keyword))
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
+ .WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), item => item.Status == input.Status);
if (input.PositionId.HasValue)
@@ -62,6 +63,8 @@ public class OnboardingTemplateService : IOnboardingTemplateService
TemplateName = summary.TemplateName,
CompanyId = summary.CompanyId,
CompanyName = summary.CompanyName,
+ DepartmentId = summary.DepartmentId,
+ DepartmentName = summary.DepartmentName,
Description = summary.Description,
Status = summary.Status,
CreateTime = summary.CreateTime,
@@ -101,6 +104,7 @@ public class OnboardingTemplateService : IOnboardingTemplateService
TemplateCode = input.TemplateCode.Trim(),
TemplateName = input.TemplateName.Trim(),
CompanyId = input.CompanyId,
+ DepartmentId = input.DepartmentId,
Description = input.Description,
Status = input.Status
};
@@ -149,9 +153,6 @@ public class OnboardingTemplateService : IOnboardingTemplateService
foreach (var id in input.Ids)
await DeleteRelations(id);
- await _db.Deleteable()
- .Where(it => input.Ids.Contains(it.Id))
- .ExecuteCommandAsync();
await _db.Updateable()
.SetColumns(it => it.IsDelete == true)
.Where(it => input.Ids.Contains(it.Id))
@@ -167,7 +168,10 @@ public class OnboardingTemplateService : IOnboardingTemplateService
}
///
- public async Task> ListEnabled(long companyId, long positionId)
+ public async Task> ListEnabled(
+ long companyId,
+ long departmentId,
+ long positionId)
{
var templateIds = await _db.Queryable()
.Where(item => item.PositionId == positionId)
@@ -175,6 +179,7 @@ public class OnboardingTemplateService : IOnboardingTemplateService
.ToListAsync();
var templates = await _db.Queryable()
.Where(item => item.CompanyId == companyId
+ && item.DepartmentId == departmentId
&& item.Status == "enable"
&& templateIds.Contains(item.Id))
.OrderBy(item => item.TemplateName)
@@ -187,10 +192,13 @@ public class OnboardingTemplateService : IOnboardingTemplateService
input.TemplateCode = input.TemplateCode?.Trim();
input.TemplateName = input.TemplateName?.Trim();
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List();
- input.Contracts = input.Contracts?
- .GroupBy(item => item.ContractTemplateVersionId)
- .Select(group => group.First())
- .ToList() ?? new List();
+ input.Contracts ??= new List();
+ var duplicateContract = input.Contracts
+ .GroupBy(item => item.ContractTemplateId)
+ .FirstOrDefault(group => group.Count() > 1);
+ if (duplicateContract != null)
+ throw Oops.Bah("同一份合同模板在入职模板中只能选择一个版本");
+
input.FormFields = input.FormFields?
.GroupBy(item => item.FieldId)
.Select(group => group.First())
@@ -204,10 +212,19 @@ public class OnboardingTemplateService : IOnboardingTemplateService
throw Oops.Bah("请填写入职模板编码和名称");
if (input.CompanyId <= 0)
throw Oops.Bah("请选择所属公司");
+ if (input.DepartmentId <= 0)
+ throw Oops.Bah("请选择所属部门");
if (input.PositionIds.Count == 0)
throw Oops.Bah("请至少选择一个适用岗位");
+ if (input.PositionIds.Any(positionId => positionId <= 0))
+ throw Oops.Bah("适用岗位数据不正确");
if (input.Contracts.Count == 0)
throw Oops.Bah("请至少选择一份合同文件");
+ if (input.Contracts.Any(item => item.ContractTemplateId <= 0
+ || item.ContractTemplateVersionId <= 0))
+ {
+ throw Oops.Bah("合同模板或版本数据不正确");
+ }
if (input.Status != "enable" && input.Status != "disable")
throw Oops.Bah("入职模板状态不正确");
if (input.Contracts.Any(item => item.PrintCopies <= 0))
@@ -221,12 +238,20 @@ public class OnboardingTemplateService : IOnboardingTemplateService
if (!companyEnabled)
throw Oops.Bah("所属公司不存在或未启用");
+ var departmentEnabled = await _db.Queryable()
+ .AnyAsync(item => item.Id == input.DepartmentId
+ && item.CompanyId == input.CompanyId
+ && item.Status == "enable");
+ if (!departmentEnabled)
+ throw Oops.Bah("所属部门不存在、未启用或不属于该公司");
+
var positionCount = await _db.Queryable()
.CountAsync(item => input.PositionIds.Contains(item.Id)
&& item.CompanyId == input.CompanyId
+ && item.DepartmentId == input.DepartmentId
&& item.Status == "enable");
if (positionCount != input.PositionIds.Count)
- throw Oops.Bah("存在不属于该公司或未启用的岗位");
+ throw Oops.Bah("存在未启用或不属于该公司、部门的岗位");
var versionIds = input.Contracts.Select(item => item.ContractTemplateVersionId).ToList();
var versions = await _db.Queryable()
@@ -239,8 +264,43 @@ public class OnboardingTemplateService : IOnboardingTemplateService
throw Oops.Bah("合同模板与版本不匹配");
}
+ var contractTemplateIds = input.Contracts
+ .Select(item => item.ContractTemplateId)
+ .Distinct()
+ .ToList();
+ var contractTemplates = await _db.Queryable()
+ .Where(item => contractTemplateIds.Contains(item.Id))
+ .ToListAsync();
+ if (contractTemplates.Count != contractTemplateIds.Count
+ || contractTemplates.Any(item => item.CompanyId != input.CompanyId
+ || item.DepartmentId != input.DepartmentId))
+ {
+ throw Oops.Bah("存在不属于当前公司、部门的合同模板");
+ }
+
+ var contractPositionRelations = await _db.Queryable()
+ .Where(item => contractTemplateIds.Contains(item.ContractTemplateId))
+ .ToListAsync();
+ var unsupportedContract = contractTemplateIds.FirstOrDefault(contractTemplateId =>
+ {
+ var supportedPositionIds = contractPositionRelations
+ .Where(item => item.ContractTemplateId == contractTemplateId)
+ .Select(item => item.PositionId)
+ .ToHashSet();
+ return input.PositionIds.Any(positionId => !supportedPositionIds.Contains(positionId));
+ });
+ if (unsupportedContract > 0)
+ {
+ var contractName = contractTemplates
+ .First(item => item.Id == unsupportedContract)
+ .TemplateName;
+ throw Oops.Bah($"合同模板“{contractName}”不能覆盖当前选择的全部岗位");
+ }
+
if (input.Status == "enable")
{
+ if (contractTemplates.Any(item => item.Status != "enable"))
+ throw Oops.Bah("启用入职模板前,请先启用全部合同模板");
if (versions.Any(item => item.VersionStatus != ContractTemplateVersionStatus.Enabled))
throw Oops.Bah("启用入职模板前,请先启用全部合同版本");
}
@@ -302,6 +362,9 @@ public class OnboardingTemplateService : IOnboardingTemplateService
await _db.Deleteable()
.Where(item => item.OnboardingTemplateId == templateId)
.ExecuteCommandAsync();
+ await _db.Deleteable()
+ .Where(item => item.OnboardingTemplateId == templateId)
+ .ExecuteCommandAsync();
}
private async Task> BuildOutputs(List templates)
@@ -311,9 +374,19 @@ public class OnboardingTemplateService : IOnboardingTemplateService
var templateIds = templates.Select(item => item.Id).ToList();
var companyIds = templates.Select(item => item.CompanyId).Distinct().ToList();
+ var departmentIds = templates
+ .Where(item => item.DepartmentId.HasValue)
+ .Select(item => item.DepartmentId.Value)
+ .Distinct()
+ .ToList();
var companies = await _db.Queryable()
.Where(item => companyIds.Contains(item.Id))
.ToListAsync();
+ var departments = departmentIds.Count == 0
+ ? new List()
+ : await _db.Queryable()
+ .Where(item => departmentIds.Contains(item.Id))
+ .ToListAsync();
var relations = await _db.Queryable()
.Where(item => templateIds.Contains(item.OnboardingTemplateId))
.ToListAsync();
@@ -338,6 +411,9 @@ public class OnboardingTemplateService : IOnboardingTemplateService
TemplateName = template.TemplateName,
CompanyId = template.CompanyId,
CompanyName = companies.FirstOrDefault(item => item.Id == template.CompanyId)?.CompanyName,
+ DepartmentId = template.DepartmentId,
+ DepartmentName = departments
+ .FirstOrDefault(item => item.Id == template.DepartmentId)?.DepartmentName,
Description = template.Description,
Status = template.Status,
CreateTime = template.CreateTime,
@@ -353,4 +429,4 @@ public class OnboardingTemplateService : IOnboardingTemplateService
};
}).ToList();
}
-}
\ No newline at end of file
+}
diff --git a/SimpleAdmin.Application/Services/Onboarding/Position/Dto/OnboardingPositionInput.cs b/SimpleAdmin.Application/Services/Onboarding/Position/Dto/OnboardingPositionInput.cs
index 8e4b5e9..5de8f08 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Position/Dto/OnboardingPositionInput.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Position/Dto/OnboardingPositionInput.cs
@@ -1,4 +1,4 @@
-namespace SimpleAdmin.Application;
+namespace SimpleAdmin.Application;
///
/// 入职业务岗位分页查询参数。
@@ -10,6 +10,11 @@ public class OnboardingPositionPageInput : BasePageInput
///
public long? CompanyId { get; set; }
+ ///
+ /// 入职业务部门 Id。
+ ///
+ public long? DepartmentId { get; set; }
+
///
/// 岗位编码。
///
@@ -21,7 +26,7 @@ public class OnboardingPositionPageInput : BasePageInput
public virtual string PositionName { get; set; }
///
- /// 启用状态或状态筛选条件。
+ /// 启用状态。
///
public string Status { get; set; }
}
@@ -31,6 +36,18 @@ public class OnboardingPositionPageInput : BasePageInput
///
public class OnboardingPositionAddInput : ObPosition
{
+ ///
+ /// 所属公司 Id。
+ ///
+ [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属公司不能为空")]
+ public new long CompanyId { get; set; }
+
+ ///
+ /// 所属部门 Id。
+ ///
+ [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属部门不能为空")]
+ public new long? DepartmentId { get; set; }
+
///
/// 岗位业务编码。
///
@@ -54,4 +71,4 @@ public class OnboardingPositionEditInput : OnboardingPositionAddInput
///
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
public override long Id { get; set; }
-}
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/Position/IOnboardingPositionService.cs b/SimpleAdmin.Application/Services/Onboarding/Position/IOnboardingPositionService.cs
index 5c8994d..9c2c09a 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Position/IOnboardingPositionService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Position/IOnboardingPositionService.cs
@@ -1,4 +1,4 @@
-namespace SimpleAdmin.Application;
+namespace SimpleAdmin.Application;
///
/// 定义入职业务岗位业务能力。
@@ -9,24 +9,34 @@ public interface IOnboardingPositionService : ITransient
/// 分页查询入职业务岗位。
///
Task> Page(OnboardingPositionPageInput input);
+
///
/// 查询指定公司下全部已启用岗位。
///
Task> ListEnabled(long companyId);
+
+ ///
+ /// 查询指定部门下全部已启用岗位。
+ ///
+ Task> ListEnabledByDepartment(long departmentId);
+
///
/// 查询岗位详情。
///
Task Detail(BaseIdInput input);
+
///
/// 新增入职业务岗位。
///
Task Add(OnboardingPositionAddInput input);
+
///
/// 编辑入职业务岗位。
///
Task Edit(OnboardingPositionEditInput input);
+
///
/// 删除未被任务或正式员工引用的岗位。
///
Task Delete(BaseIdListInput input);
-}
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/Position/OnboardingPositionService.cs b/SimpleAdmin.Application/Services/Onboarding/Position/OnboardingPositionService.cs
index f42d228..f18b7c4 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Position/OnboardingPositionService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Position/OnboardingPositionService.cs
@@ -1,4 +1,4 @@
-namespace SimpleAdmin.Application;
+namespace SimpleAdmin.Application;
///
/// 实现入职业务岗位业务能力。
@@ -10,14 +10,45 @@ public class OnboardingPositionService : DbRepository, IOnboardingPo
///
public async Task> Page(OnboardingPositionPageInput input)
{
- return await Context.Queryable()
+ var list = await Context.Queryable()
.WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value)
+ .WhereIF(input.DepartmentId.HasValue, it => it.DepartmentId == input.DepartmentId.Value)
.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)
.ToPagedListAsync(input.PageNum, input.PageSize);
+
+ if (list.List != null && list.List.Any())
+ {
+ var companyIds = list.List.Select(p => p.CompanyId).Distinct().ToList();
+ var departmentIds = list.List.Where(p => p.DepartmentId.HasValue).Select(p => p.DepartmentId.Value).Distinct().ToList();
+
+ var companies = await Context.Queryable()
+ .Where(c => companyIds.Contains(c.Id))
+ .ToListAsync();
+ var departments = departmentIds.Count == 0
+ ? new List()
+ : await Context.Queryable().Where(d => departmentIds.Contains(d.Id)).ToListAsync();
+
+ var companyMap = companies.ToDictionary(c => c.Id, c => c.CompanyName);
+ var departmentMap = departments.ToDictionary(d => d.Id, d => d.DepartmentName);
+
+ foreach (var item in list.List)
+ {
+ if (companyMap.TryGetValue(item.CompanyId, out var cName))
+ {
+ item.CompanyName = cName;
+ }
+ if (item.DepartmentId.HasValue && departmentMap.TryGetValue(item.DepartmentId.Value, out var dName))
+ {
+ item.DepartmentName = dName;
+ }
+ }
+ }
+
+ return list;
}
///
@@ -31,6 +62,17 @@ public class OnboardingPositionService : DbRepository, IOnboardingPo
.ToListAsync();
}
+ ///
+ /// 查询指定部门下全部已启用岗位。
+ ///
+ public Task> ListEnabledByDepartment(long departmentId)
+ {
+ return Context.Queryable()
+ .Where(it => it.DepartmentId == departmentId && it.Status == "enable")
+ .OrderBy(it => it.PositionName)
+ .ToListAsync();
+ }
+
///
/// 查询岗位详情。
///
@@ -44,7 +86,12 @@ public class OnboardingPositionService : DbRepository, IOnboardingPo
///
public async Task Add(OnboardingPositionAddInput input)
{
+ NormalizeInput(input);
+ if (!input.DepartmentId.HasValue)
+ throw Oops.Bah("请选择所属部门");
+
await EnsureCompanyExists(input.CompanyId);
+ await EnsureDepartmentExists(input.CompanyId, input.DepartmentId.Value);
await EnsureCodeUnique(input.CompanyId, input.PositionCode, 0);
await InsertAsync(input.Adapt());
}
@@ -54,8 +101,14 @@ public class OnboardingPositionService : DbRepository, IOnboardingPo
///
public async Task Edit(OnboardingPositionEditInput input)
{
- await Detail(new BaseIdInput { Id = input.Id });
+ NormalizeInput(input);
+ if (!input.DepartmentId.HasValue)
+ throw Oops.Bah("请选择所属部门");
+
+ var position = await Detail(new BaseIdInput { Id = input.Id });
+ await EnsureScopeCanChange(position, input.CompanyId, input.DepartmentId.Value);
await EnsureCompanyExists(input.CompanyId);
+ await EnsureDepartmentExists(input.CompanyId, input.DepartmentId.Value);
await EnsureCodeUnique(input.CompanyId, input.PositionCode, input.Id);
await UpdateAsync(input.Adapt());
}
@@ -78,14 +131,75 @@ public class OnboardingPositionService : DbRepository, IOnboardingPo
if (hasEmployees)
throw Oops.Bah("岗位已有正式员工,不能删除");
+ var hasOnboardingTemplates = await Context.Queryable()
+ .AnyAsync(item => input.Ids.Contains(item.PositionId));
+ if (hasOnboardingTemplates)
+ throw Oops.Bah("岗位已被入职模板引用,不能删除");
+
+ var hasContractTemplates = await Context.Queryable()
+ .AnyAsync(item => input.Ids.Contains(item.PositionId));
+ if (hasContractTemplates)
+ throw Oops.Bah("岗位已被合同模板引用,不能删除");
+
await DeleteByIdsAsync(input.Ids.Cast