namespace SimpleAdmin.Application; /// /// 员工手机端公开入口,仅凭一次性任务链接访问入职资料。 /// public class OnboardingPublicTaskService : IOnboardingPublicTaskService { private readonly SqlSugarScope _db; private readonly IOnboardingContractSnapshotService _snapshotService; private readonly IOnboardingContractDocumentService _contractDocumentService; public OnboardingPublicTaskService( IOnboardingContractSnapshotService snapshotService, IOnboardingContractDocumentService contractDocumentService) { _db = DbContext.DB; _snapshotService = snapshotService; _contractDocumentService = contractDocumentService; } /// /// 通过有效 Token 获取任务创建时冻结的材料、合同和新版入职登记表信息。 /// public async Task Detail(OnboardingTaskPublicAccessInput input) { var link = await GetActiveLink(input.Token); var task = await _db.Queryable().InSingleAsync(link.TaskId) ?? throw Oops.Bah("入职任务不存在"); link.AccessCount++; link.LastAccessedAt = DateTime.Now; await _db.Updateable(link).ExecuteCommandAsync(); var materialRequirements = await _db.Queryable() .Where(item => item.TaskId == task.Id) .OrderBy(item => item.SortCode) .ToListAsync(); var contracts = await _db.Queryable() .Where(item => item.TaskId == task.Id) .ToListAsync(); var materials = await _db.Queryable() .Where(item => item.TaskId == task.Id) .ToListAsync(); var materialFileIds = materials .Select(item => item.FileId) .Distinct() .ToList(); var materialFiles = materialFileIds.Count == 0 ? new List() : await _db.Queryable() .Where(item => materialFileIds.Contains(item.Id)) .ToListAsync(); var materialFileMap = materialFiles.ToDictionary(item => item.Id); var materialOutputs = materials.Select(item => { materialFileMap.TryGetValue(item.FileId, out var file); return new OnboardingPublicTaskMaterialOutput { Id = item.Id, MaterialTypeId = item.MaterialTypeId, MaterialName = item.MaterialName, FileId = item.FileId, FileName = file?.Name, FileSuffix = file?.Suffix, FileSizeInfo = file?.SizeInfo, UploadedAt = item.UploadedAt, ReviewStatus = item.ReviewStatus, ReviewRemark = item.ReviewRemark }; }).ToList(); var templateVersionIds = contracts .Select(item => item.TemplateVersionId) .Distinct() .ToList(); var contractVariables = new List(); if (templateVersionIds.Count > 0) { var employeeVariables = await _db.Queryable() .Where(item => templateVersionIds.Contains(item.TemplateVersionId) && item.VariableType == ContractVariableType.Employee) .OrderBy(item => item.SortCode) .ToListAsync(); contractVariables = employeeVariables.Select(item => new OnboardingPublicContractVariableOutput { VariableCode = item.VariableCode, VariableName = item.VariableName, VariableType = item.VariableType, DataType = item.DataType, DefaultValue = item.DefaultValue, OptionsJson = item.OptionsJson, Required = item.Required }).ToList(); } return new OnboardingTaskPublicDetailOutput { Task = task, EmployeeInfo = await _db.Queryable() .FirstAsync(item => item.TaskId == task.Id), Contracts = contracts, DynamicValues = new List(), Materials = materialOutputs, FormFields = new List(), MaterialRequirements = materialRequirements.Select(requirement => new OnboardingPublicMaterialOutput { MaterialTypeId = requirement.MaterialTypeId, MaterialName = requirement.MaterialName, AllowedExtensions = requirement.AllowedExtensions, MaxFileCount = requirement.MaxFileCount, MaxFileSizeMb = requirement.MaxFileSizeMb, Required = requirement.Required }).ToList(), ContractVariables = contractVariables }; } /// /// 保存候选人的固定基础信息并刷新尚未确认的合同变量快照。 /// public async Task SaveEmployeeInfo(OnboardingTaskPublicSaveInput input) { var link = await GetActiveLink(input.Token); var task = await _db.Queryable().InSingleAsync(link.TaskId) ?? throw Oops.Bah("入职任务不存在"); if (task.TaskStatus != OnboardingTaskStatus.PendingFill && task.TaskStatus != OnboardingTaskStatus.Rejected) { throw Oops.Bah("当前任务不可编辑"); } var old = await _db.Queryable() .FirstAsync(item => item.TaskId == task.Id); var entity = input.Adapt(); entity.TaskId = task.Id; entity.IsDelete = false; if (old == null) { entity.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(); entity.Status = CommonStatusConst.ENABLE; entity.CreateTime = DateTime.Now; entity.CreateUserId = null; entity.CreateUser = null; await _db.Insertable(entity).ExecuteCommandAsync(); } else { entity.Id = old.Id; entity.Status = old.Status; entity.CreateTime = old.CreateTime; entity.CreateUserId = old.CreateUserId; entity.CreateUser = old.CreateUser; entity.ExtJson = old.ExtJson; entity.IsDelete = old.IsDelete; entity.UpdateTime = DateTime.Now; entity.UpdateUserId = null; entity.UpdateUser = null; await _db.Updateable(entity).ExecuteCommandAsync(); } await _snapshotService.Build(task.Id, entity, invalidateGeneratedPreview: true); } /// /// 根据员工当前已填写信息生成合同预览,并返回已替换变量的 DOCX 文件 Id。 /// public async Task GetContractSourceFileId(OnboardingPublicContractSourceInput input) { var link = await GetActiveLink(input.Token); var contract = await _db.Queryable() .FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == link.TaskId) ?? throw Oops.Bah("合同不存在"); await _snapshotService.Build(link.TaskId); var fileId = await _contractDocumentService.Generate(new OnboardingGenerateContractInput { TaskContractId = contract.Id }); contract.ReadAt ??= DateTime.Now; if (contract.ContractStatus == TaskContractStatus.PendingGenerate) contract.ContractStatus = TaskContractStatus.PendingConfirm; await _db.Updateable(contract).ExecuteCommandAsync(); return fileId; } /// /// 校验一次性链接与材料归属,避免通过文件 Id 越权读取其他任务文件。 /// public async Task GetMaterialSource( OnboardingPublicMaterialSourceInput input) { var link = await GetActiveLink(input.Token); var material = await _db.Queryable() .FirstAsync(item => item.Id == input.TaskMaterialId && item.TaskId == link.TaskId) ?? throw Oops.Bah("上传材料不存在"); var file = await _db.Queryable().InSingleAsync(material.FileId) ?? throw Oops.Bah("上传文件不存在"); return new OnboardingPublicTaskMaterialOutput { Id = material.Id, MaterialTypeId = material.MaterialTypeId, MaterialName = material.MaterialName, FileId = material.FileId, FileName = file.Name, FileSuffix = file.Suffix, FileSizeInfo = file.SizeInfo, UploadedAt = material.UploadedAt, ReviewStatus = material.ReviewStatus, ReviewRemark = material.ReviewRemark }; } private async Task GetActiveLink(string token) { if (string.IsNullOrWhiteSpace(token)) throw Oops.Bah("入职链接 Token 不能为空"); var hash = Convert.ToHexString( global::System.Security.Cryptography.SHA256.HashData( global::System.Text.Encoding.UTF8.GetBytes(token))); var link = await _db.Queryable().FirstAsync(item => item.TokenHash == hash) ?? throw Oops.Bah("链接不存在"); if (link.LinkStatus != OnboardingLinkStatus.Active || link.ExpiresAt <= DateTime.Now) throw Oops.Bah("链接已失效"); return link; } }