Files
OnboardXAdmin/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs
T
2026-09-14 14:59:32 +08:00

295 lines
12 KiB
C#

namespace SimpleAdmin.Application;
/// <summary>
/// 员工手机端公开入口,仅凭一次性任务链接访问入职资料。
/// </summary>
public class OnboardingPublicTaskService : IOnboardingPublicTaskService
{
private readonly SqlSugarScope _db;
private readonly IOnboardingContractSnapshotService _snapshotService;
private readonly IOnboardingContractDocumentService _contractDocumentService;
private readonly IOnboardingIdentityValidationService _identityValidationService;
public OnboardingPublicTaskService(
IOnboardingContractSnapshotService snapshotService,
IOnboardingContractDocumentService contractDocumentService,
IOnboardingIdentityValidationService identityValidationService)
{
_db = DbContext.DB;
_snapshotService = snapshotService;
_contractDocumentService = contractDocumentService;
_identityValidationService = identityValidationService;
}
/// <summary>
/// 通过有效 Token 获取任务创建时冻结的材料、合同和新版入职登记表信息。
/// </summary>
public async Task<OnboardingTaskPublicDetailOutput> Detail(OnboardingTaskPublicAccessInput input)
{
var link = await GetActiveLink(input.Token);
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(link.TaskId)
?? throw Oops.Bah("入职任务不存在");
// 员工端必须展示当前 Token 的实际到期时间,不能依赖任务表中的冗余时间。
task.LinkExpireAt = link.ExpiresAt;
link.AccessCount++;
link.LastAccessedAt = DateTime.Now;
await _db.Updateable(link).ExecuteCommandAsync();
var materialRequirements = await _db.Queryable<ObTaskMaterialRequirement>()
.Where(item => item.TaskId == task.Id)
.OrderBy(item => item.SortCode)
.ToListAsync();
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 = materialFileIds.Count == 0
? new List<SysFile>()
: await _db.Queryable<SysFile>()
.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<OnboardingPublicContractVariableOutput>();
if (templateVersionIds.Count > 0)
{
var employeeVariables = await _db.Queryable<ObTemplateVariable>()
.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<ObTaskEmployeeInfo>()
.FirstAsync(item => item.TaskId == task.Id),
Contracts = contracts,
DynamicValues = new List<ObTaskFormValue>(),
Materials = materialOutputs,
FormFields = new List<OnboardingPublicFormFieldOutput>(),
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
};
}
/// <summary>
/// 保存候选人的固定基础信息并刷新尚未确认的合同变量快照。
/// </summary>
public async Task SaveEmployeeInfo(OnboardingTaskPublicSaveInput input)
{
var link = await GetActiveLink(input.Token);
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(link.TaskId)
?? throw Oops.Bah("入职任务不存在");
if (task.TaskStatus != OnboardingTaskStatus.PendingFill
&& task.TaskStatus != OnboardingTaskStatus.Rejected)
{
throw Oops.Bah("当前任务不可编辑");
}
OnboardingEmployeeInfoValidator.ValidateAndNormalize(input);
await _identityValidationService.EnsureNoDuplicate(
task.Id,
input.EmployeeName,
input.IdCardNumber);
var old = await _db.Queryable<ObTaskEmployeeInfo>()
.FirstAsync(item => item.TaskId == task.Id);
var entity = input.Adapt<ObTaskEmployeeInfo>();
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);
}
/// <summary>
/// 根据员工当前已填写信息校验变量并提前生成合同 DOCX。
/// </summary>
public async Task<OnboardingPublicContractPrepareOutput> PrepareContract(
OnboardingPublicContractSourceInput input)
{
var link = await GetActiveLink(input.Token);
var contract = await _db.Queryable<ObTaskContract>()
.FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == link.TaskId)
?? throw Oops.Bah("合同不存在");
// 员工资料保存时会清除旧文件 Id;仍有文件 Id 时还需确认文件记录没有被异常删除。
if (contract.GeneratedDocxFileId.HasValue)
{
var generatedFileExists = await _db.Queryable<SysFile>()
.AnyAsync(item => item.Id == contract.GeneratedDocxFileId.Value);
if (!generatedFileExists)
throw Oops.Bah("已生成的合同文件不存在,请联系管理员重新生成");
return new OnboardingPublicContractPrepareOutput
{
FileId = contract.GeneratedDocxFileId.Value,
GeneratedAt = contract.GeneratedAt
};
}
await _snapshotService.Build(link.TaskId);
var fileId = await _contractDocumentService.GenerateForPublicAccess(
contract.Id,
link.TaskId);
var generatedAt = await _db.Queryable<ObTaskContract>()
.Where(item => item.Id == contract.Id)
.Select(item => item.GeneratedAt)
.SingleAsync();
return new OnboardingPublicContractPrepareOutput
{
FileId = fileId,
GeneratedAt = generatedAt
};
}
/// <summary>
/// 返回已经准备完成的合同文件,下载阶段不再执行变量校验和文档生成。
/// </summary>
public async Task<long> GetContractSourceFileId(OnboardingPublicContractSourceInput input)
{
var link = await GetActiveLink(input.Token);
var contract = await _db.Queryable<ObTaskContract>()
.FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == link.TaskId)
?? throw Oops.Bah("合同不存在");
var fileId = contract.GeneratedDocxFileId;
if (!fileId.HasValue)
{
// 下载入口增加服务端兜底,避免前端状态与数据库短暂不同步时打开 JSON 错误页。
var prepared = await PrepareContract(input);
fileId = prepared.FileId;
}
var fileExists = await _db.Queryable<SysFile>()
.AnyAsync(item => item.Id == fileId.Value);
if (!fileExists)
throw Oops.Bah("合同文件不存在,请重新准备合同");
contract.ReadAt ??= DateTime.Now;
if (contract.ContractStatus == TaskContractStatus.PendingGenerate)
contract.ContractStatus = TaskContractStatus.PendingConfirm;
await _db.Updateable(contract)
.UpdateColumns(item => new { item.ReadAt, item.ContractStatus })
.ExecuteCommandAsync();
return fileId.Value;
}
/// <summary>
/// 校验一次性链接与材料归属,避免通过文件 Id 越权读取其他任务文件。
/// </summary>
public async Task<OnboardingPublicTaskMaterialOutput> GetMaterialSource(
OnboardingPublicMaterialSourceInput input)
{
var link = await GetActiveLink(input.Token);
var material = await _db.Queryable<ObTaskMaterial>()
.FirstAsync(item => item.Id == input.TaskMaterialId && item.TaskId == link.TaskId)
?? throw Oops.Bah("上传材料不存在");
var file = await _db.Queryable<SysFile>().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<ObTaskLink> 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<ObTaskLink>().FirstAsync(item => item.TokenHash == hash)
?? throw Oops.Bah("链接不存在");
if (link.LinkStatus != OnboardingLinkStatus.Active || link.ExpiresAt <= DateTime.Now)
throw Oops.Bah("链接已失效");
return link;
}
}