namespace SimpleAdmin.Application; /// /// 构建任务合同变量快照,合同生成阶段只读取本快照。 /// public class OnboardingContractSnapshotService : IOnboardingContractSnapshotService { private readonly SqlSugarScope _db; public OnboardingContractSnapshotService() { _db = DbContext.DB; } /// /// 根据任务、公司、员工固定信息和动态补充字段构建合同变量快照。 /// public async Task Build( 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().InSingleAsync(taskId) ?? throw Oops.Bah("入职任务不存在"); var company = await _db.Queryable().InSingleAsync(task.CompanyId) ?? throw Oops.Bah("入职公司不存在"); employeeInfo ??= await _db.Queryable() .FirstAsync(item => item.TaskId == taskId); // 员工补充合同变量统一从 CustomVariablesJson 读取,不再访问已移除的旧动态字段表。 var dynamicValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); var contracts = await _db.Queryable() .Where(item => item.TaskId == taskId) .ToListAsync(); foreach (var contract in contracts) { var definitions = await _db.Queryable() .Where(item => item.TemplateVersionId == contract.TemplateVersionId) .OrderBy(item => item.SortCode) .ToListAsync(); var oldRows = await _db.Queryable() .Where(item => item.TaskContractId == contract.Id) .ToListAsync(); var rows = definitions.Select(definition => { var old = oldRows.FirstOrDefault(item => item.VariableCode == definition.VariableCode); var preserveManual = old?.IsManualOverride == true; return new ObTaskContractVariable { Id = old?.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskContractId = contract.Id, VariableCode = definition.VariableCode, VariableName = definition.VariableName, VariableType = definition.VariableType, DataType = definition.DataType, SortCode = definition.SortCode ?? 100, VariableValue = preserveManual ? old.VariableValue : ResolveValue(definition, employeeInfo, dynamicValueMap, company, task), SourceType = preserveManual ? ContractVariableSourceType.HrInput : GetSourceType(definition.VariableType), IsManualOverride = preserveManual }; }).ToList(); // 仅当参与合同渲染的变量编码或变量值发生变化时,才作废已生成文件。 // 重复保存完全相同的员工资料不应导致员工刚查看的合同突然失效。 var snapshotChanged = oldRows.Count != rows.Count || rows.Any(row => { var old = oldRows.FirstOrDefault(item => item.VariableCode == row.VariableCode); return old == null || !string.Equals( old.VariableValue, row.VariableValue, StringComparison.Ordinal); }); await _db.Deleteable() .Where(item => item.TaskContractId == contract.Id) .ExecuteCommandAsync(); if (rows.Count > 0) await _db.Insertable(rows).ExecuteCommandAsync(); if (invalidateGeneratedPreview && snapshotChanged && task.TaskStatus is OnboardingTaskStatus.PendingFill or OnboardingTaskStatus.Rejected) { contract.GeneratedDocxFileId = null; contract.GeneratedPdfFileId = null; contract.GeneratedAt = null; contract.ReadAt = null; contract.ConfirmedAt = null; contract.ContractStatus = TaskContractStatus.PendingGenerate; await _db.Updateable(contract).ExecuteCommandAsync(); } } } private static string GetSourceType(string variableType) { return variableType switch { ContractVariableType.Employee => ContractVariableSourceType.Employee, ContractVariableType.Company => ContractVariableSourceType.Company, ContractVariableType.System => ContractVariableSourceType.System, _ => ContractVariableSourceType.HrInput }; } internal static string ResolveValue( ObTemplateVariable definition, ObTaskEmployeeInfo employeeInfo, IReadOnlyDictionary dynamicValues, ObCompany company, ObOnboardingTask task, bool useEmployeePlaceholder = false) { var sourceField = GetSourceField(definition); if (definition.VariableType == ContractVariableType.Employee) { var fixedValue = GetPropertyValue(employeeInfo, ResolveEmployeeField(sourceField), definition.FormatRule); if (!string.IsNullOrWhiteSpace(fixedValue)) return fixedValue; // 检查员工自定义补充变量 JSON if (!string.IsNullOrWhiteSpace(employeeInfo?.CustomVariablesJson)) { try { using var doc = global::System.Text.Json.JsonDocument.Parse(employeeInfo.CustomVariablesJson); if (doc.RootElement.TryGetProperty(sourceField, out var prop) || doc.RootElement.TryGetProperty(definition.VariableCode, out prop)) { var str = prop.ValueKind switch { global::System.Text.Json.JsonValueKind.String => prop.GetString(), global::System.Text.Json.JsonValueKind.Number => prop.ToString(), global::System.Text.Json.JsonValueKind.True => bool.TrueString, global::System.Text.Json.JsonValueKind.False => bool.FalseString, global::System.Text.Json.JsonValueKind.Null => null, _ => throw Oops.Bah($"员工补充变量“{definition.VariableName}”格式不正确") }; if (!string.IsNullOrWhiteSpace(str)) return str; } } catch (global::System.Text.Json.JsonException) { throw Oops.Bah("员工补充变量数据格式异常,请重新保存入职登记表后再生成合同"); } } if (dynamicValues.TryGetValue(sourceField, out var dynamicValue)) return dynamicValue; return useEmployeePlaceholder ? $"【待员工填写:{definition.VariableName ?? definition.VariableCode}】" : definition.DefaultValue; } if (definition.VariableType == ContractVariableType.Company) return GetPropertyValue(company, ResolveCompanyField(sourceField), definition.FormatRule) ?? definition.DefaultValue; if (definition.VariableType == ContractVariableType.System) { return sourceField.ToUpperInvariant() switch { "TASKNO" or "TASK_NO" or "CONTRACTNO" or "CONTRACT_NO" => task.TaskNo, "CURRENTDATE" or "CURRENT_DATE" or "SIGNDATE" or "SIGN_DATE" => DateTime.Now.ToString("yyyy年MM月dd日"), _ => definition.DefaultValue }; } return definition.DefaultValue; } private static string GetSourceField(ObTemplateVariable definition) { if (!string.IsNullOrWhiteSpace(definition.EmployeeField)) return definition.EmployeeField; var separatorIndex = definition.VariableCode?.IndexOf('.') ?? -1; return separatorIndex >= 0 ? definition.VariableCode[(separatorIndex + 1)..] : definition.VariableCode; } private static string GetPropertyValue(object source, string propertyName, string formatRule = null) { if (source == null || string.IsNullOrWhiteSpace(propertyName)) return null; var value = source.GetType() .GetProperty( propertyName, global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.IgnoreCase) ?.GetValue(source); if (value == null) return null; if (value is DateTime dateTime) return dateTime.ToString(string.IsNullOrWhiteSpace(formatRule) ? "yyyy年MM月dd日" : formatRule); if (value is DateTimeOffset dateTimeOffset) return dateTimeOffset.ToString(string.IsNullOrWhiteSpace(formatRule) ? "yyyy年MM月dd日" : formatRule); if (!string.IsNullOrWhiteSpace(formatRule) && value is IFormattable formattable) return formattable.ToString(formatRule, null); return value.ToString(); } private static string ResolveEmployeeField(string sourceField) { return sourceField.ToUpperInvariant() switch { "FULLNAME" or "FULL_NAME" or "NAME" => nameof(ObTaskEmployeeInfo.EmployeeName), "MOBILE" or "MOBILEPHONE" or "MOBILE_PHONE" => nameof(ObTaskEmployeeInfo.Phone), "IDNUMBER" or "ID_NUMBER" or "IDENTITYNUMBER" => nameof(ObTaskEmployeeInfo.IdCardNumber), _ => sourceField }; } private static string ResolveCompanyField(string sourceField) { return sourceField.ToUpperInvariant() switch { "COMPANYCODE" or "COMPANY_CODE" => nameof(ObCompany.CompanyCode), "COMPANYNAME" or "COMPANY_NAME" => nameof(ObCompany.CompanyName), "LEGALNAME" or "LEGAL_NAME" or "COMPANYLEGALNAME" => nameof(ObCompany.LegalName), "UNIFIEDSOCIALCREDITCODE" or "UNIFIED_SOCIAL_CREDIT_CODE" => nameof(ObCompany.UnifiedSocialCreditCode), "LEGALREPRESENTATIVE" or "LEGAL_REPRESENTATIVE" => nameof(ObCompany.LegalRepresentative), "ADDRESS" or "COMPANYADDRESS" or "COMPANY_ADDRESS" or "REGISTEREDADDRESS" or "REGISTERED_ADDRESS" => nameof(ObCompany.RegisteredAddress), "PHONE" or "COMPANYPHONE" or "COMPANY_PHONE" or "CONTACTPHONE" or "CONTACT_PHONE" => nameof(ObCompany.ContactPhone), _ => sourceField }; } }