249 lines
11 KiB
C#
249 lines
11 KiB
C#
namespace SimpleAdmin.Application;
|
|
|
|
/// <summary>
|
|
/// 实现员工端动态资料、上传材料、合同确认和最终提交能力。
|
|
/// </summary>
|
|
public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionService
|
|
{
|
|
private readonly SqlSugarScope _db;
|
|
private readonly IOnboardingContractSnapshotService _snapshotService;
|
|
|
|
public OnboardingPublicSubmissionService(IOnboardingContractSnapshotService snapshotService)
|
|
{
|
|
_db = DbContext.DB;
|
|
_snapshotService = snapshotService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 保存候选人填写的动态补充字段值。
|
|
/// </summary>
|
|
public async Task SaveDynamicValues(OnboardingSaveDynamicValuesInput input)
|
|
{
|
|
await GetEditableTask(input.Token);
|
|
throw Oops.Bah("新版入职登记表不再使用旧动态字段接口,请通过员工基础信息接口保存补充变量");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 关联已上传 OSS 文件与任务材料要求。
|
|
/// </summary>
|
|
public async Task SaveMaterial(OnboardingSaveMaterialInput input)
|
|
{
|
|
var task = await GetEditableTask(input.Token);
|
|
var requirement = await _db.Queryable<ObTaskMaterialRequirement>()
|
|
.FirstAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
|
?? throw Oops.Bah("材料类型不属于当前入职模板");
|
|
var fileExists = await _db.Queryable<SysFile>().AnyAsync(item => item.Id == input.FileId);
|
|
if (!fileExists)
|
|
throw Oops.Bah("上传文件不存在");
|
|
|
|
var existingMaterials = await _db.Queryable<ObTaskMaterial>()
|
|
.Where(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
|
.ToListAsync();
|
|
var supplementMaterial = existingMaterials.FirstOrDefault(
|
|
item => item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
|
|
if (supplementMaterial != null)
|
|
{
|
|
supplementMaterial.FileId = input.FileId;
|
|
supplementMaterial.SubmitRound = task.SubmitCount + 1;
|
|
supplementMaterial.UploadedAt = DateTime.Now;
|
|
supplementMaterial.ReviewStatus = TaskMaterialReviewStatus.Pending;
|
|
supplementMaterial.ReviewRemark = null;
|
|
await _db.Updateable(supplementMaterial).ExecuteCommandAsync();
|
|
return;
|
|
}
|
|
|
|
if (existingMaterials.Count >= requirement.MaxFileCount)
|
|
throw Oops.Bah("该材料已达到最大上传数量");
|
|
|
|
await _db.Insertable(new ObTaskMaterial
|
|
{
|
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
|
TaskId = task.Id,
|
|
MaterialTypeId = requirement.MaterialTypeId,
|
|
MaterialName = requirement.MaterialName,
|
|
FileId = input.FileId,
|
|
SubmitRound = task.SubmitCount + 1,
|
|
UploadedAt = DateTime.Now
|
|
}).ExecuteCommandAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 任务仍可编辑时删除一条已上传材料。
|
|
/// </summary>
|
|
public async Task DeleteMaterial(
|
|
OnboardingDeleteMaterialInput input)
|
|
{
|
|
var task = await GetEditableTask(input.Token);
|
|
var material = await _db.Queryable<ObTaskMaterial>()
|
|
.FirstAsync(item =>
|
|
item.Id == input.TaskMaterialId
|
|
&& item.TaskId == task.Id);
|
|
if (material == null)
|
|
throw Oops.Bah("上传材料不存在");
|
|
|
|
await _db.Deleteable<ObTaskMaterial>()
|
|
.In(material.Id)
|
|
.ExecuteCommandAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 记录候选人已经查看并确认动态生成的合同。
|
|
/// </summary>
|
|
public async Task ConfirmContract(OnboardingConfirmContractInput input)
|
|
{
|
|
var task = await GetEditableTask(input.Token);
|
|
var contract = await _db.Queryable<ObTaskContract>()
|
|
.FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == task.Id)
|
|
?? throw Oops.Bah("合同不存在");
|
|
if (!contract.GeneratedDocxFileId.HasValue || !contract.ReadAt.HasValue)
|
|
throw Oops.Bah("请先查看已经生成的合同内容");
|
|
|
|
contract.ConfirmedAt = DateTime.Now;
|
|
contract.ContractStatus = TaskContractStatus.Confirmed;
|
|
await _db.Updateable(contract).ExecuteCommandAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 校验固定信息、动态字段、材料、合同变量和合同确认后提交审核。
|
|
/// </summary>
|
|
public async Task Submit(OnboardingSubmitInput input)
|
|
{
|
|
var task = await GetEditableTask(input.Token);
|
|
var info = await _db.Queryable<ObTaskEmployeeInfo>()
|
|
.FirstAsync(item => item.TaskId == task.Id);
|
|
var basicInfoIncomplete = info == null
|
|
|| string.IsNullOrWhiteSpace(info.EmployeeName)
|
|
|| string.IsNullOrWhiteSpace(info.Phone)
|
|
|| string.IsNullOrWhiteSpace(info.IdCardNumber)
|
|
|| string.IsNullOrWhiteSpace(info.BankCardNumber)
|
|
|| string.IsNullOrWhiteSpace(info.BankName);
|
|
if (basicInfoIncomplete)
|
|
throw Oops.Bah("请先完整填写基本信息");
|
|
|
|
await _snapshotService.Build(task.Id, info);
|
|
|
|
var requiredMaterials = (await GetMaterialRequirements(task.Id))
|
|
.Where(item => item.Required)
|
|
.Select(item => item.MaterialTypeId)
|
|
.ToList();
|
|
var materials = await _db.Queryable<ObTaskMaterial>()
|
|
.Where(item => item.TaskId == task.Id)
|
|
.ToListAsync();
|
|
var hasMissingRequiredMaterial = requiredMaterials.Any(
|
|
id => materials.All(item => item.MaterialTypeId != id));
|
|
if (hasMissingRequiredMaterial)
|
|
throw Oops.Bah("仍有必传材料未上传");
|
|
|
|
await EnsureRequiredContractVariables(task);
|
|
|
|
var hasUnconfirmedRequiredContract = await _db.Queryable<ObTaskContract>()
|
|
.AnyAsync(item => item.TaskId == task.Id && item.Required && item.ConfirmedAt == null);
|
|
if (hasUnconfirmedRequiredContract)
|
|
throw Oops.Bah("请查看并确认全部必签合同");
|
|
|
|
task.TaskStatus = OnboardingTaskStatus.Reviewing;
|
|
task.SubmitCount++;
|
|
task.LastSubmittedAt = DateTime.Now;
|
|
task.RejectReason = null;
|
|
await _db.Updateable(task).ExecuteCommandAsync();
|
|
|
|
await _db.Insertable(new ObTaskAudit
|
|
{
|
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
|
TaskId = task.Id,
|
|
AuditRound = task.SubmitCount,
|
|
AuditAction = OnboardingAuditAction.Submit,
|
|
AuditOpinion = "员工提交入职资料",
|
|
SnapshotJson = global::System.Text.Json.JsonSerializer.Serialize(new { info, materials }),
|
|
AuditedAt = DateTime.Now
|
|
}).ExecuteCommandAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 上传 OSS 前按照任务材料快照校验扩展名、文件大小和数量。
|
|
/// </summary>
|
|
public async Task ValidateMaterialUpload(OnboardingMaterialUploadValidationInput input)
|
|
{
|
|
var task = await GetEditableTask(input.Token);
|
|
var requirement = await _db.Queryable<ObTaskMaterialRequirement>()
|
|
.FirstAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
|
?? throw Oops.Bah("材料类型不属于当前入职模板");
|
|
var suffix = Path.GetExtension(input.FileName).TrimStart('.').ToLowerInvariant();
|
|
var allowedExtensions = (requirement.AllowedExtensions ?? string.Empty)
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Select(item => item.TrimStart('.').ToLowerInvariant());
|
|
var suffixNotAllowed = allowedExtensions.Any() && !allowedExtensions.Contains(suffix);
|
|
if (suffixNotAllowed)
|
|
throw Oops.Bah("该材料不允许上传此文件类型");
|
|
|
|
var fileTooLarge = requirement.MaxFileSizeMb.HasValue
|
|
&& input.FileLength > requirement.MaxFileSizeMb.Value * 1024L * 1024L;
|
|
if (fileTooLarge)
|
|
throw Oops.Bah("文件超过允许的大小");
|
|
|
|
var materials = await _db.Queryable<ObTaskMaterial>()
|
|
.Where(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
|
.ToListAsync();
|
|
var canReplaceSupplement = materials.Any(
|
|
item => item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
|
|
if (materials.Count >= requirement.MaxFileCount && !canReplaceSupplement)
|
|
throw Oops.Bah("该材料已达到最大上传数量");
|
|
}
|
|
|
|
private async Task EnsureRequiredContractVariables(ObOnboardingTask task)
|
|
{
|
|
var contracts = await _db.Queryable<ObTaskContract>()
|
|
.Where(item => item.TaskId == task.Id)
|
|
.ToListAsync();
|
|
var contractIds = contracts.Select(contract => contract.Id).ToList();
|
|
var templateVersionIds = contracts.Select(contract => contract.TemplateVersionId).ToList();
|
|
var variables = await _db.Queryable<ObTaskContractVariable>()
|
|
.Where(item => contractIds.Contains(item.TaskContractId))
|
|
.ToListAsync();
|
|
var definitions = await _db.Queryable<ObTemplateVariable>()
|
|
.Where(item => templateVersionIds.Contains(item.TemplateVersionId) && item.Required)
|
|
.ToListAsync();
|
|
var missing = contracts
|
|
.SelectMany(contract => definitions
|
|
.Where(definition => definition.TemplateVersionId == contract.TemplateVersionId)
|
|
.Select(definition => new { contract, definition }))
|
|
.FirstOrDefault(item => string.IsNullOrWhiteSpace(
|
|
variables.FirstOrDefault(variable => variable.TaskContractId == item.contract.Id
|
|
&& variable.VariableCode == item.definition.VariableCode)
|
|
?.VariableValue));
|
|
if (missing != null)
|
|
throw Oops.Bah($"合同“{missing.contract.ContractName}”的必填变量“{missing.definition.VariableName}”尚未填写");
|
|
}
|
|
|
|
private Task<List<ObTaskMaterialRequirement>> GetMaterialRequirements(long taskId)
|
|
{
|
|
return _db.Queryable<ObTaskMaterialRequirement>()
|
|
.Where(item => item.TaskId == taskId)
|
|
.OrderBy(item => item.SortCode)
|
|
.ToListAsync();
|
|
}
|
|
|
|
private async Task<ObOnboardingTask> GetEditableTask(string token)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(token))
|
|
throw Oops.Bah("入职链接 Token 不能为空");
|
|
|
|
var hash = Convert.ToHexString(
|
|
global::System.Security.Cryptography.SHA256.HashData(
|
|
global::System.Text.Encoding.UTF8.GetBytes(token)));
|
|
var link = await _db.Queryable<ObTaskLink>().FirstAsync(item => item.TokenHash == hash)
|
|
?? throw Oops.Bah("链接不存在");
|
|
var linkUnavailable = link.LinkStatus != OnboardingLinkStatus.Active || link.ExpiresAt <= DateTime.Now;
|
|
if (linkUnavailable)
|
|
throw Oops.Bah("链接已失效");
|
|
|
|
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(link.TaskId)
|
|
?? throw Oops.Bah("入职任务不存在");
|
|
var taskNotEditable = task.TaskStatus != OnboardingTaskStatus.PendingFill
|
|
&& task.TaskStatus != OnboardingTaskStatus.Rejected;
|
|
if (taskNotEditable)
|
|
throw Oops.Bah("当前任务不可编辑");
|
|
|
|
return task;
|
|
}
|
|
} |