修复相关漏洞
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职任务审核与退回业务能力。
|
||||
@@ -28,6 +28,11 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
/// 审核通过入职任务,并在同一事务中创建正式员工档案。
|
||||
/// </summary>
|
||||
public async Task Approve(OnboardingAuditActionInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => ApproveCore(input), protectIdentity: true);
|
||||
}
|
||||
|
||||
private async Task ApproveCore(OnboardingAuditActionInput input)
|
||||
{
|
||||
var task = await GetAuditableTask(input.TaskId);
|
||||
var employeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
@@ -150,6 +155,11 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
/// 退回入职任务,保存审核意见并允许候选人补充资料。
|
||||
/// </summary>
|
||||
public async Task Reject(OnboardingAuditActionInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => RejectCore(input));
|
||||
}
|
||||
|
||||
private async Task RejectCore(OnboardingAuditActionInput input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.AuditOpinion))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
using System.Data.Common;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 所有修改同一任务的入口共用 MySQL 命名锁,适用于同一主库上的多个后端实例。
|
||||
/// 锁使用独立、非池化连接,不在第三方请求期间持有业务数据库事务。
|
||||
/// </summary>
|
||||
internal static class OnboardingConcurrency
|
||||
{
|
||||
public static Task RunTask(SqlSugarScope db, long taskId, Func<Task> action,
|
||||
bool transactional = false, bool protectIdentity = false)
|
||||
=> RunTask(db, taskId, async () => { await action(); return true; },
|
||||
transactional, protectIdentity);
|
||||
|
||||
public static Task<T> RunTask<T>(SqlSugarScope db, long taskId, Func<Task<T>> action,
|
||||
bool transactional = false, bool protectIdentity = false)
|
||||
=> Run(db, $"task:{taskId}", () => protectIdentity
|
||||
? RunPerson(db, taskId, () => InTransaction(db, action, transactional))
|
||||
: InTransaction(db, action, transactional));
|
||||
|
||||
public static Task RunEmployee(SqlSugarScope db, long employeeId, Func<Task> action)
|
||||
=> Run(db, $"employee:{employeeId}", async () => { await action(); return true; });
|
||||
|
||||
public static Task RunPublic(SqlSugarScope db, string token, Func<Task> action,
|
||||
bool transactional = false, bool protectIdentity = false)
|
||||
=> RunPublic(db, token, async () => { await action(); return true; },
|
||||
transactional, protectIdentity);
|
||||
|
||||
public static async Task<T> RunPublic<T>(SqlSugarScope db, string token, Func<Task<T>> action,
|
||||
bool transactional = false, bool protectIdentity = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
throw Oops.Bah("入职链接 Token 不能为空");
|
||||
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
|
||||
var link = await db.Queryable<ObTaskLink>().FirstAsync(item => item.TokenHash == hash)
|
||||
?? throw Oops.Bah("链接不存在");
|
||||
// 此处只定位锁;被保护的方法必须在锁内重新检查链接、任务状态和归属。
|
||||
return await RunTask(db, link.TaskId, action, transactional, protectIdentity);
|
||||
}
|
||||
|
||||
public static Task RunContract(SqlSugarScope db, long contractId, Func<Task> action,
|
||||
bool transactional = false)
|
||||
=> RunContract(db, contractId, async () => { await action(); return true; }, transactional);
|
||||
|
||||
public static async Task<T> RunContract<T>(SqlSugarScope db, long contractId, Func<Task<T>> action,
|
||||
bool transactional = false)
|
||||
{
|
||||
var contract = await db.Queryable<ObTaskContract>().InSingleAsync(contractId)
|
||||
?? throw Oops.Bah("任务合同不存在");
|
||||
return await RunTask(db, contract.TaskId, action, transactional);
|
||||
}
|
||||
|
||||
public static async Task RunMaterial(SqlSugarScope db, long materialId, Func<Task> action)
|
||||
{
|
||||
var material = await db.Queryable<ObTaskMaterial>().InSingleAsync(materialId)
|
||||
?? throw Oops.Bah("任务资料不存在");
|
||||
await RunTask(db, material.TaskId, action, transactional: true);
|
||||
}
|
||||
|
||||
private static async Task<T> RunPerson<T>(SqlSugarScope db, long taskId, Func<Task<T>> action)
|
||||
{
|
||||
var info = await db.Queryable<ObTaskEmployeeInfo>().FirstAsync(item => item.TaskId == taskId)
|
||||
?? throw Oops.Bah("请先完整填写基本信息");
|
||||
// 统一顺序:任务 -> 身份证 -> 手机;跨任务提交/建档的查重与写入也串行。
|
||||
return await Run(db, "identity:" + info.IdCardNumber?.Trim().ToUpperInvariant(),
|
||||
() => Run(db, "phone:" + info.Phone?.Trim(), action));
|
||||
}
|
||||
|
||||
private static async Task<T> InTransaction<T>(SqlSugarScope db, Func<Task<T>> action, bool enabled)
|
||||
{
|
||||
if (!enabled || db.Ado.Transaction != null)
|
||||
return await action();
|
||||
await db.Ado.BeginTranAsync();
|
||||
try
|
||||
{
|
||||
var result = await action();
|
||||
await db.Ado.CommitTranAsync();
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await db.Ado.RollbackTranAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<T> Run<T>(SqlSugarScope db, string resource, Func<Task<T>> action)
|
||||
{
|
||||
if (db.CurrentConnectionConfig.DbType != global::SqlSugar.DbType.MySql)
|
||||
throw Oops.Bah("入职并发保护需要 MySQL 数据库");
|
||||
// 使用已选择的业务主库,不能在只读副本/不同主库上分别申请锁。
|
||||
var database = db.Ado.Connection.Database;
|
||||
var key = Convert.ToHexString(SHA256.HashData(
|
||||
Encoding.UTF8.GetBytes($"onboardx:{database}:{resource}")));
|
||||
return OnboardingOperationGate.Run(key, () => Acquire(db, key), action);
|
||||
}
|
||||
|
||||
private static async Task<IAsyncDisposable> Acquire(SqlSugarScope db, string key)
|
||||
{
|
||||
var connection = (DbConnection)Activator.CreateInstance(db.Ado.Connection.GetType());
|
||||
var builder = new DbConnectionStringBuilder
|
||||
{
|
||||
ConnectionString = db.CurrentConnectionConfig.ConnectionString
|
||||
};
|
||||
// 确保异常关闭时会真正关闭持锁会话,不把命名锁留在连接池里。
|
||||
builder["Pooling"] = false;
|
||||
connection.ConnectionString = builder.ConnectionString;
|
||||
try
|
||||
{
|
||||
await connection.OpenAsync();
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT GET_LOCK(@lockName, 0)";
|
||||
command.CommandTimeout = 5;
|
||||
AddKey(command, key);
|
||||
var result = await command.ExecuteScalarAsync();
|
||||
if (result == null || result == DBNull.Value || Convert.ToInt32(result) != 1)
|
||||
throw Oops.Bah("当前数据正在处理,请稍后刷新重试");
|
||||
return new NamedLock(connection, key);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await connection.DisposeAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddKey(DbCommand command, string key)
|
||||
{
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = "@lockName";
|
||||
parameter.Value = key;
|
||||
command.Parameters.Add(parameter);
|
||||
}
|
||||
|
||||
private sealed class NamedLock : IAsyncDisposable
|
||||
{
|
||||
private readonly DbConnection _connection;
|
||||
private readonly string _key;
|
||||
|
||||
public NamedLock(DbConnection connection, string key)
|
||||
{
|
||||
_connection = connection;
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var command = _connection.CreateCommand();
|
||||
command.CommandText = "SELECT RELEASE_LOCK(@lockName)";
|
||||
command.CommandTimeout = 5;
|
||||
AddKey(command, _key);
|
||||
await command.ExecuteScalarAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 非池化物理连接关闭会释放会话锁;释放错误不覆盖业务原异常。
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 使同一异步调用链可重入,独立请求仍须取得外部互斥锁。
|
||||
/// 不在 Acquire 中设置 AsyncLocal,避免值不能流回调用方的问题。
|
||||
/// </summary>
|
||||
internal static class OnboardingOperationGate
|
||||
{
|
||||
private sealed record Frame(string Key, Frame Parent);
|
||||
private static readonly AsyncLocal<Frame> Held = new();
|
||||
|
||||
internal static async Task<T> Run<T>(
|
||||
string key,
|
||||
Func<Task<IAsyncDisposable>> acquire,
|
||||
Func<Task<T>> action)
|
||||
{
|
||||
for (var frame = Held.Value; frame != null; frame = frame.Parent)
|
||||
if (frame.Key == key)
|
||||
return await action();
|
||||
|
||||
await using var lease = await acquire();
|
||||
var previous = Held.Value;
|
||||
Held.Value = new Frame(key, previous);
|
||||
try
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Held.Value = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.CompanyCode), it => it.CompanyCode.Contains(input.CompanyCode))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.CompanyName), it => it.CompanyName.Contains(input.CompanyName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), input.GetSafeOrderBy("Id", "Status", "CreateTime", "UpdateTime", "CreateUser", "CreateUserId", "CompanyCode", "CompanyName", "LegalName", "ContactPhone"))
|
||||
.OrderBy(it => it.CreateTime)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
}
|
||||
|
||||
+10
@@ -297,6 +297,11 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
||||
/// 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
|
||||
/// </summary>
|
||||
public async Task<long> Generate(OnboardingGenerateContractInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunContract(_db, input.TaskContractId, () => GenerateCore(input));
|
||||
}
|
||||
|
||||
private async Task<long> GenerateCore(OnboardingGenerateContractInput input)
|
||||
{
|
||||
var contract = await _db.Queryable<ObTaskContract>().InSingleAsync(input.TaskContractId)
|
||||
?? throw Oops.Bah("任务合同不存在");
|
||||
@@ -312,6 +317,11 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<long> GenerateForPublicAccess(long taskContractId, long taskId)
|
||||
{
|
||||
return await OnboardingConcurrency.RunTask(_db, taskId, () => GenerateForPublicAccessCore(taskContractId, taskId));
|
||||
}
|
||||
|
||||
private async Task<long> GenerateForPublicAccessCore(long taskContractId, long taskId)
|
||||
{
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
.FirstAsync(item => item.Id == taskContractId && item.TaskId == taskId)
|
||||
|
||||
+8
@@ -19,6 +19,14 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
|
||||
long taskId,
|
||||
ObTaskEmployeeInfo employeeInfo = null,
|
||||
bool invalidateGeneratedPreview = false)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, taskId, () => BuildCore(taskId, employeeInfo, invalidateGeneratedPreview), transactional: true);
|
||||
}
|
||||
|
||||
private async Task BuildCore(
|
||||
long taskId,
|
||||
ObTaskEmployeeInfo employeeInfo = null,
|
||||
bool invalidateGeneratedPreview = false)
|
||||
{
|
||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId)
|
||||
?? throw Oops.Bah("入职任务不存在");
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
}
|
||||
|
||||
var page = await query
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), input.GetSafeOrderBy("Id", "Status", "CreateTime", "UpdateTime", "CreateUser", "CreateUserId", "CompanyId", "DepartmentId", "TemplateCode", "TemplateName", "ContractType"))
|
||||
.OrderByDescending(it => it.CreateTime)
|
||||
.OrderBy(it => it.TemplateName)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职业务部门业务能力。
|
||||
@@ -22,7 +22,7 @@ public class OnboardingDepartmentService : DbRepository<ObDepartment>, IOnboardi
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentCode), it => it.DepartmentCode.Contains(input.DepartmentCode))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.DepartmentName), it => it.DepartmentName.Contains(input.DepartmentName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), input.GetSafeOrderBy("Id", "Status", "CreateTime", "UpdateTime", "CreateUser", "CreateUserId", "CompanyId", "DepartmentCode", "DepartmentName", "SortCode"))
|
||||
.OrderBy(it => it.SortCode)
|
||||
.OrderBy(it => it.DepartmentName)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
|
||||
+109
-17
@@ -48,6 +48,12 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingElectronicSignDetailOutput> Initiate(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunTask(_db, input.TaskId, () => InitiateCore(input));
|
||||
}
|
||||
|
||||
private async Task<OnboardingElectronicSignDetailOutput> InitiateCore(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
ValidateTaskCanSign(task);
|
||||
@@ -99,6 +105,12 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingElectronicSignDetailOutput> Reinitiate(
|
||||
OnboardingElectronicSignReinitiateInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunTask(_db, input.TaskId, () => ReinitiateCore(input));
|
||||
}
|
||||
|
||||
private async Task<OnboardingElectronicSignDetailOutput> ReinitiateCore(
|
||||
OnboardingElectronicSignReinitiateInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
ValidateTaskCanSign(task);
|
||||
@@ -256,6 +268,12 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingElectronicSignDetailOutput> Sync(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunTask(_db, input.TaskId, () => SyncCore(input));
|
||||
}
|
||||
|
||||
private async Task<OnboardingElectronicSignDetailOutput> SyncCore(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
var process = await GetRequiredProcess(task.Id);
|
||||
@@ -266,6 +284,12 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingElectronicSignDetailOutput> GetSignLink(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunTask(_db, input.TaskId, () => GetSignLinkCore(input));
|
||||
}
|
||||
|
||||
private async Task<OnboardingElectronicSignDetailOutput> GetSignLinkCore(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
var process = await GetRequiredProcess(task.Id);
|
||||
@@ -289,6 +313,12 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingElectronicSignPreviewOutput> PreviewEmployeeSignedDocument(
|
||||
OnboardingElectronicSignPreviewInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunTask(_db, input.TaskId, () => PreviewEmployeeSignedDocumentCore(input));
|
||||
}
|
||||
|
||||
private async Task<OnboardingElectronicSignPreviewOutput> PreviewEmployeeSignedDocumentCore(
|
||||
OnboardingElectronicSignPreviewInput input)
|
||||
{
|
||||
var task = await GetTask(input.TaskId);
|
||||
var process = await GetRequiredProcess(task.Id);
|
||||
@@ -346,6 +376,12 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingElectronicSignDetailOutput> CompanySeal(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunTask(_db, input.TaskId, () => CompanySealCore(input));
|
||||
}
|
||||
|
||||
private async Task<OnboardingElectronicSignDetailOutput> CompanySealCore(
|
||||
OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
var process = await GetRequiredProcess(task.Id);
|
||||
@@ -378,7 +414,29 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
$"请先逐份查看员工已签文件:{string.Join("、", notPreviewed)}");
|
||||
}
|
||||
|
||||
// 发出前持久化占用。进程退出/响应丢失也不能自动重发可能已送达的盖章命令。
|
||||
var alreadySubmitted = await _db.Queryable<ObElectronicSignEvent>()
|
||||
.AnyAsync(item => item.SignProcessId == process.Id
|
||||
&& item.EventSource == AdminCompanySealEventSource
|
||||
&& (item.EventType == "company_seal_submitted"
|
||||
|| item.EventType == "company_seal_dispatch_started"));
|
||||
if (alreadySubmitted)
|
||||
throw Oops.Bah("企业盖章命令已发送或结果待确认,请先刷新签署状态并核实契约锁平台,系统不会重复盖章");
|
||||
|
||||
var companyName = await GetCompanyLegalName(task);
|
||||
await RecordAdminOperation(
|
||||
process,
|
||||
AdminCompanySealEventSource,
|
||||
"company_seal_dispatch_started",
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
process.ProviderContractId,
|
||||
companyName,
|
||||
process.SealId,
|
||||
operatorUserId = UserManager.UserId,
|
||||
operatorName = UserManager.Name
|
||||
}),
|
||||
"已占用盖章请求;发送结果待确认,禁止自动重复发送");
|
||||
try
|
||||
{
|
||||
await _qiyuesuoClient.CompanySign(
|
||||
@@ -406,10 +464,10 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
return await BuildDetail(task, process);
|
||||
}
|
||||
|
||||
process.LastError = $"企业盖章失败:{LimitError(signException.Message)}";
|
||||
process.LastError = $"企业盖章结果待核实,已阻止重复发送:{LimitError(signException.Message)}";
|
||||
process.UpdateTime = DateTime.Now;
|
||||
await _db.Updateable(process).ExecuteCommandAsync();
|
||||
throw;
|
||||
throw Oops.Bah("企业盖章未确认成功,请刷新状态并核实契约锁平台;为避免重复盖章,不能直接重发。原因:" + LimitError(signException.Message));
|
||||
}
|
||||
|
||||
await RecordAdminOperation(
|
||||
@@ -441,6 +499,11 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Notice(OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => NoticeCore(input));
|
||||
}
|
||||
|
||||
private async Task NoticeCore(OnboardingElectronicSignTaskInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
var process = await GetRequiredProcess(task.Id);
|
||||
@@ -455,6 +518,11 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Invalid(OnboardingElectronicSignInvalidInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => InvalidCore(input));
|
||||
}
|
||||
|
||||
private async Task InvalidCore(OnboardingElectronicSignInvalidInput input)
|
||||
{
|
||||
var task = await GetAuthorizedTask(input.TaskId);
|
||||
var process = await GetRequiredProcess(task.Id);
|
||||
@@ -478,6 +546,21 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task HandleCallback(string payloadJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(payloadJson))
|
||||
throw Oops.Bah("契约锁回调内容为空");
|
||||
using var json = JsonDocument.Parse(payloadJson);
|
||||
var process = await FindProcess(FindString(json.RootElement, "contractId"), FindString(json.RootElement, "bizId"));
|
||||
if (process == null)
|
||||
{
|
||||
await HandleCallbackCore(payloadJson);
|
||||
return;
|
||||
}
|
||||
// 只在锁外定位任务;锁内重新读取事件和流程,重复回调不会同时执行归档。
|
||||
await OnboardingConcurrency.RunTask(_db, process.TaskId, () => HandleCallbackCore(payloadJson, taskLockHeld: true));
|
||||
}
|
||||
|
||||
private async Task HandleCallbackCore(string payloadJson, bool taskLockHeld = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(payloadJson))
|
||||
throw Oops.Bah("契约锁回调内容为空");
|
||||
@@ -504,6 +587,13 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
"contractStatus",
|
||||
"status");
|
||||
var process = await FindProcess(providerContractId, bizId);
|
||||
// 回调首次定位时流程可能还在创建;若此刻才匹配到,先取得任务锁再重新执行。
|
||||
if (process != null && !taskLockHeld)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, process.TaskId,
|
||||
() => HandleCallbackCore(payloadJson, taskLockHeld: true));
|
||||
return;
|
||||
}
|
||||
if (callbackEvent == null)
|
||||
{
|
||||
callbackEvent = new ObElectronicSignEvent
|
||||
@@ -549,7 +639,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
try
|
||||
{
|
||||
process.LastCallbackAt = DateTime.Now;
|
||||
await _db.Updateable(process).ExecuteCommandAsync();
|
||||
await _db.Updateable(process).UpdateColumns(item => item.LastCallbackAt).ExecuteCommandAsync();
|
||||
// 明文回调只作为刷新触发器,最终状态重新向契约锁查询,伪造回调不能改变业务状态。
|
||||
await SyncProcess(process, "callback");
|
||||
callbackEvent.ProviderStatus = process.ProviderStatus;
|
||||
@@ -939,7 +1029,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
ObElectronicSignProcess process,
|
||||
QiyuesuoContractResult providerContract)
|
||||
{
|
||||
var localStatus = ResolveSignStatus(providerContract);
|
||||
var localStatus = OnboardingSignStateGuard.Resolve(process.SignStatus, ResolveSignStatus(providerContract));
|
||||
process.ProviderStatus = providerContract.Status;
|
||||
process.SignStatus = localStatus;
|
||||
if (localStatus == ElectronicSignStatus.Completed
|
||||
@@ -950,7 +1040,8 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
process.SignUrlExpireAt = null;
|
||||
await _db.Updateable<ObTaskContract>()
|
||||
.SetColumns(item => item.ContractStatus == TaskContractStatus.ElectronicSigned)
|
||||
.Where(item => item.TaskId == process.TaskId)
|
||||
.Where(item => item.TaskId == process.TaskId && process.IsCurrent == true
|
||||
&& item.ContractStatus != TaskContractStatus.Archived)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
else if (IsTerminal(localStatus))
|
||||
@@ -974,24 +1065,25 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
|
||||
foreach (var document in documents)
|
||||
{
|
||||
if (document.SignedPdfFileId.HasValue)
|
||||
continue;
|
||||
var signedPdf = await _qiyuesuoClient.DownloadDocument(
|
||||
document.ProviderDocumentId);
|
||||
var pdfFileId = await _fileService.StoreGeneratedFile(
|
||||
SysDictConst.FILE_ENGINE_ALIYUN_OSS,
|
||||
$"{SafeFileName(process.BizId)}_{document.DocumentSort:D2}_{SafeFileName(document.DocumentName)}_已签署.pdf",
|
||||
signedPdf);
|
||||
document.SignedPdfFileId = pdfFileId;
|
||||
document.UpdateTime = DateTime.Now;
|
||||
await _db.Updateable(document).ExecuteCommandAsync();
|
||||
if (!document.SignedPdfFileId.HasValue)
|
||||
{
|
||||
var signedPdf = await _qiyuesuoClient.DownloadDocument(document.ProviderDocumentId);
|
||||
document.SignedPdfFileId = await _fileService.StoreGeneratedFile(
|
||||
SysDictConst.FILE_ENGINE_ALIYUN_OSS,
|
||||
$"{SafeFileName(process.BizId)}_{document.DocumentSort:D2}_{SafeFileName(document.DocumentName)}_已签署.pdf",
|
||||
signedPdf);
|
||||
document.UpdateTime = DateTime.Now;
|
||||
await _db.Updateable(document).ExecuteCommandAsync();
|
||||
}
|
||||
// 中断重试时仍回填合同指针,不能因文档已落库而跳过未完成的关联更新。
|
||||
var pdfFileId = document.SignedPdfFileId.Value;
|
||||
await _db.Updateable<ObTaskContract>()
|
||||
.SetColumns(item => new ObTaskContract
|
||||
{
|
||||
GeneratedPdfFileId = pdfFileId,
|
||||
ContractStatus = TaskContractStatus.Archived
|
||||
})
|
||||
.Where(item => item.Id == document.TaskContractId)
|
||||
.Where(item => item.Id == document.TaskContractId && process.IsCurrent == true)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>同步第三方状态时的单向迁移保护;未知/陈旧结果不能覆盖已确认的签署进度。</summary>
|
||||
internal static class OnboardingSignStateGuard
|
||||
{
|
||||
internal static string Resolve(string current, string incoming)
|
||||
{
|
||||
if (current == ElectronicSignStatus.Archived && incoming == ElectronicSignStatus.Completed)
|
||||
return ElectronicSignStatus.Archived;
|
||||
if (IsTerminal(current) && current != incoming)
|
||||
throw Oops.Bah("契约锁返回状态与本地终态不一致,已保留本地状态,请核实后重试");
|
||||
if (current == ElectronicSignStatus.PendingCompanySeal
|
||||
&& incoming is not (ElectronicSignStatus.PendingCompanySeal
|
||||
or ElectronicSignStatus.Completed or ElectronicSignStatus.Archived
|
||||
or ElectronicSignStatus.Rejected or ElectronicSignStatus.Invalid or ElectronicSignStatus.Expired))
|
||||
throw Oops.Bah("契约锁状态暂未同步,已保留员工签署结果,请稍后重试");
|
||||
return incoming;
|
||||
}
|
||||
|
||||
private static bool IsTerminal(string status)
|
||||
=> status is ElectronicSignStatus.Completed or ElectronicSignStatus.Archived
|
||||
or ElectronicSignStatus.Rejected or ElectronicSignStatus.Invalid or ElectronicSignStatus.Expired;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace SimpleAdmin.Application;
|
||||
/// </summary>
|
||||
public class OnboardingEmployeeService : IOnboardingEmployeeService
|
||||
{
|
||||
private const string EmployeeExportTemplateFileName = "员工导出模版.xlsx";
|
||||
private readonly SqlSugarScope _db;
|
||||
private readonly IOnboardingDataPermissionService _dataPermission;
|
||||
|
||||
@@ -110,12 +111,17 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
using var package = new ExcelPackage();
|
||||
var templatePath = ResolveEmployeeExportTemplatePath();
|
||||
using var package = new ExcelPackage(new FileInfo(templatePath));
|
||||
package.Workbook.Properties.Title = "员工明细";
|
||||
package.Workbook.Properties.Subject = "员工基本、人事状态及主要合同信息";
|
||||
package.Workbook.Properties.Created = DateTime.Now;
|
||||
|
||||
WriteEmployeeSheet(package.Workbook.Worksheets.Add("员工明细"), employeeRows);
|
||||
var worksheet = package.Workbook.Worksheets.FirstOrDefault(item =>
|
||||
item.Dimension != null
|
||||
&& string.Equals(item.Cells[1, 1].Text.Trim(), "姓名", StringComparison.OrdinalIgnoreCase))
|
||||
?? throw Oops.Bah("员工导出模板未找到以“姓名”开头的表头行,请检查模板内容");
|
||||
WriteEmployeeSheet(worksheet, employeeRows);
|
||||
|
||||
return new OnboardingEmployeeExportFile
|
||||
{
|
||||
@@ -192,6 +198,11 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateHrInfo(OnboardingEmployeeHrUpdateInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunEmployee(_db, input.Id, () => UpdateHrInfoCore(input));
|
||||
}
|
||||
|
||||
private async Task UpdateHrInfoCore(OnboardingEmployeeHrUpdateInput input)
|
||||
{
|
||||
ValidateEmploymentType(input.EmploymentType);
|
||||
ValidateSocialInsuranceStatus(input.SocialInsuranceStatus);
|
||||
@@ -275,6 +286,11 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Regularize(OnboardingEmployeeRegularizeInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunEmployee(_db, input.Id, () => RegularizeCore(input));
|
||||
}
|
||||
|
||||
private async Task RegularizeCore(OnboardingEmployeeRegularizeInput input)
|
||||
{
|
||||
if (!input.RegularizationDate.HasValue)
|
||||
throw Oops.Bah("请选择转正日期");
|
||||
@@ -370,6 +386,11 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RevokeRegularization(OnboardingEmployeeRegularizationRevokeInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunEmployee(_db, input.Id, () => RevokeRegularizationCore(input));
|
||||
}
|
||||
|
||||
private async Task RevokeRegularizationCore(OnboardingEmployeeRegularizationRevokeInput input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.Remark))
|
||||
throw Oops.Bah("请填写撤销原因");
|
||||
@@ -775,7 +796,100 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
|
||||
TextColumn("工资卡开户行", row => row.Employee.BankName, 22)
|
||||
};
|
||||
|
||||
WriteWorksheet(worksheet, columns, rows);
|
||||
WriteEmployeeTemplateWorksheet(worksheet, columns, rows);
|
||||
}
|
||||
|
||||
private static void WriteEmployeeTemplateWorksheet(
|
||||
ExcelWorksheet worksheet,
|
||||
IReadOnlyList<ExportColumn<EmployeeExportContext>> columns,
|
||||
IReadOnlyList<EmployeeExportContext> rows)
|
||||
{
|
||||
var templateColumnCount = worksheet.Dimension?.End.Column ?? 0;
|
||||
if (templateColumnCount == 0)
|
||||
throw Oops.Bah("员工导出模板没有配置表头");
|
||||
|
||||
var columnsByHeader = columns.ToDictionary(
|
||||
item => item.Header,
|
||||
item => item,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var headerAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["户口所在地"] = "户籍所在地"
|
||||
};
|
||||
var templateColumns = new List<(int Index, ExportColumn<EmployeeExportContext> Column)>();
|
||||
var mappedHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var columnIndex = 1; columnIndex <= templateColumnCount; columnIndex++)
|
||||
{
|
||||
var templateHeader = worksheet.Cells[1, columnIndex].Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(templateHeader))
|
||||
throw Oops.Bah($"员工导出模板第 {columnIndex} 列表头为空,请补充后重新导出");
|
||||
|
||||
var exportHeader = headerAliases.GetValueOrDefault(templateHeader, templateHeader);
|
||||
if (!columnsByHeader.TryGetValue(exportHeader, out var exportColumn))
|
||||
throw Oops.Bah($"员工导出模板包含无法识别的表头“{templateHeader}”,请检查模板");
|
||||
if (!mappedHeaders.Add(exportHeader))
|
||||
throw Oops.Bah($"员工导出模板表头“{templateHeader}”重复,请检查模板");
|
||||
|
||||
templateColumns.Add((columnIndex, exportColumn));
|
||||
}
|
||||
|
||||
var missingHeaders = columns
|
||||
.Where(item => !mappedHeaders.Contains(item.Header))
|
||||
.Select(item => item.Header)
|
||||
.ToList();
|
||||
if (missingHeaders.Count > 0)
|
||||
throw Oops.Bah($"员工导出模板缺少表头:{string.Join("、", missingHeaders)}");
|
||||
|
||||
var existingLastRow = worksheet.Dimension?.End.Row ?? 1;
|
||||
for (var rowIndex = 2; rowIndex <= existingLastRow; rowIndex++)
|
||||
{
|
||||
for (var columnIndex = 1; columnIndex <= templateColumnCount; columnIndex++)
|
||||
worksheet.Cells[rowIndex, columnIndex].Value = null;
|
||||
}
|
||||
|
||||
for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++)
|
||||
{
|
||||
var excelRow = rowIndex + 2;
|
||||
foreach (var templateColumn in templateColumns)
|
||||
{
|
||||
var cell = worksheet.Cells[excelRow, templateColumn.Index];
|
||||
var value = templateColumn.Column.Value(rows[rowIndex]);
|
||||
if (value != null)
|
||||
cell.Value = value;
|
||||
|
||||
var templateNumberFormat = worksheet.Cells[1, templateColumn.Index]
|
||||
.Style.Numberformat.Format;
|
||||
cell.Style.Numberformat.Format = string.IsNullOrWhiteSpace(templateNumberFormat)
|
||||
|| string.Equals(
|
||||
templateNumberFormat,
|
||||
"General",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
? templateColumn.Column.NumberFormat
|
||||
: templateNumberFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveEmployeeExportTemplatePath()
|
||||
{
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, "Upload", EmployeeExportTemplateFileName),
|
||||
Path.Combine(currentDirectory, "Upload", EmployeeExportTemplateFileName),
|
||||
Path.Combine(currentDirectory, "SimpleAdmin.Web.Core", "Upload", EmployeeExportTemplateFileName),
|
||||
Path.GetFullPath(Path.Combine(
|
||||
currentDirectory,
|
||||
"..",
|
||||
"SimpleAdmin.Web.Core",
|
||||
"Upload",
|
||||
EmployeeExportTemplateFileName))
|
||||
};
|
||||
|
||||
var templatePath = candidates.FirstOrDefault(File.Exists);
|
||||
return templatePath
|
||||
?? throw Oops.Bah("员工导出模板文件不存在,请检查部署目录中的 Upload/员工导出模版.xlsx");
|
||||
}
|
||||
|
||||
private static void WriteContractSheet(
|
||||
|
||||
+15
@@ -18,6 +18,11 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
|
||||
/// 归档已审核通过的入职任务。
|
||||
/// </summary>
|
||||
public async Task Archive(OnboardingAuditActionInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => ArchiveCore(input));
|
||||
}
|
||||
|
||||
private async Task ArchiveCore(OnboardingAuditActionInput input)
|
||||
{
|
||||
var task = await Authorize(input.TaskId);
|
||||
if (task.TaskStatus != OnboardingTaskStatus.Approved)
|
||||
@@ -57,6 +62,11 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
|
||||
/// 撤销入职任务并同步撤销仍然有效的员工填写链接。
|
||||
/// </summary>
|
||||
public async Task Revoke(OnboardingAuditActionInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, input.TaskId, () => RevokeCore(input));
|
||||
}
|
||||
|
||||
private async Task RevokeCore(OnboardingAuditActionInput input)
|
||||
{
|
||||
var task = await Authorize(input.TaskId);
|
||||
if (task.TaskStatus == OnboardingTaskStatus.Approved
|
||||
@@ -102,6 +112,11 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService
|
||||
/// 删除已撤销的入职任务及其全部关联快照数据。
|
||||
/// </summary>
|
||||
public async Task Delete(BaseIdInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunTask(_db, input.Id, () => DeleteCore(input));
|
||||
}
|
||||
|
||||
private async Task DeleteCore(BaseIdInput input)
|
||||
{
|
||||
var task = await Authorize(input.Id);
|
||||
if (task.TaskStatus != OnboardingTaskStatus.Revoked)
|
||||
|
||||
+5
@@ -22,6 +22,11 @@ public class OnboardingMaterialReviewService : IOnboardingMaterialReviewService
|
||||
/// 审核单份候选人上传材料并记录审核意见。
|
||||
/// </summary>
|
||||
public async Task Review(OnboardingMaterialReviewInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunMaterial(_db, input.TaskMaterialId, () => ReviewCore(input));
|
||||
}
|
||||
|
||||
private async Task ReviewCore(OnboardingMaterialReviewInput input)
|
||||
{
|
||||
if (input.ReviewStatus != TaskMaterialReviewStatus.Passed
|
||||
&& input.ReviewStatus != TaskMaterialReviewStatus.NeedSupplement)
|
||||
|
||||
@@ -23,7 +23,7 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.PositionCode), it => it.PositionCode.Contains(input.PositionCode))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.PositionName), it => it.PositionName.Contains(input.PositionName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status)
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}")
|
||||
.OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), input.GetSafeOrderBy("Id", "Status", "CreateTime", "UpdateTime", "CreateUser", "CreateUserId", "CompanyId", "DepartmentId", "PositionCode", "PositionName"))
|
||||
.OrderByDescending(it => it.CreateTime)
|
||||
.OrderBy(it => it.PositionName)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
|
||||
+30
-1
@@ -25,6 +25,11 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
/// 关联已上传 OSS 文件与任务材料要求。
|
||||
/// </summary>
|
||||
public async Task SaveMaterial(OnboardingSaveMaterialInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunPublic(_db, input.Token, () => SaveMaterialCore(input), transactional: true);
|
||||
}
|
||||
|
||||
private async Task SaveMaterialCore(OnboardingSaveMaterialInput input)
|
||||
{
|
||||
var task = await GetEditableTask(input.Token);
|
||||
var requirement = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||
@@ -84,6 +89,12 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
/// </summary>
|
||||
public async Task DeleteMaterial(
|
||||
OnboardingDeleteMaterialInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunPublic(_db, input.Token, () => DeleteMaterialCore(input), transactional: true);
|
||||
}
|
||||
|
||||
private async Task DeleteMaterialCore(
|
||||
OnboardingDeleteMaterialInput input)
|
||||
{
|
||||
var task = await GetEditableTask(input.Token);
|
||||
var material = await _db.Queryable<ObTaskMaterial>()
|
||||
@@ -102,6 +113,11 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
/// 记录候选人已经查看并确认动态生成的合同。
|
||||
/// </summary>
|
||||
public async Task ConfirmContract(OnboardingConfirmContractInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunPublic(_db, input.Token, () => ConfirmContractCore(input), transactional: true);
|
||||
}
|
||||
|
||||
private async Task ConfirmContractCore(OnboardingConfirmContractInput input)
|
||||
{
|
||||
var task = await GetEditableTask(input.Token);
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
@@ -119,6 +135,11 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
/// 校验固定信息、动态字段、材料、合同变量和合同确认后提交审核。
|
||||
/// </summary>
|
||||
public async Task Submit(OnboardingSubmitInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunPublic(_db, input.Token, () => SubmitCore(input), transactional: true, protectIdentity: true);
|
||||
}
|
||||
|
||||
private async Task SubmitCore(OnboardingSubmitInput input)
|
||||
{
|
||||
var task = await GetEditableTask(input.Token);
|
||||
var info = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
@@ -160,11 +181,19 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
|
||||
var submittedAt = DateTime.Now;
|
||||
var identityConfirmationHash = BuildIdentityConfirmationHash(info);
|
||||
var originalStatus = task.TaskStatus;
|
||||
var originalSubmitCount = task.SubmitCount;
|
||||
task.TaskStatus = OnboardingTaskStatus.Reviewing;
|
||||
task.SubmitCount++;
|
||||
task.LastSubmittedAt = submittedAt;
|
||||
task.RejectReason = null;
|
||||
await _db.Updateable(task).ExecuteCommandAsync();
|
||||
var changed = await _db.Updateable(task)
|
||||
.UpdateColumns(item => new { item.TaskStatus, item.SubmitCount, item.LastSubmittedAt, item.RejectReason })
|
||||
.Where(item => item.Id == task.Id && item.TaskStatus == originalStatus
|
||||
&& item.SubmitCount == originalSubmitCount)
|
||||
.ExecuteCommandAsync();
|
||||
if (changed != 1)
|
||||
throw Oops.Bah("任务状态已变更,请刷新后重试");
|
||||
|
||||
await _db.Insertable(new ObTaskAudit
|
||||
{
|
||||
|
||||
@@ -25,6 +25,11 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
/// 通过有效 Token 获取任务创建时冻结的材料、合同和新版入职登记表信息。
|
||||
/// </summary>
|
||||
public async Task<OnboardingTaskPublicDetailOutput> Detail(OnboardingTaskPublicAccessInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunPublic(_db, input.Token, () => DetailCore(input));
|
||||
}
|
||||
|
||||
private async Task<OnboardingTaskPublicDetailOutput> DetailCore(OnboardingTaskPublicAccessInput input)
|
||||
{
|
||||
var link = await GetActiveLink(input.Token);
|
||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(link.TaskId)
|
||||
@@ -136,6 +141,11 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
/// 保存候选人的固定基础信息并刷新尚未确认的合同变量快照。
|
||||
/// </summary>
|
||||
public async Task SaveEmployeeInfo(OnboardingTaskPublicSaveInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunPublic(_db, input.Token, () => SaveEmployeeInfoCore(input), transactional: true);
|
||||
}
|
||||
|
||||
private async Task SaveEmployeeInfoCore(OnboardingTaskPublicSaveInput input)
|
||||
{
|
||||
var link = await GetActiveLink(input.Token);
|
||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(link.TaskId)
|
||||
@@ -192,6 +202,12 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
/// </summary>
|
||||
public async Task<OnboardingPublicContractPrepareOutput> PrepareContract(
|
||||
OnboardingPublicContractSourceInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunPublic(_db, input.Token, () => PrepareContractCore(input));
|
||||
}
|
||||
|
||||
private async Task<OnboardingPublicContractPrepareOutput> PrepareContractCore(
|
||||
OnboardingPublicContractSourceInput input)
|
||||
{
|
||||
var link = await GetActiveLink(input.Token);
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
@@ -233,6 +249,11 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
||||
/// 返回已经准备完成的合同文件,下载阶段不再执行变量校验和文档生成。
|
||||
/// </summary>
|
||||
public async Task<long> GetContractSourceFileId(OnboardingPublicContractSourceInput input)
|
||||
{
|
||||
return await OnboardingConcurrency.RunPublic(_db, input.Token, () => GetContractSourceFileIdCore(input));
|
||||
}
|
||||
|
||||
private async Task<long> GetContractSourceFileIdCore(OnboardingPublicContractSourceInput input)
|
||||
{
|
||||
var link = await GetActiveLink(input.Token);
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
|
||||
+5
@@ -81,6 +81,11 @@ public class OnboardingTaskContractVariableService : IOnboardingTaskContractVari
|
||||
/// 保存允许人事编辑的任务合同变量值。
|
||||
/// </summary>
|
||||
public async Task Save(OnboardingTaskContractVariableSaveInput input)
|
||||
{
|
||||
await OnboardingConcurrency.RunContract(_db, input.TaskContractId, () => SaveCore(input), transactional: true);
|
||||
}
|
||||
|
||||
private async Task SaveCore(OnboardingTaskContractVariableSaveInput input)
|
||||
{
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
.InSingleAsync(input.TaskContractId)
|
||||
|
||||
@@ -157,6 +157,11 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
|
||||
/// 获取或重新生成任务有效填写链接。
|
||||
/// </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("入职任务不存在");
|
||||
|
||||
Reference in New Issue
Block a user