代码优化

This commit is contained in:
fuenmao
2026-08-27 11:09:20 +08:00
parent 658869b3a7
commit ae8167a566
46 changed files with 2051 additions and 204 deletions
@@ -17,6 +17,18 @@ namespace SimpleAdmin.Application;
[Tenant(SqlSugarConst.DB_DEFAULT)]
public class ObContractTemplate : BaseEntity
{
/// <summary>
/// 合同模板所属入职业务公司 Id。
/// </summary>
[SugarColumn(IsNullable = true)]
public long? CompanyId { get; set; }
/// <summary>
/// 合同模板所属入职业务部门 Id。
/// </summary>
[SugarColumn(IsNullable = true)]
public long? DepartmentId { get; set; }
/// <summary>
/// 合同模板业务编码。
/// </summary>
@@ -36,4 +48,28 @@ public class ObContractTemplate : BaseEntity
/// 模板用途说明。
/// </summary>
public string Description { get; set; }
/// <summary>
/// 所属公司名称,仅用于接口展示。
/// </summary>
[SugarColumn(IsIgnore = true)]
public string CompanyName { get; set; }
/// <summary>
/// 所属部门名称,仅用于接口展示。
/// </summary>
[SugarColumn(IsIgnore = true)]
public string DepartmentName { get; set; }
/// <summary>
/// 适用岗位 Id 列表,仅用于接口展示和编辑回显。
/// </summary>
[SugarColumn(IsIgnore = true)]
public List<long> PositionIds { get; set; } = new();
/// <summary>
/// 适用岗位名称列表,仅用于接口展示。
/// </summary>
[SugarColumn(IsIgnore = true)]
public List<string> PositionNames { get; set; } = new();
}
@@ -0,0 +1,19 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 合同模板适用岗位关系,用于限制合同模板可被哪些岗位的入职模板选择。
/// </summary>
[SugarTable("ob_contract_template_position", TableDescription = "合同模板适用岗位")]
[Tenant(SqlSugarConst.DB_DEFAULT)]
public class ObContractTemplatePosition : BaseEntity
{
/// <summary>
/// 合同模板主档案 Id。
/// </summary>
public long ContractTemplateId { get; set; }
/// <summary>
/// 适用岗位 Id。
/// </summary>
public long PositionId { get; set; }
}
@@ -0,0 +1,40 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 入职业务部门档案,属于某家入职公司。
/// </summary>
[SugarTable("ob_department", TableDescription = "入职业务部门")]
[Tenant(SqlSugarConst.DB_DEFAULT)]
public class ObDepartment : BaseEntity
{
/// <summary>
/// 所属入职业务公司 Id。
/// </summary>
public long CompanyId { get; set; }
/// <summary>
/// 部门业务编码。
/// </summary>
public virtual string DepartmentCode { get; set; }
/// <summary>
/// 部门名称。
/// </summary>
public virtual string DepartmentName { get; set; }
/// <summary>
/// 排序码。
/// </summary>
public int SortCode { get; set; } = 99;
/// <summary>
/// 业务备注。
/// </summary>
public string Remark { get; set; }
/// <summary>
/// 所属公司名称 (展示用)
/// </summary>
[SugarColumn(IsIgnore = true)]
public string CompanyName { get; set; }
}
@@ -37,6 +37,12 @@ public class ObEmployee : BaseEntity
/// </summary>
public long CompanyId { get; set; }
/// <summary>
/// 所属入职业务部门 Id。
/// </summary>
[SugarColumn(IsNullable = true)]
public long? DepartmentId { get; set; }
/// <summary>
/// 所属部门名称。
/// </summary>
@@ -22,6 +22,12 @@ public class ObOnboardingTask : BaseEntity
/// </summary>
public long CompanyId { get; set; }
/// <summary>
/// 所属入职业务部门 Id。
/// </summary>
[SugarColumn(IsNullable = true)]
public long? DepartmentId { get; set; }
/// <summary>
/// 入职任务所属岗位 Id。
/// </summary>
@@ -83,6 +89,12 @@ public class ObOnboardingTask : BaseEntity
[SugarColumn(IsIgnore = true)]
public string CompanyName { get; set; }
/// <summary>
/// 部门名称 (展示用)
/// </summary>
[SugarColumn(IsIgnore = true)]
public string DepartmentName { get; set; }
/// <summary>
/// 岗位名称 (展示用)
/// </summary>
@@ -94,5 +106,4 @@ public class ObOnboardingTask : BaseEntity
/// </summary>
[SugarColumn(IsIgnore = true)]
public string OnboardingTemplateName { get; set; }
}
}
@@ -22,8 +22,14 @@ public class ObOnboardingTemplate : BaseEntity
/// </summary>
public long CompanyId { get; set; }
/// <summary>
/// 所属入职业务部门 Id。
/// </summary>
[SugarColumn(IsNullable = true)]
public long? DepartmentId { get; set; }
/// <summary>
/// 模板用途和适用范围说明。
/// </summary>
public string Description { get; set; }
}
}
@@ -11,7 +11,7 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 入职业务岗位档案,保存岗位默认合同期限、试用期及业务参数。
/// 入职业务岗位档案,保存岗位所属部门、默认合同期限、试用期及业务参数。
/// </summary>
[SugarTable("ob_position", TableDescription = "入职业务岗位")]
[Tenant(SqlSugarConst.DB_DEFAULT)]
@@ -22,6 +22,12 @@ public class ObPosition : BaseEntity
/// </summary>
public long CompanyId { get; set; }
/// <summary>
/// 所属入职业务部门 Id。
/// </summary>
[SugarColumn(IsNullable = true)]
public long? DepartmentId { get; set; }
/// <summary>
/// 岗位业务编码。
/// </summary>
@@ -51,4 +57,16 @@ public class ObPosition : BaseEntity
/// 业务备注。
/// </summary>
public string Remark { get; set; }
/// <summary>
/// 所属公司名称 (展示用)
/// </summary>
[SugarColumn(IsIgnore = true)]
public string CompanyName { get; set; }
/// <summary>
/// 所属部门名称 (展示用)
/// </summary>
[SugarColumn(IsIgnore = true)]
public string DepartmentName { get; set; }
}
@@ -1,7 +1,7 @@
namespace SimpleAdmin.Application;
namespace SimpleAdmin.Application;
/// <summary>
/// 实现入职审核业务能力。
/// 实现入职任务审核与退回业务能力。
/// </summary>
public class OnboardingAuditService : IOnboardingAuditService
{
@@ -47,8 +47,17 @@ public class OnboardingAuditService : IOnboardingAuditService
var employee = employeeInfo.Adapt<ObEmployee>();
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<ObDepartment>().InSingleAsync(task.DepartmentId.Value);
employee.DepartmentName = dept?.DepartmentName ?? employeeInfo.OnboardingDepartment;
}
else
{
employee.DepartmentName = employeeInfo.OnboardingDepartment;
}
employee.College = employeeInfo.GraduatedSchool;
employee.BirthDate = ParseDate(employeeInfo.Birthday);
employee.ContactAddress = employeeInfo.CurrentAddress;
@@ -68,13 +77,26 @@ public class OnboardingAuditService : IOnboardingAuditService
await _db.Ado.BeginTranAsync();
try
{
var updatedRows = await _db.Updateable<ObOnboardingTask>()
.SetColumns(item => new ObOnboardingTask
{
EmployeeId = employee.Id,
TaskStatus = OnboardingTaskStatus.Approved,
AuditedAt = DateTime.Now
})
.Where(item => item.Id == task.Id
&& item.TaskStatus == OnboardingTaskStatus.Reviewing
&& item.EmployeeId == null)
.ExecuteCommandAsync();
if (updatedRows != 1)
throw Oops.Bah("该入职任务已被其他审核人处理,请刷新后查看");
await _snapshotService.Build(task.Id, employeeInfo);
await _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<ObOnboardingTask>()
.SetColumns(item => new ObOnboardingTask
{
TaskStatus = OnboardingTaskStatus.Rejected,
AuditedAt = auditedAt,
RejectReason = input.AuditOpinion
})
.Where(item => item.Id == task.Id
&& item.TaskStatus == OnboardingTaskStatus.Reviewing)
.ExecuteCommandAsync();
if (updatedRows != 1)
throw Oops.Bah("该入职任务已被其他审核人处理,请刷新后查看");
task.TaskStatus = OnboardingTaskStatus.Rejected;
task.AuditedAt = auditedAt;
task.RejectReason = input.AuditOpinion;
await WriteAudit(task, OnboardingAuditAction.Reject, input.AuditOpinion);
await _db.Ado.CommitTranAsync();
}
catch
{
await _db.Ado.RollbackTranAsync();
throw;
}
}
private async Task<ObOnboardingTask> GetAuditableTask(long taskId)
@@ -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<long>();
if (configJson.RootElement.TryGetProperty("all", out var allReviewers))
{
reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64()));
}
if (configJson.RootElement.TryGetProperty("company", out var companyReviewers)
&& companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers))
{
reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64()));
}
var reviewerIds = OnboardingReviewerConfigParser.Parse(config.ConfigValue, task.CompanyId);
if (!reviewerIds.Contains(UserManager.UserId))
{
@@ -0,0 +1,64 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 解析入职审核人系统配置,统一支持全局审核人和公司专属审核人。
/// </summary>
internal static class OnboardingReviewerConfigParser
{
private const string ConfigKey = "onboard.reviewer.userIds";
/// <summary>
/// 获取指定公司的全部有效审核人用户 Id。
/// </summary>
public static HashSet<long> 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<long>();
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");
}
}
/// <summary>
/// 将 JSON 数组中的审核人 Id 加入集合,并拒绝非整数配置。
/// </summary>
private static void AddReviewers(
global::System.Text.Json.JsonElement element,
HashSet<long> 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);
}
}
}
@@ -70,6 +70,21 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
if (hasPositions)
throw Oops.Bah("公司下仍有岗位,不能删除");
var hasDepartments = await Context.Queryable<ObDepartment>()
.AnyAsync(item => input.Ids.Contains(item.CompanyId));
if (hasDepartments)
throw Oops.Bah("公司下仍有部门,不能删除");
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplate>()
.AnyAsync(item => input.Ids.Contains(item.CompanyId));
if (hasOnboardingTemplates)
throw Oops.Bah("公司已被入职模板引用,不能删除");
var hasContractTemplates = await Context.Queryable<ObContractTemplate>()
.AnyAsync(item => item.CompanyId.HasValue && input.Ids.Contains(item.CompanyId.Value));
if (hasContractTemplates)
throw Oops.Bah("公司已被合同模板引用,不能删除");
var hasTasks = await Context.Queryable<ObOnboardingTask>()
.AnyAsync(it => input.Ids.Contains(it.CompanyId));
if (hasTasks)
@@ -42,6 +42,15 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
var variables = await _db.Queryable<ObTaskContractVariable>()
.Where(item => item.TaskContractId == contract.Id)
.ToListAsync();
var duplicateVariable = variables
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(group => group.Count() > 1);
if (duplicateVariable != null)
{
throw Oops.Bah(
$"合同变量快照“{duplicateVariable.Key}”存在重复数据,请联系管理员修复后重试");
}
var missing = definitions
.Where(item => item.Required)
.FirstOrDefault(definition => string.IsNullOrWhiteSpace(
@@ -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)
@@ -5,6 +5,21 @@ namespace SimpleAdmin.Application;
/// </summary>
public class OnboardingContractTemplatePageInput : BasePageInput
{
/// <summary>
/// 所属公司 Id。
/// </summary>
public long? CompanyId { get; set; }
/// <summary>
/// 所属部门 Id。
/// </summary>
public long? DepartmentId { get; set; }
/// <summary>
/// 适用岗位 Id。
/// </summary>
public long? PositionId { get; set; }
/// <summary>
/// 合同模板名称。
/// </summary>
@@ -26,6 +41,23 @@ public class OnboardingContractTemplatePageInput : BasePageInput
/// </summary>
public class OnboardingContractTemplateAddInput
{
/// <summary>
/// 所属公司 Id。
/// </summary>
[IdNotNull(ErrorMessage = "所属公司不能为空")]
public long CompanyId { get; set; }
/// <summary>
/// 所属部门 Id。
/// </summary>
[IdNotNull(ErrorMessage = "所属部门不能为空")]
public long DepartmentId { get; set; }
/// <summary>
/// 合同模板适用的岗位 Id 列表。
/// </summary>
public List<long> PositionIds { get; set; } = new();
/// <summary>
/// 合同模板业务编码。
/// </summary>
@@ -66,3 +98,27 @@ public class OnboardingContractTemplateEditInput : OnboardingContractTemplateAdd
[IdNotNull(ErrorMessage = "模板Id不能为空")]
public long Id { get; set; }
}
/// <summary>
/// 查询公司、部门及岗位可选合同模板参数。
/// </summary>
public class OnboardingContractTemplateAvailableInput
{
/// <summary>
/// 所属公司 Id。
/// </summary>
[IdNotNull(ErrorMessage = "所属公司不能为空")]
public long CompanyId { get; set; }
/// <summary>
/// 所属部门 Id。
/// </summary>
[IdNotNull(ErrorMessage = "所属部门不能为空")]
public long DepartmentId { get; set; }
/// <summary>
/// 入职模板已选择的岗位 Id 列表。
/// 返回结果必须同时适用于这些岗位。
/// </summary>
public List<long> PositionIds { get; set; } = new();
}
@@ -15,6 +15,11 @@ public interface IOnboardingContractTemplateService : ITransient
/// </summary>
Task<ObContractTemplate> Detail(BaseIdInput input);
/// <summary>
/// 查询同时适用于指定公司、部门和全部已选岗位的启用合同模板。
/// </summary>
Task<List<ObContractTemplate>> ListAvailable(OnboardingContractTemplateAvailableInput input);
/// <summary>
/// 新增合同模板主档案。
/// </summary>
@@ -59,4 +64,4 @@ public interface IOnboardingContractTemplateService : ITransient
/// 编辑并确认模板版本中的变量定义。
/// </summary>
Task EditVariable(OnboardingTemplateVariableEditInput input);
}
}
@@ -17,15 +17,30 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
/// </summary>
public async Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input)
{
return await Context.Queryable<ObContractTemplate>()
var query = Context.Queryable<ObContractTemplate>()
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
.WhereIF(
!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<ObContractTemplatePosition>()
.Where(item => item.PositionId == input.PositionId.Value)
.Select(item => item.ContractTemplateId)
.ToListAsync();
query = query.Where(item => templateIds.Contains(item.Id));
}
var page = await query
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
.OrderBy(it => it.TemplateName)
.ToPagedListAsync(input.PageNum, input.PageSize);
await PopulateScopeDisplay(page.List.ToList());
return page;
}
/// <summary>
@@ -33,7 +48,47 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
/// </summary>
public async Task<ObContractTemplate> Detail(BaseIdInput input)
{
return await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在");
var template = await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在");
await PopulateScopeDisplay(new List<ObContractTemplate> { template });
return template;
}
/// <summary>
/// 查询同时覆盖全部已选岗位的启用合同模板。
/// </summary>
public async Task<List<ObContractTemplate>> ListAvailable(
OnboardingContractTemplateAvailableInput input)
{
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
if (input.PositionIds.Count == 0)
return new List<ObContractTemplate>();
await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
var candidates = await Context.Queryable<ObContractTemplate>()
.Where(item => item.CompanyId == input.CompanyId
&& item.DepartmentId == input.DepartmentId
&& item.Status == "enable")
.OrderBy(item => item.TemplateName)
.ToListAsync();
if (candidates.Count == 0)
return candidates;
var candidateIds = candidates.Select(item => item.Id).ToList();
var relations = await Context.Queryable<ObContractTemplatePosition>()
.Where(item => candidateIds.Contains(item.ContractTemplateId))
.ToListAsync();
var availableIds = relations
.GroupBy(item => item.ContractTemplateId)
.Where(group => input.PositionIds.All(positionId =>
group.Any(item => item.PositionId == positionId)))
.Select(group => group.Key)
.ToHashSet();
var availableTemplates = candidates
.Where(item => availableIds.Contains(item.Id))
.ToList();
await PopulateScopeDisplay(availableTemplates);
return availableTemplates;
}
/// <summary>
@@ -41,8 +96,24 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
/// </summary>
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<ObContractTemplate>());
var templateId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
var entity = BuildEntity(templateId, input);
await Context.Ado.BeginTranAsync();
try
{
await Context.Insertable(entity).ExecuteCommandAsync();
await InsertPositionRelations(templateId, input.PositionIds);
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
@@ -51,8 +122,29 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
public async Task Edit(OnboardingContractTemplateEditInput input)
{
await Detail(new BaseIdInput { Id = input.Id });
NormalizeInput(input);
await ValidateBusinessScope(input.CompanyId, input.DepartmentId, input.PositionIds);
await ValidateReferencedOnboardingTemplates(input);
await EnsureCodeUnique(input.TemplateCode, input.Id);
await UpdateAsync(input.Adapt<ObContractTemplate>());
var entity = BuildEntity(input.Id, input);
await Context.Ado.BeginTranAsync();
try
{
await Context.Updateable(entity)
.IgnoreColumns(ignoreAllNullColumns: true)
.ExecuteCommandAsync();
await Context.Deleteable<ObContractTemplatePosition>()
.Where(item => item.ContractTemplateId == input.Id)
.ExecuteCommandAsync();
await InsertPositionRelations(input.Id, input.PositionIds);
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
@@ -95,6 +187,10 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
.Where(it => input.Ids.Contains(it.TemplateId))
.ExecuteCommandAsync();
await Context.Deleteable<ObContractTemplatePosition>()
.Where(item => input.Ids.Contains(item.ContractTemplateId))
.ExecuteCommandAsync();
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
await Context.Ado.CommitTranAsync();
}
@@ -182,6 +278,15 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
// 严格比对变量字典库,防止手误和非法占位符
var dictList = await Context.Queryable<ObVariableDefinition>().ToListAsync();
var duplicateDefinition = dictList
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(group => group.Count() > 1);
if (duplicateDefinition != null)
{
throw Oops.Bah(
$"变量字典编码“{duplicateDefinition.Key}”存在重复数据,请先清理后再导入合同版本");
}
var dictMap = dictList.ToDictionary(it => it.VariableCode, it => it, StringComparer.OrdinalIgnoreCase);
var undefinedCodes = variableCodes.Where(code => !dictMap.ContainsKey(code)).ToList();
if (undefinedCodes.Count > 0)
@@ -336,4 +441,189 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
if (exists)
throw Oops.Bah("模板编码已存在");
}
/// <summary>
/// 标准化合同模板保存参数并执行基础业务校验。
/// </summary>
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<long>();
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("合同模板状态不正确");
}
/// <summary>
/// 校验公司、部门、岗位属于同一条启用的业务层级。
/// </summary>
private async Task ValidateBusinessScope(
long companyId,
long departmentId,
List<long> positionIds)
{
var companyEnabled = await Context.Queryable<ObCompany>()
.AnyAsync(item => item.Id == companyId && item.Status == "enable");
if (!companyEnabled)
throw Oops.Bah("所属公司不存在或未启用");
var departmentEnabled = await Context.Queryable<ObDepartment>()
.AnyAsync(item => item.Id == departmentId
&& item.CompanyId == companyId
&& item.Status == "enable");
if (!departmentEnabled)
throw Oops.Bah("所属部门不存在、未启用或不属于该公司");
var positionCount = await Context.Queryable<ObPosition>()
.CountAsync(item => positionIds.Contains(item.Id)
&& item.CompanyId == companyId
&& item.DepartmentId == departmentId
&& item.Status == "enable");
if (positionCount != positionIds.Count)
throw Oops.Bah("存在未启用或不属于该公司、部门的岗位");
}
/// <summary>
/// 防止修改合同模板归属后破坏已经保存的入职模板配置。
/// </summary>
private async Task ValidateReferencedOnboardingTemplates(
OnboardingContractTemplateEditInput input)
{
var relations = await Context.Queryable<ObOnboardingTemplateContract>()
.Where(item => item.ContractTemplateId == input.Id)
.ToListAsync();
if (relations.Count == 0)
return;
var onboardingTemplateIds = relations
.Select(item => item.OnboardingTemplateId)
.Distinct()
.ToList();
var onboardingTemplates = await Context.Queryable<ObOnboardingTemplate>()
.Where(item => onboardingTemplateIds.Contains(item.Id))
.ToListAsync();
var positionRelations = await Context.Queryable<ObOnboardingTemplatePosition>()
.Where(item => onboardingTemplateIds.Contains(item.OnboardingTemplateId))
.ToListAsync();
foreach (var template in onboardingTemplates)
{
var positionIds = positionRelations
.Where(item => item.OnboardingTemplateId == template.Id)
.Select(item => item.PositionId)
.Distinct()
.ToList();
var departmentMatches = !template.DepartmentId.HasValue
|| template.DepartmentId.Value == input.DepartmentId;
var scopeMatches = template.CompanyId == input.CompanyId
&& departmentMatches
&& positionIds.All(input.PositionIds.Contains);
if (!scopeMatches)
{
throw Oops.Bah(
$"合同模板仍被入职模板“{template.TemplateName}”引用,当前归属调整会使其失效,请先调整入职模板");
}
}
}
/// <summary>
/// 构建合同模板主档案实体。
/// </summary>
private static ObContractTemplate BuildEntity(
long templateId,
OnboardingContractTemplateAddInput input)
{
return new ObContractTemplate
{
Id = templateId,
CompanyId = input.CompanyId,
DepartmentId = input.DepartmentId,
TemplateCode = input.TemplateCode,
TemplateName = input.TemplateName,
ContractType = input.ContractType,
Description = input.Description,
Status = input.Status
};
}
/// <summary>
/// 保存合同模板适用岗位关系。
/// </summary>
private async Task InsertPositionRelations(long templateId, List<long> positionIds)
{
var relations = positionIds.Select(positionId => new ObContractTemplatePosition
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
ContractTemplateId = templateId,
PositionId = positionId
}).ToList();
await Context.Insertable(relations).ExecuteCommandAsync();
}
/// <summary>
/// 回填合同模板的公司、部门和适用岗位展示信息。
/// </summary>
private async Task PopulateScopeDisplay(List<ObContractTemplate> templates)
{
if (templates.Count == 0)
return;
var templateIds = templates.Select(item => item.Id).ToList();
var companyIds = templates
.Where(item => item.CompanyId.HasValue)
.Select(item => item.CompanyId.Value)
.Distinct()
.ToList();
var departmentIds = templates
.Where(item => item.DepartmentId.HasValue)
.Select(item => item.DepartmentId.Value)
.Distinct()
.ToList();
var companies = companyIds.Count == 0
? new List<ObCompany>()
: await Context.Queryable<ObCompany>()
.Where(item => companyIds.Contains(item.Id))
.ToListAsync();
var departments = departmentIds.Count == 0
? new List<ObDepartment>()
: await Context.Queryable<ObDepartment>()
.Where(item => departmentIds.Contains(item.Id))
.ToListAsync();
var relations = await Context.Queryable<ObContractTemplatePosition>()
.Where(item => templateIds.Contains(item.ContractTemplateId))
.ToListAsync();
var positionIds = relations.Select(item => item.PositionId).Distinct().ToList();
var positions = positionIds.Count == 0
? new List<ObPosition>()
: await Context.Queryable<ObPosition>()
.Where(item => positionIds.Contains(item.Id))
.ToListAsync();
foreach (var template in templates)
{
template.CompanyName = companies
.FirstOrDefault(item => item.Id == template.CompanyId)?.CompanyName;
template.DepartmentName = departments
.FirstOrDefault(item => item.Id == template.DepartmentId)?.DepartmentName;
template.PositionIds = relations
.Where(item => item.ContractTemplateId == template.Id)
.Select(item => item.PositionId)
.ToList();
template.PositionNames = template.PositionIds
.Select(positionId => positions.FirstOrDefault(item => item.Id == positionId)?.PositionName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
}
}
}
@@ -0,0 +1,63 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 入职业务部门分页查询参数。
/// </summary>
public class OnboardingDepartmentPageInput : BasePageInput
{
/// <summary>
/// 入职业务公司 Id。
/// </summary>
public long? CompanyId { get; set; }
/// <summary>
/// 部门编码。
/// </summary>
public virtual string DepartmentCode { get; set; }
/// <summary>
/// 部门名称。
/// </summary>
public virtual string DepartmentName { get; set; }
/// <summary>
/// 启用状态。
/// </summary>
public string Status { get; set; }
}
/// <summary>
/// 新增入职业务部门参数。
/// </summary>
public class OnboardingDepartmentAddInput : ObDepartment
{
/// <summary>
/// 所属公司 Id。
/// </summary>
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属公司不能为空")]
public new long CompanyId { get; set; }
/// <summary>
/// 部门业务编码。
/// </summary>
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "部门编码不能为空")]
public override string DepartmentCode { get; set; }
/// <summary>
/// 部门名称。
/// </summary>
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "部门名称不能为空")]
public override string DepartmentName { get; set; }
}
/// <summary>
/// 编辑入职业务部门参数。
/// </summary>
public class OnboardingDepartmentEditInput : OnboardingDepartmentAddInput
{
/// <summary>
/// 业务数据 Id。
/// </summary>
[IdNotNull(ErrorMessage = "部门Id不能为空")]
public override long Id { get; set; }
}
@@ -0,0 +1,37 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 定义入职业务部门业务能力。
/// </summary>
public interface IOnboardingDepartmentService : ITransient
{
/// <summary>
/// 分页查询入职业务部门。
/// </summary>
Task<SqlSugarPagedList<ObDepartment>> Page(OnboardingDepartmentPageInput input);
/// <summary>
/// 查询指定公司下全部已启用部门列表。
/// </summary>
Task<List<ObDepartment>> ListEnabled(long companyId);
/// <summary>
/// 查询部门详情。
/// </summary>
Task<ObDepartment> Detail(BaseIdInput input);
/// <summary>
/// 新增入职业务部门。
/// </summary>
Task Add(OnboardingDepartmentAddInput input);
/// <summary>
/// 编辑入职业务部门。
/// </summary>
Task Edit(OnboardingDepartmentEditInput input);
/// <summary>
/// 批量删除部门。
/// </summary>
Task Delete(BaseIdListInput input);
}
@@ -0,0 +1,177 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 实现入职业务部门业务能力。
/// </summary>
public class OnboardingDepartmentService : DbRepository<ObDepartment>, IOnboardingDepartmentService
{
/// <summary>
/// 分页查询入职业务部门。
/// </summary>
public async Task<SqlSugarPagedList<ObDepartment>> Page(OnboardingDepartmentPageInput input)
{
var list = await Context.Queryable<ObDepartment>()
.WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value)
.WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentCode), it => it.DepartmentCode.Contains(input.DepartmentCode))
.WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentName), it => it.DepartmentName.Contains(input.DepartmentName))
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.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<ObCompany>()
.Where(c => companyIds.Contains(c.Id))
.ToListAsync();
var companyMap = companies.ToDictionary(c => c.Id, c => c.CompanyName);
foreach (var item in list.List)
{
if (companyMap.TryGetValue(item.CompanyId, out var cName))
{
item.CompanyName = cName;
}
}
}
return list;
}
/// <summary>
/// 查询指定公司下全部已启用部门列表。
/// </summary>
public Task<List<ObDepartment>> ListEnabled(long companyId)
{
return Context.Queryable<ObDepartment>()
.Where(it => it.CompanyId == companyId && it.Status == "enable")
.OrderBy(it => it.SortCode)
.OrderBy(it => it.DepartmentName)
.ToListAsync();
}
/// <summary>
/// 查询部门详情。
/// </summary>
public async Task<ObDepartment> Detail(BaseIdInput input)
{
return await GetByIdAsync(input.Id) ?? throw Oops.Bah("部门不存在");
}
/// <summary>
/// 新增入职业务部门。
/// </summary>
public async Task Add(OnboardingDepartmentAddInput input)
{
NormalizeInput(input);
await EnsureCompanyExists(input.CompanyId);
await EnsureCodeUnique(input.CompanyId, input.DepartmentCode, 0);
await InsertAsync(input.Adapt<ObDepartment>());
}
/// <summary>
/// 编辑入职业务部门。
/// </summary>
public async Task Edit(OnboardingDepartmentEditInput input)
{
NormalizeInput(input);
var department = await Detail(new BaseIdInput { Id = input.Id });
await EnsureCompanyCanChange(department, input.CompanyId);
await EnsureCompanyExists(input.CompanyId);
await EnsureCodeUnique(input.CompanyId, input.DepartmentCode, input.Id);
await UpdateAsync(input.Adapt<ObDepartment>());
}
/// <summary>
/// 删除未被岗位或任务引用的部门。
/// </summary>
public async Task Delete(BaseIdListInput input)
{
if (input.Ids.Count == 0)
return;
var hasPositions = await Context.Queryable<ObPosition>()
.AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
if (hasPositions)
throw Oops.Bah("部门下已有岗位,不能删除");
var hasTasks = await Context.Queryable<ObOnboardingTask>()
.AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
if (hasTasks)
throw Oops.Bah("部门已有入职任务,不能删除");
var hasEmployees = await Context.Queryable<ObEmployee>()
.AnyAsync(it => it.DepartmentId.HasValue && input.Ids.Contains(it.DepartmentId.Value));
if (hasEmployees)
throw Oops.Bah("部门已有正式员工,不能删除");
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplate>()
.AnyAsync(item => item.DepartmentId.HasValue && input.Ids.Contains(item.DepartmentId.Value));
if (hasOnboardingTemplates)
throw Oops.Bah("部门已被入职模板引用,不能删除");
var hasContractTemplates = await Context.Queryable<ObContractTemplate>()
.AnyAsync(item => item.DepartmentId.HasValue && input.Ids.Contains(item.DepartmentId.Value));
if (hasContractTemplates)
throw Oops.Bah("部门已被合同模板引用,不能删除");
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
}
private async Task EnsureCompanyExists(long companyId)
{
var exists = await Context.Queryable<ObCompany>()
.AnyAsync(it => it.Id == companyId && it.Status == "enable");
if (!exists)
throw Oops.Bah("所属公司不存在、已删除或未启用");
}
/// <summary>
/// 标准化部门输入并拒绝非法状态。
/// </summary>
private static void NormalizeInput(OnboardingDepartmentAddInput input)
{
input.DepartmentCode = input.DepartmentCode?.Trim();
input.DepartmentName = input.DepartmentName?.Trim();
if (string.IsNullOrWhiteSpace(input.DepartmentCode)
|| string.IsNullOrWhiteSpace(input.DepartmentName))
{
throw Oops.Bah("请填写部门编码和部门名称");
}
if (input.Status != "enable" && input.Status != "disable")
throw Oops.Bah("部门状态不正确");
}
/// <summary>
/// 已产生下级或业务引用的部门不允许直接变更所属公司。
/// </summary>
private async Task EnsureCompanyCanChange(ObDepartment department, long companyId)
{
if (department.CompanyId == companyId)
return;
var hasPosition = await Context.Queryable<ObPosition>()
.AnyAsync(item => item.DepartmentId == department.Id);
var hasTask = await Context.Queryable<ObOnboardingTask>()
.AnyAsync(item => item.DepartmentId == department.Id);
var hasEmployee = await Context.Queryable<ObEmployee>()
.AnyAsync(item => item.DepartmentId == department.Id);
var hasOnboardingTemplate = await Context.Queryable<ObOnboardingTemplate>()
.AnyAsync(item => item.DepartmentId == department.Id);
var hasContractTemplate = await Context.Queryable<ObContractTemplate>()
.AnyAsync(item => item.DepartmentId == department.Id);
if (hasPosition || hasTask || hasEmployee || hasOnboardingTemplate || hasContractTemplate)
throw Oops.Bah("部门已有岗位、模板、任务或员工引用,不能直接变更所属公司");
}
private async Task EnsureCodeUnique(long companyId, string code, long id)
{
var exists = await Context.Queryable<ObDepartment>()
.AnyAsync(it => it.CompanyId == companyId && it.DepartmentCode == code && it.Id != id);
if (exists)
throw Oops.Bah("该公司下部门编码已存在");
}
}
@@ -21,7 +21,12 @@ public class OnboardingEmployeePageInput : BasePageInput
public long? CompanyId { get; set; }
/// <summary>
/// 岗位 Id;为空时表示公司级配置。
/// 入职业务部门 Id。
/// </summary>
public long? DepartmentId { get; set; }
/// <summary>
/// 岗位 Id。
/// </summary>
public long? PositionId { get; set; }
}
@@ -1,33 +1,61 @@
namespace SimpleAdmin.Application;
/// <summary>仅展示审核通过后创建的正式员工档案;入职资料的来源任务可追溯。</summary>
/// <summary>
/// 实现正式员工档案查询业务能力。
/// 实现正式员工档案查询业务能力,仅展示审核通过后创建的正式员工。
/// </summary>
public class OnboardingEmployeeService : IOnboardingEmployeeService
{
private readonly SqlSugarScope _db;
public OnboardingEmployeeService() => _db = DbContext.DB;
public OnboardingEmployeeService()
{
_db = DbContext.DB;
}
/// <summary>
/// 分页查询审核通过后生成的正式员工档案。
/// </summary>
public Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input) => _db.Queryable<ObEmployee>()
.WhereIF(!string.IsNullOrWhiteSpace(input.EmployeeName), x => x.EmployeeName.Contains(input.EmployeeName) || x.IdCardNumber.Contains(input.EmployeeName))
.WhereIF(!string.IsNullOrWhiteSpace(input.Phone), x => x.Phone.Contains(input.Phone))
.WhereIF(input.CompanyId.HasValue, x => x.CompanyId == input.CompanyId.Value)
.WhereIF(input.PositionId.HasValue, x => x.PositionId == input.PositionId.Value)
.OrderByDescending(x => x.CreateTime).ToPagedListAsync(input.PageNum, input.PageSize);
public Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input)
{
return _db.Queryable<ObEmployee>()
.WhereIF(
!string.IsNullOrWhiteSpace(input.EmployeeName),
item => item.EmployeeName.Contains(input.EmployeeName)
|| item.IdCardNumber.Contains(input.EmployeeName))
.WhereIF(!string.IsNullOrWhiteSpace(input.Phone), item => item.Phone.Contains(input.Phone))
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
.WhereIF(input.PositionId.HasValue, item => item.PositionId == input.PositionId.Value)
.OrderByDescending(item => item.CreateTime)
.ToPagedListAsync(input.PageNum, input.PageSize);
}
/// <summary>
/// 查询正式员工及其来源任务、合同和审核记录。
/// </summary>
public async Task<OnboardingEmployeeDetailOutput> Detail(BaseIdInput input)
{
var employee = await _db.Queryable<ObEmployee>().InSingleAsync(input.Id) ?? throw Oops.Bah("正式员工不存在");
var employee = await _db.Queryable<ObEmployee>().InSingleAsync(input.Id)
?? throw Oops.Bah("正式员工不存在");
var task = await _db.Queryable<ObOnboardingTask>().FirstAsync(x => x.EmployeeId == employee.Id);
var contracts = task == null ? new List<ObTaskContract>() : await _db.Queryable<ObTaskContract>().Where(x => x.TaskId == task.Id).ToListAsync();
var audits = task == null ? new List<ObTaskAudit>() : await _db.Queryable<ObTaskAudit>().Where(x => x.TaskId == task.Id).OrderByDescending(x => x.AuditedAt).ToListAsync();
return new OnboardingEmployeeDetailOutput { Employee = employee, Task = task, Contracts = contracts, Audits = audits };
var contracts = task == null
? new List<ObTaskContract>()
: await _db.Queryable<ObTaskContract>()
.Where(item => item.TaskId == task.Id)
.ToListAsync();
var audits = task == null
? new List<ObTaskAudit>()
: await _db.Queryable<ObTaskAudit>()
.Where(item => item.TaskId == task.Id)
.OrderByDescending(item => item.AuditedAt)
.ToListAsync();
return new OnboardingEmployeeDetailOutput
{
Employee = employee,
Task = task,
Contracts = contracts,
Audits = audits
};
}
}
@@ -25,10 +25,32 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
throw Oops.Bah("仅审核通过任务可归档");
}
task.TaskStatus = OnboardingTaskStatus.Archived;
task.ArchivedAt = DateTime.Now;
await _db.Updateable(task).ExecuteCommandAsync();
await Log(task, OnboardingAuditAction.Archive, input.AuditOpinion);
await _db.Ado.BeginTranAsync();
try
{
var archivedAt = DateTime.Now;
var updatedRows = await _db.Updateable<ObOnboardingTask>()
.SetColumns(item => new ObOnboardingTask
{
TaskStatus = OnboardingTaskStatus.Archived,
ArchivedAt = archivedAt
})
.Where(item => item.Id == task.Id
&& item.TaskStatus == OnboardingTaskStatus.Approved)
.ExecuteCommandAsync();
if (updatedRows != 1)
throw Oops.Bah("该入职任务状态已变化,请刷新后重试");
task.TaskStatus = OnboardingTaskStatus.Archived;
task.ArchivedAt = archivedAt;
await Log(task, OnboardingAuditAction.Archive, input.AuditOpinion);
await _db.Ado.CommitTranAsync();
}
catch
{
await _db.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
@@ -37,22 +59,43 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
public async Task Revoke(OnboardingAuditActionInput input)
{
var task = await Authorize(input.TaskId);
if (task.TaskStatus == OnboardingTaskStatus.Approved || task.TaskStatus == OnboardingTaskStatus.Archived)
if (task.TaskStatus == OnboardingTaskStatus.Approved
|| task.TaskStatus == OnboardingTaskStatus.Archived
|| task.TaskStatus == OnboardingTaskStatus.Revoked)
{
throw Oops.Bah("已通过或已归档任务不能撤销");
throw Oops.Bah("已通过、已归档或已撤销任务不能再次撤销");
}
task.TaskStatus = OnboardingTaskStatus.Revoked;
await _db.Updateable(task).ExecuteCommandAsync();
await _db.Updateable<ObTaskLink>()
.SetColumns(item => new ObTaskLink
{
LinkStatus = OnboardingLinkStatus.Revoked,
RevokedAt = DateTime.Now
})
.Where(item => item.TaskId == task.Id && item.LinkStatus == OnboardingLinkStatus.Active)
.ExecuteCommandAsync();
await Log(task, OnboardingAuditAction.Revoke, input.AuditOpinion);
await _db.Ado.BeginTranAsync();
try
{
var originalStatus = task.TaskStatus;
var revokedAt = DateTime.Now;
var updatedRows = await _db.Updateable<ObOnboardingTask>()
.SetColumns(item => item.TaskStatus == OnboardingTaskStatus.Revoked)
.Where(item => item.Id == task.Id && item.TaskStatus == originalStatus)
.ExecuteCommandAsync();
if (updatedRows != 1)
throw Oops.Bah("该入职任务状态已变化,请刷新后重试");
await _db.Updateable<ObTaskLink>()
.SetColumns(item => new ObTaskLink
{
LinkStatus = OnboardingLinkStatus.Revoked,
RevokedAt = revokedAt
})
.Where(item => item.TaskId == task.Id
&& item.LinkStatus == OnboardingLinkStatus.Active)
.ExecuteCommandAsync();
task.TaskStatus = OnboardingTaskStatus.Revoked;
await Log(task, OnboardingAuditAction.Revoke, input.AuditOpinion);
await _db.Ado.CommitTranAsync();
}
catch
{
await _db.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
@@ -86,6 +129,9 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
await _db.Deleteable<ObTaskMaterialRequirement>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
await _db.Deleteable<ObTaskMaterial>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
await _db.Deleteable<ObTaskEmployeeInfo>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
await _db.Deleteable<ObTaskFormRequirement>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
await _db.Deleteable<ObTaskFormValue>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
await _db.Deleteable<ObTaskOperationLog>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
await _db.Deleteable<ObTaskAudit>().Where(x => x.TaskId == task.Id).ExecuteCommandAsync();
await _db.Deleteable<ObOnboardingTask>().In(task.Id).ExecuteCommandAsync();
await _db.Ado.CommitTranAsync();
@@ -103,19 +149,7 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
?? throw Oops.Bah("入职任务不存在");
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<long>();
if (configJson.RootElement.TryGetProperty("all", out var allReviewers))
{
reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64()));
}
if (configJson.RootElement.TryGetProperty("company", out var companyReviewers)
&& companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers))
{
reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64()));
}
var reviewerIds = OnboardingReviewerConfigParser.Parse(config.ConfigValue, task.CompanyId);
if (!reviewerIds.Contains(UserManager.UserId))
{
@@ -50,18 +50,7 @@ public class OnboardingMaterialReviewService : IOnboardingMaterialReviewService
{
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<long>();
if (configJson.RootElement.TryGetProperty("all", out var allReviewers))
{
reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64()));
}
if (configJson.RootElement.TryGetProperty("company", out var companyReviewers)
&& companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers))
{
reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64()));
}
var reviewerIds = OnboardingReviewerConfigParser.Parse(config.ConfigValue, task.CompanyId);
if (!reviewerIds.Contains(UserManager.UserId))
{
@@ -15,6 +15,11 @@ public class OnboardingTemplatePageInput : BasePageInput
/// </summary>
public long? CompanyId { get; set; }
/// <summary>
/// 所属部门 Id。
/// </summary>
public long? DepartmentId { get; set; }
/// <summary>
/// 适用岗位 Id。
/// </summary>
@@ -51,6 +56,12 @@ public class OnboardingTemplateSaveInput
/// </summary>
public long CompanyId { get; set; }
/// <summary>
/// 所属部门 Id。
/// </summary>
[IdNotNull(ErrorMessage = "所属部门不能为空")]
public long DepartmentId { get; set; }
/// <summary>
/// 适用岗位 Id 列表。
/// </summary>
@@ -165,6 +176,11 @@ public class OnboardingTemplateOutput : ObOnboardingTemplate
/// </summary>
public string CompanyName { get; set; }
/// <summary>
/// 所属部门名称。
/// </summary>
public string DepartmentName { get; set; }
/// <summary>
/// 适用岗位 Id 列表。
/// </summary>
@@ -210,4 +226,4 @@ public class OnboardingTemplateDetailOutput : OnboardingTemplateOutput
/// 上传材料配置明细。
/// </summary>
public List<ObOnboardingTemplateMaterial> Materials { get; set; } = new();
}
}
@@ -26,7 +26,10 @@ public interface IOnboardingTemplateService : ITransient
Task Delete(BaseIdListInput input);
/// <summary>
/// 查询公司岗位可使用的入职模板。
/// 查询公司、部门和岗位可使用的入职模板。
/// </summary>
Task<List<OnboardingTemplateOutput>> ListEnabled(long companyId, long positionId);
}
Task<List<OnboardingTemplateOutput>> ListEnabled(
long companyId,
long departmentId,
long positionId);
}
@@ -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<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))
@@ -167,7 +168,10 @@ public class OnboardingTemplateService : IOnboardingTemplateService
}
/// <inheritdoc />
public async Task<List<OnboardingTemplateOutput>> ListEnabled(long companyId, long positionId)
public async Task<List<OnboardingTemplateOutput>> ListEnabled(
long companyId,
long departmentId,
long positionId)
{
var templateIds = await _db.Queryable<ObOnboardingTemplatePosition>()
.Where(item => item.PositionId == positionId)
@@ -175,6 +179,7 @@ public class OnboardingTemplateService : IOnboardingTemplateService
.ToListAsync();
var templates = await _db.Queryable<ObOnboardingTemplate>()
.Where(item => item.CompanyId == companyId
&& item.DepartmentId == departmentId
&& item.Status == "enable"
&& templateIds.Contains(item.Id))
.OrderBy(item => item.TemplateName)
@@ -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<long>();
input.Contracts = input.Contracts?
.GroupBy(item => item.ContractTemplateVersionId)
.Select(group => group.First())
.ToList() ?? new List<OnboardingTemplateContractInput>();
input.Contracts ??= new List<OnboardingTemplateContractInput>();
var duplicateContract = input.Contracts
.GroupBy(item => item.ContractTemplateId)
.FirstOrDefault(group => group.Count() > 1);
if (duplicateContract != null)
throw Oops.Bah("同一份合同模板在入职模板中只能选择一个版本");
input.FormFields = input.FormFields?
.GroupBy(item => item.FieldId)
.Select(group => group.First())
@@ -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<ObDepartment>()
.AnyAsync(item => item.Id == input.DepartmentId
&& item.CompanyId == input.CompanyId
&& item.Status == "enable");
if (!departmentEnabled)
throw Oops.Bah("所属部门不存在、未启用或不属于该公司");
var positionCount = await _db.Queryable<ObPosition>()
.CountAsync(item => input.PositionIds.Contains(item.Id)
&& item.CompanyId == input.CompanyId
&& item.DepartmentId == input.DepartmentId
&& item.Status == "enable");
if (positionCount != input.PositionIds.Count)
throw Oops.Bah("存在不属于该公司或未启用的岗位");
throw Oops.Bah("存在未启用或不属于该公司、部门的岗位");
var versionIds = input.Contracts.Select(item => item.ContractTemplateVersionId).ToList();
var versions = await _db.Queryable<ObContractTemplateVersion>()
@@ -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<ObContractTemplate>()
.Where(item => contractTemplateIds.Contains(item.Id))
.ToListAsync();
if (contractTemplates.Count != contractTemplateIds.Count
|| contractTemplates.Any(item => item.CompanyId != input.CompanyId
|| item.DepartmentId != input.DepartmentId))
{
throw Oops.Bah("存在不属于当前公司、部门的合同模板");
}
var contractPositionRelations = await _db.Queryable<ObContractTemplatePosition>()
.Where(item => contractTemplateIds.Contains(item.ContractTemplateId))
.ToListAsync();
var unsupportedContract = contractTemplateIds.FirstOrDefault(contractTemplateId =>
{
var supportedPositionIds = contractPositionRelations
.Where(item => item.ContractTemplateId == contractTemplateId)
.Select(item => item.PositionId)
.ToHashSet();
return input.PositionIds.Any(positionId => !supportedPositionIds.Contains(positionId));
});
if (unsupportedContract > 0)
{
var contractName = contractTemplates
.First(item => item.Id == unsupportedContract)
.TemplateName;
throw Oops.Bah($"合同模板“{contractName}”不能覆盖当前选择的全部岗位");
}
if (input.Status == "enable")
{
if (contractTemplates.Any(item => 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<ObOnboardingTemplateMaterial>()
.Where(item => item.OnboardingTemplateId == templateId)
.ExecuteCommandAsync();
await _db.Deleteable<ObOnboardingTemplateFormField>()
.Where(item => item.OnboardingTemplateId == templateId)
.ExecuteCommandAsync();
}
private async Task<List<OnboardingTemplateOutput>> BuildOutputs(List<ObOnboardingTemplate> 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<ObCompany>()
.Where(item => companyIds.Contains(item.Id))
.ToListAsync();
var departments = departmentIds.Count == 0
? new List<ObDepartment>()
: await _db.Queryable<ObDepartment>()
.Where(item => departmentIds.Contains(item.Id))
.ToListAsync();
var relations = await _db.Queryable<ObOnboardingTemplatePosition>()
.Where(item => templateIds.Contains(item.OnboardingTemplateId))
.ToListAsync();
@@ -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();
}
}
}
@@ -1,4 +1,4 @@
namespace SimpleAdmin.Application;
namespace SimpleAdmin.Application;
/// <summary>
/// 入职业务岗位分页查询参数。
@@ -10,6 +10,11 @@ public class OnboardingPositionPageInput : BasePageInput
/// </summary>
public long? CompanyId { get; set; }
/// <summary>
/// 入职业务部门 Id。
/// </summary>
public long? DepartmentId { get; set; }
/// <summary>
/// 岗位编码。
/// </summary>
@@ -21,7 +26,7 @@ public class OnboardingPositionPageInput : BasePageInput
public virtual string PositionName { get; set; }
/// <summary>
/// 启用状态或状态筛选条件。
/// 启用状态。
/// </summary>
public string Status { get; set; }
}
@@ -31,6 +36,18 @@ public class OnboardingPositionPageInput : BasePageInput
/// </summary>
public class OnboardingPositionAddInput : ObPosition
{
/// <summary>
/// 所属公司 Id。
/// </summary>
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属公司不能为空")]
public new long CompanyId { get; set; }
/// <summary>
/// 所属部门 Id。
/// </summary>
[global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "所属部门不能为空")]
public new long? DepartmentId { get; set; }
/// <summary>
/// 岗位业务编码。
/// </summary>
@@ -54,4 +71,4 @@ public class OnboardingPositionEditInput : OnboardingPositionAddInput
/// </summary>
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
public override long Id { get; set; }
}
}
@@ -1,4 +1,4 @@
namespace SimpleAdmin.Application;
namespace SimpleAdmin.Application;
/// <summary>
/// 定义入职业务岗位业务能力。
@@ -9,24 +9,34 @@ public interface IOnboardingPositionService : ITransient
/// 分页查询入职业务岗位。
/// </summary>
Task<SqlSugarPagedList<ObPosition>> Page(OnboardingPositionPageInput input);
/// <summary>
/// 查询指定公司下全部已启用岗位。
/// </summary>
Task<List<ObPosition>> ListEnabled(long companyId);
/// <summary>
/// 查询指定部门下全部已启用岗位。
/// </summary>
Task<List<ObPosition>> ListEnabledByDepartment(long departmentId);
/// <summary>
/// 查询岗位详情。
/// </summary>
Task<ObPosition> Detail(BaseIdInput input);
/// <summary>
/// 新增入职业务岗位。
/// </summary>
Task Add(OnboardingPositionAddInput input);
/// <summary>
/// 编辑入职业务岗位。
/// </summary>
Task Edit(OnboardingPositionEditInput input);
/// <summary>
/// 删除未被任务或正式员工引用的岗位。
/// </summary>
Task Delete(BaseIdListInput input);
}
}
@@ -1,4 +1,4 @@
namespace SimpleAdmin.Application;
namespace SimpleAdmin.Application;
/// <summary>
/// 实现入职业务岗位业务能力。
@@ -10,14 +10,45 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
/// </summary>
public async Task<SqlSugarPagedList<ObPosition>> Page(OnboardingPositionPageInput input)
{
return await Context.Queryable<ObPosition>()
var list = await Context.Queryable<ObPosition>()
.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<ObCompany>()
.Where(c => companyIds.Contains(c.Id))
.ToListAsync();
var departments = departmentIds.Count == 0
? new List<ObDepartment>()
: await Context.Queryable<ObDepartment>().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;
}
/// <summary>
@@ -31,6 +62,17 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
.ToListAsync();
}
/// <summary>
/// 查询指定部门下全部已启用岗位。
/// </summary>
public Task<List<ObPosition>> ListEnabledByDepartment(long departmentId)
{
return Context.Queryable<ObPosition>()
.Where(it => it.DepartmentId == departmentId && it.Status == "enable")
.OrderBy(it => it.PositionName)
.ToListAsync();
}
/// <summary>
/// 查询岗位详情。
/// </summary>
@@ -44,7 +86,12 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
/// </summary>
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<ObPosition>());
}
@@ -54,8 +101,14 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
/// </summary>
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<ObPosition>());
}
@@ -78,14 +131,75 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
if (hasEmployees)
throw Oops.Bah("岗位已有正式员工,不能删除");
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplatePosition>()
.AnyAsync(item => input.Ids.Contains(item.PositionId));
if (hasOnboardingTemplates)
throw Oops.Bah("岗位已被入职模板引用,不能删除");
var hasContractTemplates = await Context.Queryable<ObContractTemplatePosition>()
.AnyAsync(item => input.Ids.Contains(item.PositionId));
if (hasContractTemplates)
throw Oops.Bah("岗位已被合同模板引用,不能删除");
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
}
private async Task EnsureCompanyExists(long companyId)
{
var exists = await Context.Queryable<ObCompany>().AnyAsync(it => it.Id == companyId);
var exists = await Context.Queryable<ObCompany>()
.AnyAsync(it => it.Id == companyId && it.Status == "enable");
if (!exists)
throw Oops.Bah("所属公司不存在或已删除");
throw Oops.Bah("所属公司不存在、已删除或未启用");
}
private async Task EnsureDepartmentExists(long companyId, long departmentId)
{
var exists = await Context.Queryable<ObDepartment>()
.AnyAsync(it => it.Id == departmentId
&& it.CompanyId == companyId
&& it.Status == "enable");
if (!exists)
throw Oops.Bah("所属部门不存在、未启用或不属于该公司");
}
/// <summary>
/// 标准化岗位输入并拒绝非法状态。
/// </summary>
private static void NormalizeInput(OnboardingPositionAddInput input)
{
input.PositionCode = input.PositionCode?.Trim();
input.PositionName = input.PositionName?.Trim();
if (string.IsNullOrWhiteSpace(input.PositionCode)
|| string.IsNullOrWhiteSpace(input.PositionName))
{
throw Oops.Bah("请填写岗位编码和岗位名称");
}
if (input.Status != "enable" && input.Status != "disable")
throw Oops.Bah("岗位状态不正确");
}
/// <summary>
/// 已被业务配置或历史数据引用的岗位不允许直接改变公司、部门归属。
/// </summary>
private async Task EnsureScopeCanChange(
ObPosition position,
long companyId,
long departmentId)
{
if (position.CompanyId == companyId && position.DepartmentId == departmentId)
return;
var hasTask = await Context.Queryable<ObOnboardingTask>()
.AnyAsync(item => item.PositionId == position.Id);
var hasEmployee = await Context.Queryable<ObEmployee>()
.AnyAsync(item => item.PositionId == position.Id);
var hasOnboardingTemplate = await Context.Queryable<ObOnboardingTemplatePosition>()
.AnyAsync(item => item.PositionId == position.Id);
var hasContractTemplate = await Context.Queryable<ObContractTemplatePosition>()
.AnyAsync(item => item.PositionId == position.Id);
if (hasTask || hasEmployee || hasOnboardingTemplate || hasContractTemplate)
throw Oops.Bah("岗位已被模板、任务或员工引用,不能直接变更所属公司、部门");
}
private async Task EnsureCodeUnique(long companyId, string code, long id)
@@ -1,38 +1,5 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 单个动态字段填写值。
/// </summary>
public class OnboardingDynamicValueInput
{
/// <summary>
/// 动态字段 Id。
/// </summary>
public long FieldId { get; set; }
/// <summary>
/// 动态字段填写值。
/// </summary>
public string FieldValue { get; set; }
}
/// <summary>
/// 批量保存员工动态字段参数。
/// </summary>
public class OnboardingSaveDynamicValuesInput
{
/// <summary>
/// 员工填写链接中的明文 Token。
/// </summary>
[global::System.ComponentModel.DataAnnotations.Required]
public string Token { get; set; }
/// <summary>
/// 待保存的字段或变量值集合。
/// </summary>
public List<OnboardingDynamicValueInput> Values { get; set; } = new();
}
/// <summary>
/// 保存员工已上传材料参数。
/// </summary>
@@ -5,11 +5,6 @@ namespace SimpleAdmin.Application;
/// </summary>
public interface IOnboardingPublicSubmissionService : ITransient
{
/// <summary>
/// 保存候选人填写的动态字段值。
/// </summary>
Task SaveDynamicValues(OnboardingSaveDynamicValuesInput input);
/// <summary>
/// 关联候选人已经上传的系统文件与材料类型。
/// </summary>
@@ -14,15 +14,6 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
_snapshotService = snapshotService;
}
/// <summary>
/// 保存候选人填写的动态补充字段值。
/// </summary>
public async Task SaveDynamicValues(OnboardingSaveDynamicValuesInput input)
{
await GetEditableTask(input.Token);
throw Oops.Bah("新版入职登记表不再使用旧动态字段接口,请通过员工基础信息接口保存补充变量");
}
/// <summary>
/// 关联已上传 OSS 文件与任务材料要求。
/// </summary>
@@ -128,6 +128,8 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
throw Oops.Bah("当前任务不可编辑");
}
ValidateEmployeeInfo(input);
var old = await _db.Queryable<ObTaskEmployeeInfo>()
.FirstAsync(item => item.TaskId == task.Id);
var entity = input.Adapt<ObTaskEmployeeInfo>();
@@ -161,6 +163,46 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
await _snapshotService.Build(task.Id, entity, invalidateGeneratedPreview: true);
}
/// <summary>
/// 校验候选人核心身份信息,避免非法格式进入合同和正式员工档案。
/// </summary>
private static void ValidateEmployeeInfo(OnboardingTaskPublicSaveInput input)
{
input.EmployeeName = input.EmployeeName?.Trim();
input.Phone = input.Phone?.Trim();
input.IdCardNumber = input.IdCardNumber?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(input.EmployeeName) || input.EmployeeName.Length > 100)
throw Oops.Bah("员工姓名不能为空且不能超过 100 个字符");
if (!global::System.Text.RegularExpressions.Regex.IsMatch(
input.Phone ?? string.Empty,
"^1[3-9]\\d{9}$"))
{
throw Oops.Bah("请输入正确的 11 位手机号码");
}
if (!global::System.Text.RegularExpressions.Regex.IsMatch(
input.IdCardNumber ?? string.Empty,
"^\\d{17}[0-9X]$"))
{
throw Oops.Bah("请输入正确的 18 位身份证号码");
}
if (string.IsNullOrWhiteSpace(input.CustomVariablesJson))
return;
try
{
using var document = global::System.Text.Json.JsonDocument.Parse(input.CustomVariablesJson);
if (document.RootElement.ValueKind != global::System.Text.Json.JsonValueKind.Object)
throw new global::System.Text.Json.JsonException();
}
catch (global::System.Text.Json.JsonException)
{
throw Oops.Bah("员工补充变量格式不正确,请刷新页面后重新填写");
}
}
/// <summary>
/// 根据员工当前已填写信息校验变量并提前生成合同 DOCX。
/// </summary>
@@ -1,4 +1,4 @@
namespace SimpleAdmin.Application;
namespace SimpleAdmin.Application;
/// <summary>
/// 创建新员工入职任务参数。
@@ -11,11 +11,18 @@ public class OnboardingTaskCreateInput
[IdNotNull(ErrorMessage = "公司Id不能为空")]
public long CompanyId { get; set; }
/// <summary>
/// 入职业务部门 Id。
/// </summary>
[IdNotNull(ErrorMessage = "部门Id不能为空")]
public long DepartmentId { get; set; }
/// <summary>
/// 入职任务所属岗位 Id。
/// </summary>
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
public long PositionId { get; set; }
/// <summary>
/// 本次任务使用的入职模板 Id。
/// </summary>
@@ -89,4 +96,4 @@ public class OnboardingTaskCreatedOutput
/// 员工填写链接过期时间。
/// </summary>
public DateTime ExpiresAt { get; set; }
}
}
@@ -28,18 +28,25 @@ public class OnboardingTaskService : IOnboardingTaskService
if (!companyEnabled)
throw Oops.Bah("公司不存在或未启用");
var departmentEnabled = await _db.Queryable<ObDepartment>()
.AnyAsync(item => item.Id == input.DepartmentId && item.CompanyId == input.CompanyId && item.Status == "enable");
if (!departmentEnabled)
throw Oops.Bah("部门不存在、未启用或不属于该公司");
var positionEnabled = await _db.Queryable<ObPosition>()
.AnyAsync(item => item.Id == input.PositionId
&& item.CompanyId == input.CompanyId
&& item.DepartmentId == input.DepartmentId
&& item.Status == "enable");
if (!positionEnabled)
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
throw Oops.Bah("岗位不存在、未启用或不属于该部门/公司");
var onboardingTemplate = await _db.Queryable<ObOnboardingTemplate>()
.FirstAsync(item => item.Id == input.OnboardingTemplateId
&& item.CompanyId == input.CompanyId
&& item.DepartmentId == input.DepartmentId
&& item.Status == "enable")
?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司");
?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司、部门");
var templateSupportsPosition = await _db.Queryable<ObOnboardingTemplatePosition>()
.AnyAsync(item => item.OnboardingTemplateId == onboardingTemplate.Id
&& item.PositionId == input.PositionId);
@@ -62,6 +69,20 @@ public class OnboardingTaskService : IOnboardingTaskService
var contractTemplates = await _db.Queryable<ObContractTemplate>()
.Where(item => templateIds.Contains(item.Id))
.ToListAsync();
if (contractTemplates.Count != templateIds.Count
|| contractTemplates.Any(item => item.CompanyId != input.CompanyId
|| item.DepartmentId != input.DepartmentId
|| item.Status != "enable"))
{
throw Oops.Bah("入职模板包含未启用或不属于当前公司、部门的合同模板");
}
var contractPositionCount = await _db.Queryable<ObContractTemplatePosition>()
.CountAsync(item => templateIds.Contains(item.ContractTemplateId)
&& item.PositionId == input.PositionId);
if (contractPositionCount != templateIds.Count)
throw Oops.Bah("入职模板包含不适用于当前岗位的合同模板");
var versionIds = contractConfigs.Select(item => item.ContractTemplateVersionId).Distinct().ToList();
var enabledVersionCount = await _db.Queryable<ObContractTemplateVersion>()
.CountAsync(item => versionIds.Contains(item.Id)
@@ -94,6 +115,7 @@ public class OnboardingTaskService : IOnboardingTaskService
Id = taskId,
TaskNo = $"OB{DateTime.Now:yyyyMMddHHmmss}{taskId.ToString()[^4..]}",
CompanyId = input.CompanyId,
DepartmentId = input.DepartmentId,
PositionId = input.PositionId,
OnboardingTemplateId = onboardingTemplate.Id,
AssignedReviewerUserId = input.AssignedReviewerUserId,
@@ -236,6 +258,12 @@ public class OnboardingTaskService : IOnboardingTaskService
if (inputs.Count == 0)
return;
var duplicateContractVersion = contracts
.GroupBy(item => item.TemplateVersionId)
.FirstOrDefault(group => group.Count() > 1);
if (duplicateContractVersion != null)
throw Oops.Bah("入职模板中存在重复的合同版本,请重新保存入职模板后再创建任务");
var contractByVersion = contracts.ToDictionary(item => item.TemplateVersionId);
var contractIds = contracts.Select(item => item.Id).ToList();
var rows = await _db.Queryable<ObTaskContractVariable>()
@@ -1,4 +1,4 @@
namespace SimpleAdmin.Application;
namespace SimpleAdmin.Application;
/// <summary>
/// 入职任务分页查询参数。
@@ -19,6 +19,11 @@ public class OnboardingTaskPageInput : BasePageInput
/// 入职业务公司 Id。
/// </summary>
public long? CompanyId { get; set; }
/// <summary>
/// 入职业务部门 Id。
/// </summary>
public long? DepartmentId { get; set; }
}
/// <summary>
@@ -71,4 +76,4 @@ public class OnboardingTaskMaterialDetailOutput : ObTaskMaterial
/// 材料文件后缀,用于判断是否支持图片预览。
/// </summary>
public string FileSuffix { get; set; }
}
}
@@ -1,4 +1,4 @@
namespace SimpleAdmin.Application;
namespace SimpleAdmin.Application;
/// <summary>
/// 实现入职任务查询业务能力。
@@ -23,6 +23,7 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo), item => item.TaskNo.Contains(input.TaskNo))
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskStatus), item => item.TaskStatus == input.TaskStatus)
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
.OrderByDescending(item => item.CreateTime)
.ToPagedListAsync(input.PageNum, input.PageSize);
@@ -191,20 +192,26 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
{
var taskList = tasks.ToList();
var companyIds = taskList.Select(t => t.CompanyId).Distinct().ToList();
var departmentIds = taskList.Where(t => t.DepartmentId.HasValue).Select(t => t.DepartmentId.Value).Distinct().ToList();
var positionIds = taskList.Select(t => t.PositionId).Distinct().ToList();
var templateIds = taskList.Select(t => t.OnboardingTemplateId).Distinct().ToList();
var companies = await _db.Queryable<ObCompany>().Where(c => companyIds.Contains(c.Id)).ToListAsync();
var departments = departmentIds.Count == 0
? new List<ObDepartment>()
: await _db.Queryable<ObDepartment>().Where(d => departmentIds.Contains(d.Id)).ToListAsync();
var positions = await _db.Queryable<ObPosition>().Where(p => positionIds.Contains(p.Id)).ToListAsync();
var templates = await _db.Queryable<ObOnboardingTemplate>().Where(t => templateIds.Contains(t.Id)).ToListAsync();
var companyMap = companies.ToDictionary(c => c.Id, c => c.CompanyName);
var departmentMap = departments.ToDictionary(d => d.Id, d => d.DepartmentName);
var positionMap = positions.ToDictionary(p => p.Id, p => p.PositionName);
var templateMap = templates.ToDictionary(t => t.Id, t => t.TemplateName);
foreach (var task in taskList)
{
if (companyMap.TryGetValue(task.CompanyId, out var cName)) task.CompanyName = cName;
if (task.DepartmentId.HasValue && departmentMap.TryGetValue(task.DepartmentId.Value, out var dName)) task.DepartmentName = dName;
if (positionMap.TryGetValue(task.PositionId, out var pName)) task.PositionName = pName;
if (templateMap.TryGetValue(task.OnboardingTemplateId, out var tName)) task.OnboardingTemplateName = tName;
}
@@ -1,10 +1,32 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 合同变量字典服务。
/// </summary>
public interface IOnboardingVariableDefinitionService : ITransient
{
/// <summary>
/// 分页查询合同变量定义。
/// </summary>
Task<SqlSugarPagedList<ObVariableDefinition>> Page(OnboardingVariableDefinitionPageInput input);
/// <summary>
/// 查询全部合同变量定义。
/// </summary>
Task<List<ObVariableDefinition>> ListAll();
/// <summary>
/// 新增合同变量定义。
/// </summary>
Task Add(OnboardingVariableDefinitionAddInput input);
/// <summary>
/// 编辑合同变量定义。
/// </summary>
Task Edit(OnboardingVariableDefinitionEditInput input);
/// <summary>
/// 删除非内置合同变量定义。
/// </summary>
Task Delete(BaseIdListInput input);
}
}
@@ -2,32 +2,79 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 合同变量字典分页查询参数。
/// </summary>
public class OnboardingVariableDefinitionPageInput : BasePageInput
{
/// <summary>
/// 变量编码或名称关键字。
/// </summary>
public string Keyword { get; set; }
/// <summary>
/// 变量分类:employee、hr、company 或 system。
/// </summary>
public string VariableCategory { get; set; }
}
/// <summary>
/// 新增合同变量定义参数。
/// </summary>
public class OnboardingVariableDefinitionAddInput
{
/// <summary>
/// 完整变量编码,例如 employee.EmployeeName 或 hr.BaseSalary。
/// </summary>
[Required(ErrorMessage = "变量编码不能为空")]
public string VariableCode { get; set; }
/// <summary>
/// 后台展示的变量名称。
/// </summary>
[Required(ErrorMessage = "变量名称不能为空")]
public string VariableName { get; set; }
/// <summary>
/// 变量分类:employee、hr、company 或 system。
/// </summary>
[Required(ErrorMessage = "变量分类不能为空")]
public string VariableCategory { get; set; }
/// <summary>
/// 数据类型:string、number 或 date。
/// </summary>
public string DataType { get; set; } = "string";
/// <summary>
/// 未提供变量值时使用的默认值。
/// </summary>
public string DefaultValue { get; set; }
/// <summary>
/// 是否必须填写。
/// </summary>
public bool Required { get; set; }
/// <summary>
/// 展示排序值。
/// </summary>
public int SortCode { get; set; } = 100;
/// <summary>
/// 变量用途说明。
/// </summary>
public string Remark { get; set; }
}
/// <summary>
/// 编辑合同变量定义参数。
/// </summary>
public class OnboardingVariableDefinitionEditInput : OnboardingVariableDefinitionAddInput
{
/// <summary>
/// 变量定义主键。
/// </summary>
[IdNotNull(ErrorMessage = "Id不能为空")]
public long Id { get; set; }
}
}
@@ -1,26 +1,37 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 实现合同变量字典的查询和维护。
/// </summary>
public class OnboardingVariableDefinitionService : IOnboardingVariableDefinitionService
{
private readonly SqlSugarScope _db;
private static bool _initialized = false;
private static readonly object _lock = new();
/// <summary>
/// 初始化合同变量字典服务。
/// </summary>
public OnboardingVariableDefinitionService()
{
_db = DbContext.DB;
}
/// <inheritdoc />
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)
.WhereIF(
!string.IsNullOrWhiteSpace(input.Keyword),
item => item.VariableCode.Contains(input.Keyword)
|| item.VariableName.Contains(input.Keyword))
.WhereIF(
!string.IsNullOrWhiteSpace(input.VariableCategory),
item => item.VariableCategory == input.VariableCategory)
.OrderBy(it => it.SortCode)
.OrderBy(it => it.Id)
.ToPagedListAsync(input.PageNum, input.PageSize);
}
/// <inheritdoc />
public async Task<List<ObVariableDefinition>> ListAll()
{
return await _db.Queryable<ObVariableDefinition>()
@@ -29,26 +40,34 @@ public class OnboardingVariableDefinitionService : IOnboardingVariableDefinition
.ToListAsync();
}
/// <inheritdoc />
public async Task Add(OnboardingVariableDefinitionAddInput input)
{
var code = input.VariableCode.Trim();
var exists = await _db.Queryable<ObVariableDefinition>().AnyAsync(it => it.VariableCode == code);
NormalizeAndValidate(input);
var exists = await _db.Queryable<ObVariableDefinition>()
.AnyAsync(item => item.VariableCode == input.VariableCode);
if (exists)
throw Oops.Bah($"变量编码“{code}”已存在");
throw Oops.Bah($"变量编码“{input.VariableCode}”已存在");
var entity = input.Adapt<ObVariableDefinition>();
entity.VariableCode = code;
entity.IsBuiltIn = false;
await _db.Insertable(entity).ExecuteCommandAsync();
}
/// <inheritdoc />
public async Task Edit(OnboardingVariableDefinitionEditInput input)
{
NormalizeAndValidate(input);
var entity = await _db.Queryable<ObVariableDefinition>().InSingleAsync(input.Id)
?? throw Oops.Bah("变量不存在");
if (!string.Equals(entity.VariableCode, input.VariableCode, StringComparison.Ordinal))
throw Oops.Bah("变量编码创建后不能修改");
entity.VariableName = input.VariableName;
entity.DataType = input.DataType ?? "string";
entity.DataType = input.DataType;
entity.DefaultValue = input.DefaultValue;
entity.Required = input.Required;
entity.SortCode = input.SortCode;
@@ -61,9 +80,11 @@ public class OnboardingVariableDefinitionService : IOnboardingVariableDefinition
await _db.Updateable(entity).ExecuteCommandAsync();
}
/// <inheritdoc />
public async Task Delete(BaseIdListInput input)
{
if (input.Ids.Count == 0) return;
if (input.Ids.Count == 0)
return;
var hasBuiltIn = await _db.Queryable<ObVariableDefinition>()
.Where(it => input.Ids.Contains(it.Id) && it.IsBuiltIn)
@@ -75,4 +96,40 @@ public class OnboardingVariableDefinitionService : IOnboardingVariableDefinition
.Where(it => input.Ids.Contains(it.Id))
.ExecuteCommandAsync();
}
}
/// <summary>
/// 规范化并校验合同变量定义,防止无效编码在生成合同时才暴露。
/// </summary>
private static void NormalizeAndValidate(OnboardingVariableDefinitionAddInput input)
{
input.VariableCode = input.VariableCode?.Trim();
input.VariableName = input.VariableName?.Trim();
input.VariableCategory = input.VariableCategory?.Trim().ToLowerInvariant();
input.DataType = string.IsNullOrWhiteSpace(input.DataType)
? "string"
: input.DataType.Trim().ToLowerInvariant();
input.DefaultValue = input.DefaultValue?.Trim();
input.Remark = input.Remark?.Trim();
if (string.IsNullOrWhiteSpace(input.VariableCode))
throw Oops.Bah("变量编码不能为空");
if (string.IsNullOrWhiteSpace(input.VariableName))
throw Oops.Bah("变量名称不能为空");
var validCategories = new[] { "employee", "hr", "company", "system" };
if (!validCategories.Contains(input.VariableCategory))
throw Oops.Bah("变量分类只能是员工填写、人事填写、公司信息或系统生成");
var codePattern = $@"^{input.VariableCategory}\.[A-Za-z][A-Za-z0-9_]*$";
var codeIsValid = global::System.Text.RegularExpressions.Regex.IsMatch(
input.VariableCode,
codePattern);
if (!codeIsValid)
throw Oops.Bah($"变量编码必须以“{input.VariableCategory}.”开头,后半段只能包含字母、数字和下划线");
var validDataTypes = new[] { "string", "number", "date" };
if (!validDataTypes.Contains(input.DataType))
throw Oops.Bah("数据类型只能是文本、数字或日期");
}
}
@@ -55,6 +55,15 @@ public class OnboardingContractTemplateController
return await _service.Detail(input);
}
/// <summary>
/// 查询入职模板当前公司、部门及岗位可选择的合同模板。
/// </summary>
[HttpPost("list-available")]
public async Task<dynamic> ListAvailable([FromBody] OnboardingContractTemplateAvailableInput input)
{
return await _service.ListAvailable(input);
}
/// <summary>
/// 新增合同模板。
/// </summary>
@@ -135,4 +144,4 @@ public class OnboardingContractTemplateController
{
await _service.EditVariable(input);
}
}
}
@@ -0,0 +1,71 @@
namespace SimpleAdmin.Web.Core;
/// <summary>
/// 提供入职业务部门相关 HTTP 接口。
/// </summary>
[ApiDescriptionSettings("Application", Tag = "入职部门管理")]
[Route("biz/onboarding/department")]
[RolePermission]
public class OnboardingDepartmentController
{
private readonly IOnboardingDepartmentService _service;
public OnboardingDepartmentController(IOnboardingDepartmentService service)
{
_service = service;
}
/// <summary>
/// 分页查询入职业务部门。
/// </summary>
[HttpGet("page")]
public async Task<dynamic> Page([FromQuery] OnboardingDepartmentPageInput input)
{
return await _service.Page(input);
}
/// <summary>
/// 查询指定公司的已启用部门。
/// </summary>
[HttpGet("list-enabled")]
public async Task<dynamic> ListEnabled([FromQuery] long companyId)
{
return await _service.ListEnabled(companyId);
}
/// <summary>
/// 查询部门详情。
/// </summary>
[HttpGet("detail")]
public async Task<dynamic> Detail([FromQuery] BaseIdInput input)
{
return await _service.Detail(input);
}
/// <summary>
/// 新增入职业务部门。
/// </summary>
[HttpPost("add")]
public async Task Add([FromBody] OnboardingDepartmentAddInput input)
{
await _service.Add(input);
}
/// <summary>
/// 编辑入职业务部门。
/// </summary>
[HttpPost("edit")]
public async Task Edit([FromBody] OnboardingDepartmentEditInput input)
{
await _service.Edit(input);
}
/// <summary>
/// 批量删除入职业务部门。
/// </summary>
[HttpPost("delete")]
public async Task Delete([FromBody] BaseIdListInput input)
{
await _service.Delete(input);
}
}
@@ -1,4 +1,4 @@
namespace SimpleAdmin.Web.Core;
namespace SimpleAdmin.Web.Core;
/// <summary>
/// 提供入职业务岗位相关 HTTP 接口。
@@ -33,6 +33,15 @@ public class OnboardingPositionController
return await _service.ListEnabled(companyId);
}
/// <summary>
/// 查询指定部门的已启用岗位。
/// </summary>
[HttpGet("list-by-department")]
public async Task<dynamic> ListByDepartment([FromQuery] long departmentId)
{
return await _service.ListEnabledByDepartment(departmentId);
}
/// <summary>
/// 查询岗位详情。
/// </summary>
@@ -61,11 +70,11 @@ public class OnboardingPositionController
}
/// <summary>
/// 删除入职业务岗位。
/// 批量删除入职业务岗位。
/// </summary>
[HttpPost("delete")]
public async Task Delete([FromBody] BaseIdListInput input)
{
await _service.Delete(input);
}
}
}
@@ -17,24 +17,6 @@ public class OnboardingPublicSubmissionController
_fileService = fileService;
}
/// <summary>
/// 保存员工动态字段。
/// </summary>
[HttpPost("save-dynamic-values")]
public async Task SaveDynamicValues([FromBody] OnboardingSaveDynamicValuesInput input)
{
await _service.SaveDynamicValues(input);
}
/// <summary>
/// 保存员工已上传材料记录。
/// </summary>
[HttpPost("save-material")]
public async Task SaveMaterial([FromBody] OnboardingSaveMaterialInput input)
{
await _service.SaveMaterial(input);
}
/// <summary>
/// 删除当前入职任务中的一条已上传材料。
/// </summary>
@@ -52,11 +52,14 @@ public class OnboardingTemplateController
}
/// <summary>
/// 查询指定公司岗位可使用的入职模板。
/// 查询指定公司、部门和岗位可使用的入职模板。
/// </summary>
[HttpGet("list-enabled")]
public async Task<dynamic> ListEnabled([FromQuery] long companyId, [FromQuery] long positionId)
public async Task<dynamic> ListEnabled(
[FromQuery] long companyId,
[FromQuery] long departmentId,
[FromQuery] long positionId)
{
return await _service.ListEnabled(companyId, positionId);
return await _service.ListEnabled(companyId, departmentId, positionId);
}
}
}
@@ -0,0 +1,80 @@
-- OnboardX 部门模块独立维护及层级关联迁移脚本
-- 适用数据库:MySQL 8.0+
-- 包含:
-- 1. 新建 ob_department 表
-- 2. 在 ob_position / ob_onboarding_task / ob_employee 中补充 DepartmentId 字段
SET NAMES utf8mb4;
-- 1. 新建入职业务部门表
CREATE TABLE IF NOT EXISTS `ob_department` (
`Id` BIGINT NOT NULL COMMENT '主键',
`CompanyId` BIGINT NOT NULL COMMENT '所属业务公司Id',
`DepartmentCode` VARCHAR(50) NOT NULL COMMENT '部门编码',
`DepartmentName` VARCHAR(200) NOT NULL COMMENT '部门名称',
`SortCode` INT NOT NULL DEFAULT 99 COMMENT '显示排序',
`Remark` VARCHAR(500) NULL COMMENT '部门说明',
`ExtJson` LONGTEXT NULL COMMENT '扩展信息',
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '启用状态',
`CreateTime` DATETIME(3) NULL COMMENT '创建时间',
`UpdateTime` DATETIME(3) NULL COMMENT '更新时间',
`CreateUserId` BIGINT NULL COMMENT '创建者Id',
`UpdateUserId` BIGINT NULL COMMENT '修改者Id',
`CreateUser` VARCHAR(200) NULL COMMENT '创建人',
`UpdateUser` VARCHAR(200) NULL COMMENT '更新人',
`IsDelete` TINYINT(1) NULL DEFAULT 0 COMMENT '软删除',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_ob_department_company_code` (`CompanyId`, `DepartmentCode`),
KEY `idx_ob_department_company_status` (`CompanyId`, `Status`, `IsDelete`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='入职业务部门';
-- 2. 定义安全补列存储过程(防重复执行)
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 ;
-- 3. 在岗位表中新增 DepartmentId 字段
CALL `ob_add_column_if_missing`(
'ob_position',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 4. 在入职任务表中新增 DepartmentId 字段
CALL `ob_add_column_if_missing`(
'ob_onboarding_task',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 5. 在正式员工档案表中新增 DepartmentId 字段
CALL `ob_add_column_if_missing`(
'ob_employee',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 清理临时存储过程
DROP PROCEDURE IF EXISTS `ob_add_column_if_missing`;
@@ -0,0 +1,343 @@
-- OnboardX 公司、部门、岗位、合同模板及入职模板层级升级脚本
-- 适用数据库:MySQL 8.0+
-- 说明:
-- 1. 可重复执行,不会覆盖已经人工维护的归属数据。
-- 2. 仅对能够从现有岗位/入职模板关系中唯一确定的数据执行安全回填。
-- 3. 脚本末尾会输出仍需人工处理的数据,请先处理完再启用相关模板。
SET NAMES utf8mb4;
DROP PROCEDURE IF EXISTS `ob_scope_add_column_if_missing`;
DELIMITER $$
CREATE PROCEDURE `ob_scope_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 ;
DROP PROCEDURE IF EXISTS `ob_scope_add_index_if_missing`;
DELIMITER $$
CREATE PROCEDURE `ob_scope_add_index_if_missing`(
IN p_table_name VARCHAR(128),
IN p_index_name VARCHAR(128),
IN p_index_definition VARCHAR(1000)
)
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = p_table_name
AND INDEX_NAME = p_index_name
) THEN
SET @ddl = CONCAT(
'ALTER TABLE `', p_table_name, '` ADD ', p_index_definition
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END$$
DELIMITER ;
DROP PROCEDURE IF EXISTS `ob_scope_add_unique_index_if_clean`;
DELIMITER $$
CREATE PROCEDURE `ob_scope_add_unique_index_if_clean`(
IN p_table_name VARCHAR(128),
IN p_column_name VARCHAR(128),
IN p_index_name VARCHAR(128)
)
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = p_table_name
AND INDEX_NAME = p_index_name
) THEN
SET @duplicate_count = 0;
SET @check_sql = CONCAT(
'SELECT COUNT(*) INTO @duplicate_count FROM (',
'SELECT `', p_column_name, '` FROM `', p_table_name, '` ',
'WHERE `', p_column_name, '` IS NOT NULL ',
'AND LENGTH(TRIM(`', p_column_name, '`)) > 0 ',
'GROUP BY `', p_column_name, '` HAVING COUNT(*) > 1',
') duplicated_values'
);
PREPARE stmt FROM @check_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
IF @duplicate_count = 0 THEN
SET @ddl = CONCAT(
'ALTER TABLE `', p_table_name, '` ADD UNIQUE INDEX `',
p_index_name, '` (`', p_column_name, '`)'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END IF;
END$$
DELIMITER ;
-- 入职模板增加所属部门。
CALL `ob_scope_add_column_if_missing`(
'ob_onboarding_template',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 合同模板增加所属公司和部门。暂时允许 NULL,以兼容待人工归类的历史模板。
CALL `ob_scope_add_column_if_missing`(
'ob_contract_template',
'CompanyId',
'BIGINT NULL COMMENT ''所属公司Id'' AFTER `Id`'
);
CALL `ob_scope_add_column_if_missing`(
'ob_contract_template',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 兼容尚未执行上一版部门迁移的数据库,重复调用不会新增重复字段。
CALL `ob_scope_add_column_if_missing`(
'ob_position',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
CALL `ob_scope_add_column_if_missing`(
'ob_onboarding_task',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
CALL `ob_scope_add_column_if_missing`(
'ob_employee',
'DepartmentId',
'BIGINT NULL COMMENT ''所属部门Id'' AFTER `CompanyId`'
);
-- 合同模板与岗位为多对多关系,一份通用合同可同时适用于多个岗位。
CREATE TABLE IF NOT EXISTS `ob_contract_template_position` (
`Id` BIGINT NOT NULL COMMENT '主键',
`ContractTemplateId` BIGINT NOT NULL COMMENT '合同模板Id',
`PositionId` BIGINT NOT NULL COMMENT '适用岗位Id',
`ExtJson` LONGTEXT NULL COMMENT '扩展信息',
`Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '启用状态',
`CreateTime` DATETIME(3) NULL COMMENT '创建时间',
`UpdateTime` DATETIME(3) NULL COMMENT '更新时间',
`CreateUserId` BIGINT NULL COMMENT '创建者Id',
`UpdateUserId` BIGINT NULL COMMENT '修改者Id',
`CreateUser` VARCHAR(200) NULL COMMENT '创建人',
`UpdateUser` VARCHAR(200) NULL COMMENT '更新人',
`IsDelete` TINYINT(1) NULL DEFAULT 0 COMMENT '软删除',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_ob_contract_template_position` (`ContractTemplateId`, `PositionId`),
KEY `idx_ob_contract_template_position_position` (`PositionId`, `ContractTemplateId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同模板适用岗位';
CALL `ob_scope_add_index_if_missing`(
'ob_position',
'idx_ob_position_department_status',
'INDEX `idx_ob_position_department_status` (`DepartmentId`, `Status`, `IsDelete`)'
);
CALL `ob_scope_add_index_if_missing`(
'ob_onboarding_template',
'idx_ob_onboarding_template_scope',
'INDEX `idx_ob_onboarding_template_scope` (`CompanyId`, `DepartmentId`, `Status`, `IsDelete`)'
);
CALL `ob_scope_add_index_if_missing`(
'ob_contract_template',
'idx_ob_contract_template_scope',
'INDEX `idx_ob_contract_template_scope` (`CompanyId`, `DepartmentId`, `Status`, `IsDelete`)'
);
CALL `ob_scope_add_index_if_missing`(
'ob_onboarding_task',
'idx_ob_onboarding_task_department',
'INDEX `idx_ob_onboarding_task_department` (`DepartmentId`, `TaskStatus`, `IsDelete`)'
);
CALL `ob_scope_add_index_if_missing`(
'ob_employee',
'idx_ob_employee_department',
'INDEX `idx_ob_employee_department` (`DepartmentId`, `IsDelete`)'
);
-- 身份证号和手机号是正式员工防重复的数据库最终防线。
-- 若历史数据已经重复,本次会安全跳过建索引,并由脚本末尾查询列出冲突数据。
CALL `ob_scope_add_unique_index_if_clean`(
'ob_employee',
'IdCardNumber',
'uk_ob_employee_id_card'
);
CALL `ob_scope_add_unique_index_if_clean`(
'ob_employee',
'Phone',
'uk_ob_employee_phone'
);
-- 安全回填入职模板部门:只有全部适用岗位均属于同一个非空部门时才回填。
UPDATE `ob_onboarding_template` template
INNER JOIN (
SELECT
relation.`OnboardingTemplateId`,
MIN(position.`DepartmentId`) AS `DepartmentId`
FROM `ob_onboarding_template_position` relation
INNER JOIN `ob_position` position ON position.`Id` = relation.`PositionId`
WHERE COALESCE(relation.`IsDelete`, 0) = 0
AND COALESCE(position.`IsDelete`, 0) = 0
GROUP BY relation.`OnboardingTemplateId`
HAVING COUNT(*) > 0
AND COUNT(DISTINCT position.`DepartmentId`) = 1
AND SUM(position.`DepartmentId` IS NULL) = 0
) source ON source.`OnboardingTemplateId` = template.`Id`
SET template.`DepartmentId` = source.`DepartmentId`
WHERE template.`DepartmentId` IS NULL;
-- 安全回填合同模板公司、部门:只有历史引用的入职模板归属完全一致时才回填。
UPDATE `ob_contract_template` contract_template
INNER JOIN (
SELECT
relation.`ContractTemplateId`,
MIN(template.`CompanyId`) AS `CompanyId`,
MIN(template.`DepartmentId`) AS `DepartmentId`
FROM `ob_onboarding_template_contract` relation
INNER JOIN `ob_onboarding_template` template
ON template.`Id` = relation.`OnboardingTemplateId`
WHERE COALESCE(relation.`IsDelete`, 0) = 0
AND COALESCE(template.`IsDelete`, 0) = 0
GROUP BY relation.`ContractTemplateId`
HAVING COUNT(DISTINCT template.`CompanyId`) = 1
AND COUNT(DISTINCT template.`DepartmentId`) = 1
AND SUM(template.`DepartmentId` IS NULL) = 0
) source ON source.`ContractTemplateId` = contract_template.`Id`
SET contract_template.`CompanyId` = COALESCE(contract_template.`CompanyId`, source.`CompanyId`),
contract_template.`DepartmentId` = COALESCE(contract_template.`DepartmentId`, source.`DepartmentId`)
WHERE contract_template.`CompanyId` IS NULL
OR contract_template.`DepartmentId` IS NULL;
-- 根据历史入职模板关系,回填合同模板已经实际使用过的岗位范围。
INSERT IGNORE INTO `ob_contract_template_position` (
`Id`,
`ContractTemplateId`,
`PositionId`,
`Status`,
`CreateTime`,
`IsDelete`
)
SELECT DISTINCT
CAST(CONV(SUBSTRING(MD5(CONCAT(
'obctp:', contract_relation.`ContractTemplateId`, ':', position_relation.`PositionId`
)), 1, 15), 16, 10) AS UNSIGNED) AS `Id`,
contract_relation.`ContractTemplateId`,
position_relation.`PositionId`,
'enable',
NOW(3),
0
FROM `ob_onboarding_template_contract` contract_relation
INNER JOIN `ob_onboarding_template_position` position_relation
ON position_relation.`OnboardingTemplateId` = contract_relation.`OnboardingTemplateId`
INNER JOIN `ob_contract_template` contract_template
ON contract_template.`Id` = contract_relation.`ContractTemplateId`
INNER JOIN `ob_position` position
ON position.`Id` = position_relation.`PositionId`
WHERE COALESCE(contract_relation.`IsDelete`, 0) = 0
AND COALESCE(position_relation.`IsDelete`, 0) = 0
AND contract_template.`CompanyId` = position.`CompanyId`
AND contract_template.`DepartmentId` = position.`DepartmentId`;
DROP PROCEDURE IF EXISTS `ob_scope_add_unique_index_if_clean`;
DROP PROCEDURE IF EXISTS `ob_scope_add_index_if_missing`;
DROP PROCEDURE IF EXISTS `ob_scope_add_column_if_missing`;
-- 结果 1:仍未明确部门归属的岗位。必须在岗位管理中补齐。
SELECT
position.`Id`,
position.`PositionCode`,
position.`PositionName`,
position.`CompanyId`
FROM `ob_position` position
WHERE position.`DepartmentId` IS NULL
AND COALESCE(position.`IsDelete`, 0) = 0;
-- 结果 2:仍未明确部门归属的入职模板。必须编辑并重新保存。
SELECT
template.`Id`,
template.`TemplateCode`,
template.`TemplateName`,
template.`CompanyId`
FROM `ob_onboarding_template` template
WHERE template.`DepartmentId` IS NULL
AND COALESCE(template.`IsDelete`, 0) = 0;
-- 结果 3:仍未明确公司/部门或没有适用岗位的合同模板。必须编辑并重新保存。
SELECT
contract_template.`Id`,
contract_template.`TemplateCode`,
contract_template.`TemplateName`,
contract_template.`CompanyId`,
contract_template.`DepartmentId`,
COUNT(position_relation.`Id`) AS `PositionCount`
FROM `ob_contract_template` contract_template
LEFT JOIN `ob_contract_template_position` position_relation
ON position_relation.`ContractTemplateId` = contract_template.`Id`
AND COALESCE(position_relation.`IsDelete`, 0) = 0
WHERE COALESCE(contract_template.`IsDelete`, 0) = 0
GROUP BY
contract_template.`Id`,
contract_template.`TemplateCode`,
contract_template.`TemplateName`,
contract_template.`CompanyId`,
contract_template.`DepartmentId`
HAVING contract_template.`CompanyId` IS NULL
OR contract_template.`DepartmentId` IS NULL
OR COUNT(position_relation.`Id`) = 0;
-- 结果 4:检查层级不一致的数据,正常情况下应返回 0 行。
SELECT
contract_template.`Id` AS `ContractTemplateId`,
contract_template.`TemplateName`,
position.`Id` AS `PositionId`,
position.`PositionName`
FROM `ob_contract_template` contract_template
INNER JOIN `ob_contract_template_position` relation
ON relation.`ContractTemplateId` = contract_template.`Id`
INNER JOIN `ob_position` position ON position.`Id` = relation.`PositionId`
WHERE contract_template.`CompanyId` <> position.`CompanyId`
OR contract_template.`DepartmentId` <> position.`DepartmentId`;
-- 结果 5:正式员工身份证号或手机号重复时,唯一索引会被安全跳过;请清理后重新运行脚本。
SELECT
'IdCardNumber' AS `ConflictField`,
employee.`IdCardNumber` AS `ConflictValue`,
COUNT(*) AS `ConflictCount`
FROM `ob_employee` employee
WHERE NULLIF(TRIM(employee.`IdCardNumber`), '') IS NOT NULL
GROUP BY employee.`IdCardNumber`
HAVING COUNT(*) > 1
UNION ALL
SELECT
'Phone' AS `ConflictField`,
employee.`Phone` AS `ConflictValue`,
COUNT(*) AS `ConflictCount`
FROM `ob_employee` employee
WHERE NULLIF(TRIM(employee.`Phone`), '') IS NOT NULL
GROUP BY employee.`Phone`
HAVING COUNT(*) > 1;