320 lines
13 KiB
C#
320 lines
13 KiB
C#
namespace SimpleAdmin.Application;
|
|
|
|
/// <summary>
|
|
/// 实现入职任务查询业务能力。
|
|
/// </summary>
|
|
public class OnboardingTaskQueryService : IOnboardingTaskQueryService
|
|
{
|
|
private readonly SqlSugarScope _db;
|
|
private readonly IConfigService _configService;
|
|
private readonly IOnboardingDataPermissionService _dataPermission;
|
|
|
|
public OnboardingTaskQueryService(
|
|
IConfigService configService,
|
|
IOnboardingDataPermissionService dataPermission)
|
|
{
|
|
_db = DbContext.DB;
|
|
_configService = configService;
|
|
_dataPermission = dataPermission;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 分页查询后台入职任务。
|
|
/// </summary>
|
|
public async Task<OnboardingTaskPageOutput> Page(OnboardingTaskPageInput input)
|
|
{
|
|
const string approvedGroupStatus = "approved_or_archived";
|
|
var filterApprovedGroup = string.Equals(
|
|
input.TaskStatus,
|
|
approvedGroupStatus,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
|
|
var list = await _db.Queryable<ObOnboardingTask>()
|
|
.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<ObOnboardingTask>()
|
|
.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)
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。
|
|
/// </summary>
|
|
public async Task<OnboardingTaskDetailOutput> Detail(BaseIdInput input)
|
|
{
|
|
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(input.Id)
|
|
?? throw Oops.Bah("入职任务不存在");
|
|
|
|
await FillTaskMetadata(new List<ObOnboardingTask> { task });
|
|
|
|
var employeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
|
.FirstAsync(item => item.TaskId == task.Id);
|
|
var contracts = await _db.Queryable<ObTaskContract>()
|
|
.Where(item => item.TaskId == task.Id)
|
|
.ToListAsync();
|
|
var materials = await _db.Queryable<ObTaskMaterial>()
|
|
.Where(item => item.TaskId == task.Id)
|
|
.ToListAsync();
|
|
var materialFileIds = materials
|
|
.Select(item => item.FileId)
|
|
.Distinct()
|
|
.ToList();
|
|
var materialFiles = await _db.Queryable<SysFile>()
|
|
.Where(item => materialFileIds.Contains(item.Id))
|
|
.ToListAsync();
|
|
var materialFileMap = materialFiles.ToDictionary(item => item.Id);
|
|
var materialOutputs = materials.Select(material =>
|
|
{
|
|
var output = material.Adapt<OnboardingTaskMaterialDetailOutput>();
|
|
if (materialFileMap.TryGetValue(material.FileId, out var file))
|
|
{
|
|
output.FileName = file.Name;
|
|
output.FileSuffix = file.Suffix;
|
|
output.FileSizeInfo = file.SizeInfo;
|
|
}
|
|
|
|
return output;
|
|
}).ToList();
|
|
var link = await _db.Queryable<ObTaskLink>()
|
|
.Where(item => item.TaskId == task.Id && item.LinkStatus == "active")
|
|
.OrderByDescending(item => item.CreateTime)
|
|
.FirstAsync();
|
|
var audits = await _db.Queryable<ObTaskAudit>()
|
|
.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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取或重新生成任务有效填写链接。
|
|
/// </summary>
|
|
public async Task<OnboardingTaskCreatedOutput> GetTaskLink(BaseIdInput input)
|
|
{
|
|
return await OnboardingConcurrency.RunTask(_db, input.Id, () => GetTaskLinkCore(input));
|
|
}
|
|
|
|
private async Task<OnboardingTaskCreatedOutput> GetTaskLinkCore(BaseIdInput input)
|
|
{
|
|
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(input.Id)
|
|
?? throw Oops.Bah("入职任务不存在");
|
|
await _dataPermission.EnsureCanManage(
|
|
task.CreateUserId,
|
|
OnboardingPermissionCodes.TaskManageAll,
|
|
"入职任务");
|
|
var canShareLink = task.TaskStatus == OnboardingTaskStatus.PendingFill
|
|
|| task.TaskStatus == OnboardingTaskStatus.Rejected;
|
|
if (!canShareLink)
|
|
{
|
|
throw Oops.Bah("仅待填写或已退回的入职任务可以获取填写链接");
|
|
}
|
|
|
|
var activeLink = await _db.Queryable<ObTaskLink>()
|
|
.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<ObOnboardingTask>()
|
|
.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<ObTaskLink>()
|
|
.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<ObTaskLink>()
|
|
.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<ObOnboardingTask>()
|
|
.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<ObOnboardingTask> 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<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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从系统配置读取员工填写链接有效期,创建任务与续期保持同一规则。
|
|
/// </summary>
|
|
private async Task<int> 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;
|
|
}
|
|
}
|