namespace SimpleAdmin.Application; /// /// 实现入职任务查询业务能力。 /// public class OnboardingTaskQueryService : IOnboardingTaskQueryService { private readonly SqlSugarScope _db; private readonly IConfigService _configService; public OnboardingTaskQueryService(IConfigService configService) { _db = DbContext.DB; _configService = configService; } /// /// 分页查询后台入职任务。 /// public async Task Page(OnboardingTaskPageInput input) { const string approvedGroupStatus = "approved_or_archived"; var filterApprovedGroup = string.Equals( input.TaskStatus, approvedGroupStatus, StringComparison.OrdinalIgnoreCase); var list = await _db.Queryable() .WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo), item => item.TaskNo.Contains(input.TaskNo)) .WhereIF(!string.IsNullOrWhiteSpace(input.TaskStatus) && !filterApprovedGroup, item => item.TaskStatus == input.TaskStatus) .WhereIF(filterApprovedGroup, item => item.TaskStatus == OnboardingTaskStatus.Approved || item.TaskStatus == OnboardingTaskStatus.Archived) .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); if (list.List != null && list.List.Any()) await FillTaskMetadata(list.List); // 统计继续应用组织、岗位和任务编号条件,但不受当前状态筛选影响。 var statusRows = await _db.Queryable() .WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo), item => item.TaskNo.Contains(input.TaskNo)) .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) .GroupBy(item => item.TaskStatus) .Select(item => new OnboardingTaskStatusCountOutput { TaskStatus = item.TaskStatus, Count = SqlFunc.AggregateCount(item.Id) }) .ToListAsync(); var statusCountMap = statusRows.ToDictionary( item => item.TaskStatus, item => item.Count); return new OnboardingTaskPageOutput { PageNum = list.PageNum, PageSize = list.PageSize, Total = list.Total, Pages = list.Pages, List = list.List, HasPrevPages = list.HasPrevPages, HasNextPages = list.HasNextPages, Statistics = new OnboardingTaskStatisticsOutput { PendingFill = statusCountMap.GetValueOrDefault( OnboardingTaskStatus.PendingFill), Reviewing = statusCountMap.GetValueOrDefault( OnboardingTaskStatus.Reviewing), Rejected = statusCountMap.GetValueOrDefault( OnboardingTaskStatus.Rejected), Approved = statusCountMap.GetValueOrDefault( OnboardingTaskStatus.Approved) + statusCountMap.GetValueOrDefault( OnboardingTaskStatus.Archived) } }; } /// /// 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。 /// public async Task Detail(BaseIdInput input) { 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() .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 = await _db.Queryable() .Where(item => materialFileIds.Contains(item.Id)) .ToListAsync(); var materialFileMap = materialFiles.ToDictionary(item => item.Id); var materialOutputs = materials.Select(material => { var output = material.Adapt(); if (materialFileMap.TryGetValue(material.FileId, out var file)) { output.FileName = file.Name; output.FileSuffix = file.Suffix; } return output; }).ToList(); var link = await _db.Queryable() .Where(item => item.TaskId == task.Id && item.LinkStatus == "active") .OrderByDescending(item => item.CreateTime) .FirstAsync(); var audits = await _db.Queryable() .Where(item => item.TaskId == task.Id) .OrderByDescending(item => item.AuditedAt) .ToListAsync(); return new OnboardingTaskDetailOutput { Task = task, EmployeeInfo = employeeInfo, Contracts = contracts, Materials = materialOutputs, Link = link, 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) { // 修复历史数据或旧页面造成的冗余到期时间不一致。 var activeExpiresAt = activeLink.ExpiresAt; if (task.LinkExpireAt != activeExpiresAt) { await _db.Updateable() .SetColumns(item => item.LinkExpireAt == activeExpiresAt) .Where(item => item.Id == task.Id) .ExecuteCommandAsync(); } 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 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().Where(c => companyIds.Contains(c.Id)).ToListAsync(); var departments = departmentIds.Count == 0 ? new List() : await _db.Queryable().Where(d => departmentIds.Contains(d.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 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; } } /// /// 从系统配置读取员工填写链接有效期,创建任务与续期保持同一规则。 /// 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; } }