From db3d58bc99a201dbc7b5f667f67e6f652efcc77f Mon Sep 17 00:00:00 2001 From: fuenmao <980740792@qq.com> Date: Wed, 26 Aug 2026 15:30:31 +0800 Subject: [PATCH] 1111 --- .../Entity/Onboarding/ObOnboardingTask.cs | 31 ++-- .../Entity/Onboarding/ObTaskLink.cs | 23 ++- ...OnboardingMaterialUploadValidationInput.cs | 5 + .../Dto/OnboardingPublicSubmissionInput.cs | 5 + .../OnboardingPublicSubmissionService.cs | 26 ++- .../IOnboardingPublicTaskService.cs | 6 + .../PublicTask/OnboardingPublicTaskService.cs | 65 ++++++- .../Task/Dto/OnboardingTaskPublicInput.cs | 81 ++++++++- .../Onboarding/Task/OnboardingTaskService.cs | 1 + .../TaskQuery/IOnboardingTaskQueryService.cs | 5 + .../TaskQuery/OnboardingTaskQueryService.cs | 158 +++++++++++++++++- .../SimpleAdmin.Application.xml | 134 ++++++++++++++- .../OnboardingPublicSubmissionController.cs | 7 +- .../OnboardingPublicTaskController.cs | 31 ++++ .../OnboardingTaskQueryController.cs | 9 + SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml | 17 +- 16 files changed, 565 insertions(+), 39 deletions(-) diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs index dd0cf51..a990480 100644 --- a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs +++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs @@ -1,14 +1,4 @@ -// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 -// -// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: -// 1.请不要删除和修改根目录下的LICENSE文件。 -// 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 -// 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin -// 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。 -// 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为。 -// 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关。 - -namespace SimpleAdmin.Application; +namespace SimpleAdmin.Application; /// /// 新员工入职任务,承载候选人填写、审核、转正式员工和归档全过程。 @@ -86,4 +76,23 @@ public class ObOnboardingTask : BaseEntity /// 业务备注。 /// public string Remark { get; set; } + + /// + /// 公司名称 (展示用) + /// + [SugarColumn(IsIgnore = true)] + public string CompanyName { get; set; } + + /// + /// 岗位名称 (展示用) + /// + [SugarColumn(IsIgnore = true)] + public string PositionName { get; set; } + + /// + /// 入职模板名称 (展示用) + /// + [SugarColumn(IsIgnore = true)] + public string OnboardingTemplateName { get; set; } + } diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObTaskLink.cs b/SimpleAdmin.Application/Entity/Onboarding/ObTaskLink.cs index 7de1398..9ff8391 100644 --- a/SimpleAdmin.Application/Entity/Onboarding/ObTaskLink.cs +++ b/SimpleAdmin.Application/Entity/Onboarding/ObTaskLink.cs @@ -1,17 +1,7 @@ -// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 -// -// SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: -// 1.请不要删除和修改根目录下的LICENSE文件。 -// 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 -// 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin -// 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。 -// 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为。 -// 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关。 - -namespace SimpleAdmin.Application; +namespace SimpleAdmin.Application; /// -/// 新员工专属填写链接,仅保存 Token 哈希及链接生命周期信息。 +/// 新员工专属填写链接。 /// [SugarTable("ob_task_link", TableDescription = "新员工专属填写链接")] [Tenant(SqlSugarConst.DB_DEFAULT)] @@ -23,7 +13,14 @@ public class ObTaskLink : BaseEntity public long TaskId { get; set; } /// - /// 访问 Token 的 SHA-256 哈希,不保存明文 Token。 + /// 明文 Token,仅供受保护的管理端链接接口内部读取。 + /// + [SugarColumn(IsNullable = true)] + [global::System.Text.Json.Serialization.JsonIgnore] + public string PlainToken { get; set; } + + /// + /// 访问 Token 的 SHA-256 哈希。 /// public string TokenHash { get; set; } diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingMaterialUploadValidationInput.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingMaterialUploadValidationInput.cs index e78792b..87d5630 100644 --- a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingMaterialUploadValidationInput.cs +++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingMaterialUploadValidationInput.cs @@ -24,4 +24,9 @@ public class OnboardingMaterialUploadValidationInput /// 待上传文件大小,单位为字节。 /// public long FileLength { get; set; } + + /// + /// 需要重新上传并替换的任务材料记录 Id;为空时按新增数量校验。 + /// + public long? TaskMaterialId { get; set; } } diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingPublicSubmissionInput.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingPublicSubmissionInput.cs index 4f500fb..07a3272 100644 --- a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingPublicSubmissionInput.cs +++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingPublicSubmissionInput.cs @@ -53,6 +53,11 @@ public class OnboardingSaveMaterialInput /// 系统文件 Id。 /// public long FileId { get; set; } + + /// + /// 需要重新上传并替换的任务材料记录 Id;为空时新增材料。 + /// + public long? TaskMaterialId { get; set; } } /// diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs index 053074e..9776b42 100644 --- a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs @@ -39,6 +39,20 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ var existingMaterials = await _db.Queryable() .Where(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId) .ToListAsync(); + if (input.TaskMaterialId.HasValue) + { + var replacement = existingMaterials.FirstOrDefault( + item => item.Id == input.TaskMaterialId.Value) + ?? throw Oops.Bah("需要重新上传的材料不存在"); + replacement.FileId = input.FileId; + replacement.SubmitRound = task.SubmitCount + 1; + replacement.UploadedAt = DateTime.Now; + replacement.ReviewStatus = TaskMaterialReviewStatus.Pending; + replacement.ReviewRemark = null; + await _db.Updateable(replacement).ExecuteCommandAsync(); + return; + } + var supplementMaterial = existingMaterials.FirstOrDefault( item => item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement); if (supplementMaterial != null) @@ -184,6 +198,16 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ var materials = await _db.Queryable() .Where(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId) .ToListAsync(); + if (input.TaskMaterialId.HasValue) + { + var replacementExists = materials.Any( + item => item.Id == input.TaskMaterialId.Value); + if (!replacementExists) + throw Oops.Bah("需要重新上传的材料不存在"); + + return; + } + var canReplaceSupplement = materials.Any( item => item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement); if (materials.Count >= requirement.MaxFileCount && !canReplaceSupplement) @@ -246,4 +270,4 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ return task; } -} \ No newline at end of file +} diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/IOnboardingPublicTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/IOnboardingPublicTaskService.cs index 1ad6d39..0c5dbaa 100644 --- a/SimpleAdmin.Application/Services/Onboarding/PublicTask/IOnboardingPublicTaskService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/IOnboardingPublicTaskService.cs @@ -17,4 +17,10 @@ public interface IOnboardingPublicTaskService : ITransient /// 校验任务归属后返回合同模板源文件 Id。 /// Task GetContractSourceFileId(OnboardingPublicContractSourceInput input); + + /// + /// 校验 Token 和任务归属后返回已上传材料的文件信息。 + /// + Task GetMaterialSource( + OnboardingPublicMaterialSourceInput input); } diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs index aca0113..ed59ffd 100644 --- a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs @@ -38,6 +38,36 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService 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() @@ -69,9 +99,7 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService .FirstAsync(item => item.TaskId == task.Id), Contracts = contracts, DynamicValues = new List(), - Materials = await _db.Queryable() - .Where(item => item.TaskId == task.Id) - .ToListAsync(), + Materials = materialOutputs, FormFields = new List(), MaterialRequirements = materialRequirements.Select(requirement => new OnboardingPublicMaterialOutput { @@ -79,6 +107,7 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService MaterialName = requirement.MaterialName, AllowedExtensions = requirement.AllowedExtensions, MaxFileCount = requirement.MaxFileCount, + MaxFileSizeMb = requirement.MaxFileSizeMb, Required = requirement.Required }).ToList(), ContractVariables = contractVariables @@ -155,6 +184,34 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService 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)) @@ -171,4 +228,4 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService return link; } -} \ No newline at end of file +} diff --git a/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskPublicInput.cs b/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskPublicInput.cs index 8742350..f4475de 100644 --- a/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskPublicInput.cs +++ b/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskPublicInput.cs @@ -30,6 +30,24 @@ public class OnboardingPublicContractSourceInput public long TaskContractId { get; set; } } +/// +/// 员工端预览已上传材料参数。 +/// +public class OnboardingPublicMaterialSourceInput +{ + /// + /// 员工填写链接中的明文 Token。 + /// + [global::System.ComponentModel.DataAnnotations.Required] + public string Token { get; set; } + + /// + /// 任务材料记录 Id。 + /// + [IdNotNull(ErrorMessage = "任务材料Id不能为空")] + public long TaskMaterialId { get; set; } +} + /// /// 员工端保存固定基础信息参数。 /// @@ -162,12 +180,73 @@ public class OnboardingPublicMaterialOutput /// public int MaxFileCount { get; set; } + /// + /// 单个文件最大大小,单位为 MB;为空表示不限制。 + /// + public int? MaxFileSizeMb { get; set; } + /// /// 是否必填、必传或必签。 /// public bool Required { get; set; } } +/// +/// 员工端已上传材料展示信息。 +/// +public class OnboardingPublicTaskMaterialOutput +{ + /// + /// 任务材料记录 Id。 + /// + public long Id { get; set; } + + /// + /// 材料类型 Id。 + /// + public long MaterialTypeId { get; set; } + + /// + /// 材料显示名称。 + /// + public string MaterialName { get; set; } + + /// + /// 系统文件 Id。 + /// + public long FileId { get; set; } + + /// + /// 用户上传时的原始文件名。 + /// + public string FileName { get; set; } + + /// + /// 文件扩展名,不包含点号。 + /// + public string FileSuffix { get; set; } + + /// + /// 格式化后的文件大小。 + /// + public string FileSizeInfo { get; set; } + + /// + /// 材料上传时间。 + /// + public DateTime UploadedAt { get; set; } + + /// + /// 材料审核状态。 + /// + public string ReviewStatus { get; set; } + + /// + /// 材料审核意见。 + /// + public string ReviewRemark { get; set; } +} + /// /// 员工端入职任务完整详情。 /// @@ -206,7 +285,7 @@ public class OnboardingTaskPublicDetailOutput /// /// 候选人已上传材料列表。 /// - public List Materials { get; set; } + public List Materials { get; set; } /// /// 合同中由员工填写的扩展变量。 diff --git a/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs index 20baa88..fb36245 100644 --- a/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs @@ -112,6 +112,7 @@ public class OnboardingTaskService : IOnboardingTaskService { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskId = taskId, + PlainToken = token, TokenHash = Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData( global::System.Text.Encoding.UTF8.GetBytes(token))), ExpiresAt = expiresAt diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskQuery/IOnboardingTaskQueryService.cs b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/IOnboardingTaskQueryService.cs index 2a11668..9d9371f 100644 --- a/SimpleAdmin.Application/Services/Onboarding/TaskQuery/IOnboardingTaskQueryService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/IOnboardingTaskQueryService.cs @@ -14,4 +14,9 @@ public interface IOnboardingTaskQueryService : ITransient /// 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。 /// Task Detail(BaseIdInput input); + + /// + /// 获取或重新生成任务有效填写链接。 + /// + Task GetTaskLink(BaseIdInput input); } diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskQuery/OnboardingTaskQueryService.cs b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/OnboardingTaskQueryService.cs index e6b2dd4..beda0dc 100644 --- a/SimpleAdmin.Application/Services/Onboarding/TaskQuery/OnboardingTaskQueryService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/OnboardingTaskQueryService.cs @@ -1,4 +1,4 @@ -namespace SimpleAdmin.Application; +namespace SimpleAdmin.Application; /// /// 实现入职任务查询业务能力。 @@ -6,23 +6,32 @@ namespace SimpleAdmin.Application; public class OnboardingTaskQueryService : IOnboardingTaskQueryService { private readonly SqlSugarScope _db; + private readonly IConfigService _configService; - public OnboardingTaskQueryService() + public OnboardingTaskQueryService(IConfigService configService) { _db = DbContext.DB; + _configService = configService; } /// /// 分页查询后台入职任务。 /// - public Task> Page(OnboardingTaskPageInput input) + public async Task> Page(OnboardingTaskPageInput input) { - return _db.Queryable() + var list = await _db.Queryable() .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) .OrderByDescending(item => item.CreateTime) .ToPagedListAsync(input.PageNum, input.PageSize); + + if (list.List != null && list.List.Any()) + { + await FillTaskMetadata(list.List); + } + + return list; } /// @@ -33,6 +42,8 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService var task = await _db.Queryable().InSingleAsync(input.Id) ?? throw Oops.Bah("入职任务不存在"); + await FillTaskMetadata(new List { task }); + var employeeInfo = await _db.Queryable() .FirstAsync(item => item.TaskId == task.Id); var contracts = await _db.Queryable() @@ -79,4 +90,143 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService Audits = audits }; } + + /// + /// 获取或重新生成任务有效填写链接。 + /// + public async Task GetTaskLink(BaseIdInput input) + { + var task = await _db.Queryable().InSingleAsync(input.Id) + ?? throw Oops.Bah("入职任务不存在"); + var canShareLink = task.TaskStatus == OnboardingTaskStatus.PendingFill + || task.TaskStatus == OnboardingTaskStatus.Rejected; + if (!canShareLink) + { + throw Oops.Bah("仅待填写或已退回的入职任务可以获取填写链接"); + } + + var activeLink = await _db.Queryable() + .Where(item => item.TaskId == task.Id + && item.LinkStatus == OnboardingLinkStatus.Active + && item.ExpiresAt > DateTime.Now + && !string.IsNullOrWhiteSpace(item.PlainToken)) + .OrderByDescending(item => item.CreateTime) + .FirstAsync(); + + if (activeLink != null) + { + return new OnboardingTaskCreatedOutput + { + TaskId = task.Id, + TaskNo = task.TaskNo, + LinkToken = activeLink.PlainToken, + ExpiresAt = activeLink.ExpiresAt + }; + } + + var expiryHours = await GetLinkExpiryHours(); + var currentTime = DateTime.Now; + var token = Guid.NewGuid().ToString("N"); + var expiresAt = currentTime.AddHours(expiryHours); + + var newLink = new ObTaskLink + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = task.Id, + PlainToken = token, + TokenHash = Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData( + global::System.Text.Encoding.UTF8.GetBytes(token))), + ExpiresAt = expiresAt, + LinkStatus = OnboardingLinkStatus.Active + }; + + await _db.Ado.BeginTranAsync(); + try + { + // 历史链接虽然已超过有效期,但状态可能仍为 active,续期前统一修正为已过期。 + await _db.Updateable() + .SetColumns(item => new ObTaskLink + { + LinkStatus = OnboardingLinkStatus.Expired + }) + .Where(item => item.TaskId == task.Id + && item.LinkStatus == OnboardingLinkStatus.Active + && item.ExpiresAt <= currentTime) + .ExecuteCommandAsync(); + + // 旧数据可能只有 TokenHash、没有可返回的 PlainToken,需要撤销后重新签发。 + await _db.Updateable() + .SetColumns(item => new ObTaskLink + { + LinkStatus = OnboardingLinkStatus.Revoked, + RevokedAt = currentTime + }) + .Where(item => item.TaskId == task.Id + && item.LinkStatus == OnboardingLinkStatus.Active) + .ExecuteCommandAsync(); + + await _db.Insertable(newLink).ExecuteCommandAsync(); + await _db.Updateable() + .SetColumns(item => item.LinkExpireAt == expiresAt) + .Where(item => item.Id == task.Id) + .ExecuteCommandAsync(); + await _db.Ado.CommitTranAsync(); + } + catch + { + await _db.Ado.RollbackTranAsync(); + throw; + } + + return new OnboardingTaskCreatedOutput + { + TaskId = task.Id, + TaskNo = task.TaskNo, + LinkToken = token, + ExpiresAt = expiresAt + }; + } + + private async Task FillTaskMetadata(IEnumerable tasks) + { + var taskList = tasks.ToList(); + var companyIds = taskList.Select(t => t.CompanyId).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().Where(c => companyIds.Contains(c.Id)).ToListAsync(); + var positions = await _db.Queryable().Where(p => positionIds.Contains(p.Id)).ToListAsync(); + var templates = await _db.Queryable().Where(t => templateIds.Contains(t.Id)).ToListAsync(); + + var companyMap = companies.ToDictionary(c => c.Id, c => c.CompanyName); + 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 (positionMap.TryGetValue(task.PositionId, out var pName)) task.PositionName = pName; + if (templateMap.TryGetValue(task.OnboardingTemplateId, out var tName)) task.OnboardingTemplateName = tName; + } + } + + /// + /// 从系统配置读取员工填写链接有效期,创建任务与续期保持同一规则。 + /// + private async Task GetLinkExpiryHours() + { + var expiryConfig = await _configService.GetByConfigKey( + CateGoryConst.CONFIG_BIZ_DEFINE, + "onboard.link.expireHours"); + var expiryHours = 0; + var configInvalid = expiryConfig == null + || !int.TryParse(expiryConfig.ConfigValue, out expiryHours) + || expiryHours <= 0; + if (configInvalid) + { + throw Oops.Bah("请在系统配置中设置 onboard.link.expireHours(正整数小时)"); + } + + return expiryHours; + } } diff --git a/SimpleAdmin.Application/SimpleAdmin.Application.xml b/SimpleAdmin.Application/SimpleAdmin.Application.xml index b9057ee..bb56b7f 100644 --- a/SimpleAdmin.Application/SimpleAdmin.Application.xml +++ b/SimpleAdmin.Application/SimpleAdmin.Application.xml @@ -554,6 +554,21 @@ 业务备注。 + + + 公司名称 (展示用) + + + + + 岗位名称 (展示用) + + + + + 入职模板名称 (展示用) + + 入职模板,统一组织公司、岗位、合同、补充字段和上传材料。 @@ -1151,7 +1166,7 @@ - 新员工专属填写链接,仅保存 Token 哈希及链接生命周期信息。 + 新员工专属填写链接。 @@ -1159,9 +1174,14 @@ 所属入职任务 Id。 + + + 明文 Token,仅供受保护的管理端链接接口内部读取。 + + - 访问 Token 的 SHA-256 哈希,不保存明文 Token。 + 访问 Token 的 SHA-256 哈希。 @@ -2977,6 +2997,11 @@ 待上传文件大小,单位为字节。 + + + 需要重新上传并替换的任务材料记录 Id;为空时按新增数量校验。 + + 单个动态字段填写值。 @@ -3027,6 +3052,11 @@ 系统文件 Id。 + + + 需要重新上传并替换的任务材料记录 Id;为空时新增材料。 + + 删除员工在当前可编辑任务中上传的一条材料。 @@ -3157,6 +3187,11 @@ 校验任务归属后返回合同模板源文件 Id。 + + + 校验 Token 和任务归属后返回已上传材料的文件信息。 + + 员工手机端公开入口,仅凭一次性任务链接访问入职资料。 @@ -3177,6 +3212,11 @@ 根据员工当前已填写信息生成合同预览,并返回已替换变量的 DOCX 文件 Id。 + + + 校验一次性链接与材料归属,避免通过文件 Id 越权读取其他任务文件。 + + 查询任务合同变量参数。 @@ -3407,6 +3447,11 @@ 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。 + + + 获取或重新生成任务有效填写链接。 + + 实现入职任务查询业务能力。 @@ -3422,6 +3467,16 @@ 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。 + + + 获取或重新生成任务有效填写链接。 + + + + + 从系统配置读取员工填写链接有效期,创建任务与续期保持同一规则。 + + 创建新员工入职任务参数。 @@ -3532,6 +3587,21 @@ 任务合同 Id。 + + + 员工端预览已上传材料参数。 + + + + + 员工填写链接中的明文 Token。 + + + + + 任务材料记录 Id。 + + 员工端保存固定基础信息参数。 @@ -3657,11 +3727,71 @@ 允许上传的最大文件数量。 + + + 单个文件最大大小,单位为 MB;为空表示不限制。 + + 是否必填、必传或必签。 + + + 员工端已上传材料展示信息。 + + + + + 任务材料记录 Id。 + + + + + 材料类型 Id。 + + + + + 材料显示名称。 + + + + + 系统文件 Id。 + + + + + 用户上传时的原始文件名。 + + + + + 文件扩展名,不包含点号。 + + + + + 格式化后的文件大小。 + + + + + 材料上传时间。 + + + + + 材料审核状态。 + + + + + 材料审核意见。 + + 员工端入职任务完整详情。 diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicSubmissionController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicSubmissionController.cs index 873f916..998fef0 100644 --- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicSubmissionController.cs +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicSubmissionController.cs @@ -72,6 +72,7 @@ public class OnboardingPublicSubmissionController public async Task UploadMaterial( [FromQuery] string token, [FromQuery] long materialTypeId, + [FromQuery] long? taskMaterialId, [FromForm] IFormFile file) { if (file == null || file.Length == 0) @@ -84,7 +85,8 @@ public class OnboardingPublicSubmissionController Token = token, MaterialTypeId = materialTypeId, FileName = file.FileName, - FileLength = file.Length + FileLength = file.Length, + TaskMaterialId = taskMaterialId }); var fileId = await _fileService.UploadFile(SysDictConst.FILE_ENGINE_ALIYUN_OSS, file); @@ -92,7 +94,8 @@ public class OnboardingPublicSubmissionController { Token = token, MaterialTypeId = materialTypeId, - FileId = fileId + FileId = fileId, + TaskMaterialId = taskMaterialId }); } } diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs index 7692220..e63ba17 100644 --- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs @@ -44,4 +44,35 @@ public class OnboardingPublicTaskController var fileId = await _service.GetContractSourceFileId(input); return await _fileService.Download(new BaseIdInput { Id = fileId }); } + + /// + /// 通过任务 Token 内联输出候选人已上传的图片材料,供手机端预览。 + /// + [HttpGet("material-source")] + public async Task MaterialSource( + [FromQuery] OnboardingPublicMaterialSourceInput input) + { + var material = await _service.GetMaterialSource(input); + var source = await _fileService.Download(new BaseIdInput { Id = material.FileId }) + ?? throw Oops.Bah("上传文件不存在"); + return new FileStreamResult( + source.FileStream, + GetImageContentType(material.FileSuffix)); + } + + /// + /// 根据图片扩展名设置浏览器可内联展示的响应类型。 + /// + private static string GetImageContentType(string suffix) + { + return suffix?.TrimStart('.').ToLowerInvariant() switch + { + "png" => "image/png", + "jpg" or "jpeg" => "image/jpeg", + "gif" => "image/gif", + "bmp" => "image/bmp", + "webp" => "image/webp", + _ => "application/octet-stream" + }; + } } diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskQueryController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskQueryController.cs index b8a90fb..5eae2e6 100644 --- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskQueryController.cs +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskQueryController.cs @@ -32,4 +32,13 @@ public class OnboardingTaskQueryController { return await _service.Detail(input); } + + /// + /// 获取或重新生成入职任务专属填写链接。 + /// + [HttpGet("get-link")] + public async Task GetTaskLink([FromQuery] BaseIdInput input) + { + return await _service.GetTaskLink(input); + } } diff --git a/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml b/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml index 018d789..123c174 100644 --- a/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml +++ b/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml @@ -276,7 +276,7 @@ 提交入职资料进入审核。 - + 校验并上传员工入职材料。 @@ -301,6 +301,16 @@ 下载根据当前任务资料动态生成并已替换变量的合同文件。 + + + 通过任务 Token 内联输出候选人已上传的图片材料,供手机端预览。 + + + + + 根据图片扩展名设置浏览器可内联展示的响应类型。 + + 提供任务合同变量维护相关 HTTP 接口。 @@ -361,6 +371,11 @@ 查询入职任务完整详情。 + + + 获取或重新生成入职任务专属填写链接。 + + 提供入职模板聚合配置接口。