1111
This commit is contained in:
+5
@@ -24,4 +24,9 @@ public class OnboardingMaterialUploadValidationInput
|
||||
/// 待上传文件大小,单位为字节。
|
||||
/// </summary>
|
||||
public long FileLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 需要重新上传并替换的任务材料记录 Id;为空时按新增数量校验。
|
||||
/// </summary>
|
||||
public long? TaskMaterialId { get; set; }
|
||||
}
|
||||
|
||||
+5
@@ -53,6 +53,11 @@ public class OnboardingSaveMaterialInput
|
||||
/// 系统文件 Id。
|
||||
/// </summary>
|
||||
public long FileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 需要重新上传并替换的任务材料记录 Id;为空时新增材料。
|
||||
/// </summary>
|
||||
public long? TaskMaterialId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+25
-1
@@ -39,6 +39,20 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
var existingMaterials = await _db.Queryable<ObTaskMaterial>()
|
||||
.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<ObTaskMaterial>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,10 @@ public interface IOnboardingPublicTaskService : ITransient
|
||||
/// 校验任务归属后返回合同模板源文件 Id。
|
||||
/// </summary>
|
||||
Task<long> GetContractSourceFileId(OnboardingPublicContractSourceInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 校验 Token 和任务归属后返回已上传材料的文件信息。
|
||||
/// </summary>
|
||||
Task<OnboardingPublicTaskMaterialOutput> GetMaterialSource(
|
||||
OnboardingPublicMaterialSourceInput input);
|
||||
}
|
||||
|
||||
+61
-4
@@ -38,6 +38,36 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
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()
|
||||
@@ -69,9 +99,7 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
.FirstAsync(item => item.TaskId == task.Id),
|
||||
Contracts = contracts,
|
||||
DynamicValues = new List<ObTaskFormValue>(),
|
||||
Materials = await _db.Queryable<ObTaskMaterial>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
Materials = materialOutputs,
|
||||
FormFields = new List<OnboardingPublicFormFieldOutput>(),
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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))
|
||||
@@ -171,4 +228,4 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,24 @@ public class OnboardingPublicContractSourceInput
|
||||
public long TaskContractId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工端预览已上传材料参数。
|
||||
/// </summary>
|
||||
public class OnboardingPublicMaterialSourceInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 员工填写链接中的明文 Token。
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.DataAnnotations.Required]
|
||||
public string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务材料记录 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "任务材料Id不能为空")]
|
||||
public long TaskMaterialId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工端保存固定基础信息参数。
|
||||
/// </summary>
|
||||
@@ -162,12 +180,73 @@ public class OnboardingPublicMaterialOutput
|
||||
/// </summary>
|
||||
public int MaxFileCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单个文件最大大小,单位为 MB;为空表示不限制。
|
||||
/// </summary>
|
||||
public int? MaxFileSizeMb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填、必传或必签。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工端已上传材料展示信息。
|
||||
/// </summary>
|
||||
public class OnboardingPublicTaskMaterialOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务材料记录 Id。
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料类型 Id。
|
||||
/// </summary>
|
||||
public long MaterialTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料显示名称。
|
||||
/// </summary>
|
||||
public string MaterialName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 系统文件 Id。
|
||||
/// </summary>
|
||||
public long FileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户上传时的原始文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件扩展名,不包含点号。
|
||||
/// </summary>
|
||||
public string FileSuffix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 格式化后的文件大小。
|
||||
/// </summary>
|
||||
public string FileSizeInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料上传时间。
|
||||
/// </summary>
|
||||
public DateTime UploadedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料审核状态。
|
||||
/// </summary>
|
||||
public string ReviewStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料审核意见。
|
||||
/// </summary>
|
||||
public string ReviewRemark { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工端入职任务完整详情。
|
||||
/// </summary>
|
||||
@@ -206,7 +285,7 @@ public class OnboardingTaskPublicDetailOutput
|
||||
/// <summary>
|
||||
/// 候选人已上传材料列表。
|
||||
/// </summary>
|
||||
public List<ObTaskMaterial> Materials { get; set; }
|
||||
public List<OnboardingPublicTaskMaterialOutput> Materials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同中由员工填写的扩展变量。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,4 +14,9 @@ public interface IOnboardingTaskQueryService : ITransient
|
||||
/// 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。
|
||||
/// </summary>
|
||||
Task<OnboardingTaskDetailOutput> Detail(BaseIdInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取或重新生成任务有效填写链接。
|
||||
/// </summary>
|
||||
Task<OnboardingTaskCreatedOutput> GetTaskLink(BaseIdInput input);
|
||||
}
|
||||
|
||||
+154
-4
@@ -1,4 +1,4 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职任务查询业务能力。
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询后台入职任务。
|
||||
/// </summary>
|
||||
public Task<SqlSugarPagedList<ObOnboardingTask>> Page(OnboardingTaskPageInput input)
|
||||
public async Task<SqlSugarPagedList<ObOnboardingTask>> Page(OnboardingTaskPageInput input)
|
||||
{
|
||||
return _db.Queryable<ObOnboardingTask>()
|
||||
var list = await _db.Queryable<ObOnboardingTask>()
|
||||
.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,6 +42,8 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
|
||||
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>()
|
||||
@@ -79,4 +90,143 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
|
||||
Audits = audits
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或重新生成任务有效填写链接。
|
||||
/// </summary>
|
||||
public async Task<OnboardingTaskCreatedOutput> GetTaskLink(BaseIdInput input)
|
||||
{
|
||||
var task = await _db.Queryable<ObOnboardingTask>().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<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)
|
||||
{
|
||||
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 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 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 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user