From f5b40e56590a68ccc52cc7a05370e5f3a0f13f1a Mon Sep 17 00:00:00 2001
From: fuenmao <980740792@qq.com>
Date: Fri, 28 Aug 2026 17:33:30 +0800
Subject: [PATCH] 1111
---
.../Entity/Onboarding/ObEmployee.cs | 12 ++
.../Audit/OnboardingAuditService.cs | 47 +++++-
.../Employee/Dto/OnboardingEmployeeInput.cs | 5 +
.../Employee/OnboardingEmployeeService.cs | 141 +++++++++++++++++-
.../IOnboardingIdentityValidationService.cs | 15 ++
.../OnboardingIdentityValidationService.cs | 50 +++++++
.../OnboardingPublicSubmissionService.cs | 11 +-
.../PublicTask/OnboardingPublicTaskService.cs | 9 +-
...0260828_reset_onboarding_business_data.sql | 130 ++++++++++++++++
...0828_reset_onboarding_business_data_v2.sql | 122 +++++++++++++++
10 files changed, 537 insertions(+), 5 deletions(-)
create mode 100644 SimpleAdmin.Application/Services/Onboarding/IdentityValidation/IOnboardingIdentityValidationService.cs
create mode 100644 SimpleAdmin.Application/Services/Onboarding/IdentityValidation/OnboardingIdentityValidationService.cs
create mode 100644 database/scripts/20260828_reset_onboarding_business_data.sql
create mode 100644 database/scripts/20260828_reset_onboarding_business_data_v2.sql
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObEmployee.cs b/SimpleAdmin.Application/Entity/Onboarding/ObEmployee.cs
index b7c347c..d12d3c0 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/ObEmployee.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObEmployee.cs
@@ -37,6 +37,12 @@ public class ObEmployee : BaseEntity
///
public long CompanyId { get; set; }
+ ///
+ /// 所属公司名称,仅用于接口展示,不映射数据库字段。
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public string CompanyName { get; set; }
+
///
/// 所属入职业务部门 Id。
///
@@ -53,6 +59,12 @@ public class ObEmployee : BaseEntity
///
public long PositionId { get; set; }
+ ///
+ /// 所属岗位名称,仅用于接口展示,不映射数据库字段。
+ ///
+ [SugarColumn(IsIgnore = true)]
+ public string PositionName { get; set; }
+
///
/// 性别。
///
diff --git a/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs b/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
index 3c12cbc..660f42c 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
@@ -44,6 +44,8 @@ public class OnboardingAuditService : IOnboardingAuditService
throw Oops.Bah("该身份证号或手机号已存在正式员工");
}
+ var contractStartDate = await ResolveContractStartDate(task.Id);
+
var employee = employeeInfo.Adapt();
employee.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
employee.CompanyId = task.CompanyId;
@@ -61,7 +63,8 @@ public class OnboardingAuditService : IOnboardingAuditService
employee.College = employeeInfo.GraduatedSchool;
employee.BirthDate = ParseDate(employeeInfo.Birthday);
employee.ContactAddress = employeeInfo.CurrentAddress;
- employee.EntryDate = ParseDate(employeeInfo.OnboardingDate);
+ employee.EntryDate = contractStartDate;
+ employee.ContractStartDate = contractStartDate;
employee.Remark = employeeInfo.OtherRemarks;
employee.ExtraInfoJson = global::System.Text.Json.JsonSerializer.Serialize(employeeInfo);
employee.Status = CommonStatusConst.ENABLE;
@@ -170,6 +173,48 @@ public class OnboardingAuditService : IOnboardingAuditService
return task;
}
+
+ ///
+ /// 从任务签署材料参数中读取唯一的合同开始日期,作为正式员工入职日期。
+ ///
+ private async Task ResolveContractStartDate(long taskId)
+ {
+ var taskContractIds = await _db.Queryable()
+ .Where(item => item.TaskId == taskId)
+ .Select(item => item.Id)
+ .ToListAsync();
+ if (taskContractIds.Count == 0)
+ throw Oops.Bah("入职任务没有签署材料,无法确定入职日期");
+
+ var startDateValues = await _db.Queryable()
+ .Where(item => taskContractIds.Contains(item.TaskContractId)
+ && item.VariableCode == "hr.ContractStartDate")
+ .Select(item => item.VariableValue)
+ .ToListAsync();
+ var normalizedValues = startDateValues
+ .Where(item => !string.IsNullOrWhiteSpace(item))
+ .Select(item => item.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ if (normalizedValues.Count == 0)
+ throw Oops.Bah("签署材料参数“合同开始日期”尚未填写,无法生成正式员工档案");
+
+ var parsedDates = new List();
+ foreach (var value in normalizedValues)
+ {
+ if (!DateTime.TryParse(value, out var parsedDate))
+ throw Oops.Bah("签署材料参数“合同开始日期”格式不正确");
+
+ parsedDates.Add(parsedDate.Date);
+ }
+
+ var distinctDates = parsedDates.Distinct().ToList();
+ if (distinctDates.Count > 1)
+ throw Oops.Bah("多个签署材料的合同开始日期不一致,请统一后再审核通过");
+
+ return distinctDates[0];
+ }
+
///
/// 将登记表中的日期文本转换为正式员工档案日期。
///
diff --git a/SimpleAdmin.Application/Services/Onboarding/Employee/Dto/OnboardingEmployeeInput.cs b/SimpleAdmin.Application/Services/Onboarding/Employee/Dto/OnboardingEmployeeInput.cs
index 00c39ff..c0f97d6 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Employee/Dto/OnboardingEmployeeInput.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Employee/Dto/OnboardingEmployeeInput.cs
@@ -41,6 +41,11 @@ public class OnboardingEmployeeDetailOutput
///
public ObEmployee Employee { get; set; }
+ ///
+ /// 审核通过时对应的完整入职登记表。
+ ///
+ public ObTaskEmployeeInfo EmployeeInfo { get; set; }
+
///
/// 入职任务主数据。
///
diff --git a/SimpleAdmin.Application/Services/Onboarding/Employee/OnboardingEmployeeService.cs b/SimpleAdmin.Application/Services/Onboarding/Employee/OnboardingEmployeeService.cs
index 08e89ce..dcd0d39 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Employee/OnboardingEmployeeService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Employee/OnboardingEmployeeService.cs
@@ -15,9 +15,9 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
///
/// 分页查询审核通过后生成的正式员工档案。
///
- public Task> Page(OnboardingEmployeePageInput input)
+ public async Task> Page(OnboardingEmployeePageInput input)
{
- return _db.Queryable()
+ var result = await _db.Queryable()
.WhereIF(
!string.IsNullOrWhiteSpace(input.EmployeeName),
item => item.EmployeeName.Contains(input.EmployeeName)
@@ -28,6 +28,11 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
.WhereIF(input.PositionId.HasValue, item => item.PositionId == input.PositionId.Value)
.OrderByDescending(item => item.CreateTime)
.ToPagedListAsync(input.PageNum, input.PageSize);
+
+ if (result.List != null && result.List.Any())
+ await FillEmployeeMetadata(result.List);
+
+ return result;
}
///
@@ -38,6 +43,25 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
var employee = await _db.Queryable().InSingleAsync(input.Id)
?? throw Oops.Bah("正式员工不存在");
var task = await _db.Queryable().FirstAsync(x => x.EmployeeId == employee.Id);
+ await FillEmployeeMetadata(new[] { employee });
+
+ var employeeInfo = task == null
+ ? null
+ : await _db.Queryable()
+ .FirstAsync(item => item.TaskId == task.Id);
+
+ if (employeeInfo == null && !string.IsNullOrWhiteSpace(employee.ExtraInfoJson))
+ {
+ try
+ {
+ employeeInfo = global::System.Text.Json.JsonSerializer.Deserialize(
+ employee.ExtraInfoJson);
+ }
+ catch (global::System.Text.Json.JsonException)
+ {
+ // 兼容历史异常快照:员工主档仍可正常查看,其余信息显示为空。
+ }
+ }
var contracts = task == null
? new List()
: await _db.Queryable()
@@ -54,8 +78,121 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
{
Employee = employee,
Task = task,
+ EmployeeInfo = employeeInfo,
Contracts = contracts,
Audits = audits
};
}
+
+ ///
+ /// 批量补充员工所属公司、部门和岗位名称,避免列表逐行查询数据库。
+ ///
+ private async Task FillEmployeeMetadata(IEnumerable employees)
+ {
+ var employeeList = employees.ToList();
+ if (employeeList.Count == 0)
+ return;
+
+ var companyIds = employeeList.Select(item => item.CompanyId).Distinct().ToList();
+ var departmentIds = employeeList
+ .Where(item => item.DepartmentId.HasValue)
+ .Select(item => item.DepartmentId.Value)
+ .Distinct()
+ .ToList();
+ var positionIds = employeeList.Select(item => item.PositionId).Distinct().ToList();
+
+ var companies = await _db.Queryable()
+ .Where(item => companyIds.Contains(item.Id))
+ .ToListAsync();
+ var departments = departmentIds.Count == 0
+ ? new List()
+ : await _db.Queryable()
+ .Where(item => departmentIds.Contains(item.Id))
+ .ToListAsync();
+ var positions = await _db.Queryable()
+ .Where(item => positionIds.Contains(item.Id))
+ .ToListAsync();
+
+ var companyMap = companies.ToDictionary(item => item.Id, item => item.CompanyName);
+ var departmentMap = departments.ToDictionary(item => item.Id, item => item.DepartmentName);
+ var positionMap = positions.ToDictionary(item => item.Id, item => item.PositionName);
+
+ foreach (var employee in employeeList)
+ {
+ if (companyMap.TryGetValue(employee.CompanyId, out var companyName))
+ employee.CompanyName = companyName;
+ if (employee.DepartmentId.HasValue
+ && departmentMap.TryGetValue(employee.DepartmentId.Value, out var departmentName))
+ {
+ employee.DepartmentName = departmentName;
+ }
+ if (positionMap.TryGetValue(employee.PositionId, out var positionName))
+ employee.PositionName = positionName;
+ }
+
+ await FillMissingEntryDates(employeeList);
+ }
+
+ ///
+ /// 兼容已建档但入职日期为空的历史员工,从来源任务的合同开始日期进行只读回填。
+ /// 新审核通过的员工会直接持久化该日期,不会进入此兼容分支。
+ ///
+ private async Task FillMissingEntryDates(List employees)
+ {
+ var missingEmployees = employees
+ .Where(item => !item.EntryDate.HasValue || !item.ContractStartDate.HasValue)
+ .ToList();
+ if (missingEmployees.Count == 0)
+ return;
+
+ var employeeIds = missingEmployees.Select(item => item.Id).ToList();
+ var tasks = await _db.Queryable()
+ .Where(item => item.EmployeeId.HasValue
+ && employeeIds.Contains(item.EmployeeId.Value))
+ .ToListAsync();
+ if (tasks.Count == 0)
+ return;
+
+ var taskIds = tasks.Select(item => item.Id).ToList();
+ var contracts = await _db.Queryable()
+ .Where(item => taskIds.Contains(item.TaskId))
+ .ToListAsync();
+ if (contracts.Count == 0)
+ return;
+
+ var contractIds = contracts.Select(item => item.Id).ToList();
+ var startDateVariables = await _db.Queryable()
+ .Where(item => contractIds.Contains(item.TaskContractId)
+ && item.VariableCode == "hr.ContractStartDate")
+ .ToListAsync();
+
+ var employeeIdByTaskId = tasks
+ .Where(item => item.EmployeeId.HasValue)
+ .ToDictionary(item => item.Id, item => item.EmployeeId.Value);
+ var taskIdByContractId = contracts.ToDictionary(item => item.Id, item => item.TaskId);
+ var datesByEmployeeId = startDateVariables
+ .Where(item => !string.IsNullOrWhiteSpace(item.VariableValue)
+ && taskIdByContractId.ContainsKey(item.TaskContractId))
+ .Select(item => new
+ {
+ EmployeeId = employeeIdByTaskId[taskIdByContractId[item.TaskContractId]],
+ EntryDate = DateTime.TryParse(item.VariableValue, out var value)
+ ? value.Date
+ : (DateTime?)null
+ })
+ .Where(item => item.EntryDate.HasValue)
+ .GroupBy(item => item.EmployeeId)
+ .ToDictionary(
+ group => group.Key,
+ group => group.Select(item => item.EntryDate.Value).Distinct().ToList());
+
+ foreach (var employee in missingEmployees)
+ {
+ if (!datesByEmployeeId.TryGetValue(employee.Id, out var dates) || dates.Count != 1)
+ continue;
+
+ employee.EntryDate ??= dates[0];
+ employee.ContractStartDate ??= dates[0];
+ }
+ }
}
diff --git a/SimpleAdmin.Application/Services/Onboarding/IdentityValidation/IOnboardingIdentityValidationService.cs b/SimpleAdmin.Application/Services/Onboarding/IdentityValidation/IOnboardingIdentityValidationService.cs
new file mode 100644
index 0000000..ef85b4c
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/IdentityValidation/IOnboardingIdentityValidationService.cs
@@ -0,0 +1,15 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 定义候选人身份重复校验能力。
+///
+public interface IOnboardingIdentityValidationService : ITransient
+{
+ ///
+ /// 校验姓名和身份证号没有出现在其他有效入职流程或正式员工档案中。
+ ///
+ Task EnsureNoDuplicate(
+ long currentTaskId,
+ string employeeName,
+ string idCardNumber);
+}
diff --git a/SimpleAdmin.Application/Services/Onboarding/IdentityValidation/OnboardingIdentityValidationService.cs b/SimpleAdmin.Application/Services/Onboarding/IdentityValidation/OnboardingIdentityValidationService.cs
new file mode 100644
index 0000000..40545de
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/IdentityValidation/OnboardingIdentityValidationService.cs
@@ -0,0 +1,50 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 统一校验候选人身份,避免同一人员通过多个链接重复提交入职资料。
+///
+public class OnboardingIdentityValidationService : IOnboardingIdentityValidationService
+{
+ private readonly SqlSugarScope _db;
+
+ public OnboardingIdentityValidationService()
+ {
+ _db = DbContext.DB;
+ }
+
+ ///
+ public async Task EnsureNoDuplicate(
+ long currentTaskId,
+ string employeeName,
+ string idCardNumber)
+ {
+ employeeName = employeeName?.Trim();
+ idCardNumber = idCardNumber?.Trim().ToUpperInvariant();
+ if (string.IsNullOrWhiteSpace(employeeName) || string.IsNullOrWhiteSpace(idCardNumber))
+ return;
+
+ var formalEmployeeExists = await _db.Queryable()
+ .AnyAsync(item => item.EmployeeName == employeeName
+ && item.IdCardNumber == idCardNumber);
+ if (formalEmployeeExists)
+ {
+ throw Oops.Bah("该姓名和身份证号已完成正式员工建档,请勿重复提交入职资料");
+ }
+
+ // 待填写任务不占用身份,以实际先提交的任务为准;撤销或过期任务允许重新发起。
+ var duplicateTaskExists = await _db.Queryable()
+ .InnerJoin((info, task) => info.TaskId == task.Id)
+ .Where((info, task) => info.TaskId != currentTaskId
+ && info.EmployeeName == employeeName
+ && info.IdCardNumber == idCardNumber
+ && (task.TaskStatus == OnboardingTaskStatus.Reviewing
+ || task.TaskStatus == OnboardingTaskStatus.Rejected
+ || task.TaskStatus == OnboardingTaskStatus.Approved
+ || task.TaskStatus == OnboardingTaskStatus.Archived))
+ .AnyAsync();
+ if (!duplicateTaskExists)
+ return;
+
+ throw Oops.Bah("该姓名和身份证号已存在正在办理的入职任务,请勿重复提交;如需补充材料,请联系 HR 继续使用原任务链接");
+ }
+}
diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs
index bc45890..82967e3 100644
--- a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs
@@ -7,11 +7,15 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
{
private readonly SqlSugarScope _db;
private readonly IOnboardingContractSnapshotService _snapshotService;
+ private readonly IOnboardingIdentityValidationService _identityValidationService;
- public OnboardingPublicSubmissionService(IOnboardingContractSnapshotService snapshotService)
+ public OnboardingPublicSubmissionService(
+ IOnboardingContractSnapshotService snapshotService,
+ IOnboardingIdentityValidationService identityValidationService)
{
_db = DbContext.DB;
_snapshotService = snapshotService;
+ _identityValidationService = identityValidationService;
}
///
@@ -125,6 +129,11 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
if (basicInfoIncomplete)
throw Oops.Bah("请先完整填写基本信息");
+ await _identityValidationService.EnsureNoDuplicate(
+ task.Id,
+ info.EmployeeName,
+ info.IdCardNumber);
+
await _snapshotService.Build(task.Id, info);
var requiredMaterials = (await GetMaterialRequirements(task.Id))
diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs
index a30b223..6339bca 100644
--- a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs
@@ -8,14 +8,17 @@ 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)
+ IOnboardingContractDocumentService contractDocumentService,
+ IOnboardingIdentityValidationService identityValidationService)
{
_db = DbContext.DB;
_snapshotService = snapshotService;
_contractDocumentService = contractDocumentService;
+ _identityValidationService = identityValidationService;
}
///
@@ -129,6 +132,10 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
}
ValidateEmployeeInfo(input);
+ await _identityValidationService.EnsureNoDuplicate(
+ task.Id,
+ input.EmployeeName,
+ input.IdCardNumber);
var old = await _db.Queryable()
.FirstAsync(item => item.TaskId == task.Id);
diff --git a/database/scripts/20260828_reset_onboarding_business_data.sql b/database/scripts/20260828_reset_onboarding_business_data.sql
new file mode 100644
index 0000000..f63715a
--- /dev/null
+++ b/database/scripts/20260828_reset_onboarding_business_data.sql
@@ -0,0 +1,130 @@
+-- OnboardX 入职业务数据重置脚本
+--
+-- 保留:
+-- 1. ob_variable_definition 合同变量字典
+-- 2. ob_material_type 上传资料类型
+--
+-- 清除:公司、部门、岗位、合同模板、入职模板、任务、员工档案及全部流程明细。
+-- 不处理:sys_file 表记录和阿里云 OSS 对象,避免误删系统其他模块共用文件。
+--
+-- 重要:请确认已经连接 OnboardX 数据库后再执行。
+
+USE `OnboardX`;
+
+DROP PROCEDURE IF EXISTS `ob_reset_business_data`;
+
+DELIMITER $$
+
+CREATE PROCEDURE `ob_reset_business_data`()
+BEGIN
+ DECLARE old_foreign_key_checks INT DEFAULT 1;
+ DECLARE variable_definition_count_before BIGINT DEFAULT 0;
+ DECLARE variable_definition_count_after BIGINT DEFAULT 0;
+ DECLARE material_type_count_before BIGINT DEFAULT 0;
+ DECLARE material_type_count_after BIGINT DEFAULT 0;
+
+ DECLARE EXIT HANDLER FOR SQLEXCEPTION
+ BEGIN
+ ROLLBACK;
+ SET FOREIGN_KEY_CHECKS = old_foreign_key_checks;
+ RESIGNAL;
+ END;
+
+ IF LOWER(DATABASE()) <> 'onboardx' THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = '当前数据库不是 OnboardX,已拒绝执行入职业务数据清理';
+ END IF;
+
+ SELECT COUNT(*)
+ INTO variable_definition_count_before
+ FROM `ob_variable_definition`;
+
+ SELECT COUNT(*)
+ INTO material_type_count_before
+ FROM `ob_material_type`;
+
+ SET old_foreign_key_checks = @@FOREIGN_KEY_CHECKS;
+ SET FOREIGN_KEY_CHECKS = 0;
+
+ START TRANSACTION;
+
+ -- 一、删除任务运行数据及快照。
+ DELETE FROM `ob_task_operation_log`;
+ DELETE FROM `ob_task_audit`;
+ DELETE FROM `ob_task_material`;
+ DELETE FROM `ob_task_contract_variable`;
+ DELETE FROM `ob_task_contract`;
+ DELETE FROM `ob_task_form_value`;
+ DELETE FROM `ob_task_material_requirement`;
+ DELETE FROM `ob_task_form_requirement`;
+ DELETE FROM `ob_task_employee_info`;
+ DELETE FROM `ob_task_link`;
+ DELETE FROM `ob_onboarding_task`;
+ DELETE FROM `ob_employee`;
+
+ -- 二、删除入职模板及其关联配置。
+ DELETE FROM `ob_onboarding_template_material`;
+ DELETE FROM `ob_onboarding_template_form_field`;
+ DELETE FROM `ob_onboarding_template_contract`;
+ DELETE FROM `ob_onboarding_template_position`;
+ DELETE FROM `ob_onboarding_template`;
+
+ -- 三、删除合同模板实例数据。
+ -- ob_template_variable 是具体 DOCX 版本识别出的变量,不是全局合同变量字典。
+ DELETE FROM `ob_contract_template_position`;
+ DELETE FROM `ob_template_variable`;
+ DELETE FROM `ob_contract_template_version`;
+ DELETE FROM `ob_contract_template`;
+
+ -- 四、删除旧版公司岗位配置和动态字段实例。
+ DELETE FROM `ob_contract_scope`;
+ DELETE FROM `ob_form_requirement`;
+ DELETE FROM `ob_material_requirement`;
+ DELETE FROM `ob_form_field`;
+
+ -- 五、最后删除组织基础数据。
+ DELETE FROM `ob_position`;
+ DELETE FROM `ob_department`;
+ DELETE FROM `ob_company`;
+
+ -- 防止后续维护脚本时误把两张保留表加入删除范围。
+ SELECT COUNT(*)
+ INTO variable_definition_count_after
+ FROM `ob_variable_definition`;
+
+ SELECT COUNT(*)
+ INTO material_type_count_after
+ FROM `ob_material_type`;
+
+ IF variable_definition_count_before <> variable_definition_count_after THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = '合同变量字典数量发生变化,事务已回滚';
+ END IF;
+
+ IF material_type_count_before <> material_type_count_after THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = '上传资料类型数量发生变化,事务已回滚';
+ END IF;
+
+ COMMIT;
+ SET FOREIGN_KEY_CHECKS = old_foreign_key_checks;
+END$$
+
+DELIMITER ;
+
+CALL `ob_reset_business_data`();
+DROP PROCEDURE IF EXISTS `ob_reset_business_data`;
+
+-- 执行结果校验:以下两张保留表应仍有数据。
+SELECT 'ob_variable_definition' AS `PreservedTable`, COUNT(*) AS `RemainingRows`
+FROM `ob_variable_definition`
+UNION ALL
+SELECT 'ob_material_type', COUNT(*)
+FROM `ob_material_type`;
+
+-- 其余 ob_ 业务表的 TABLE_ROWS 应为 0(InnoDB 下该值为估算值,仅供快速查看)。
+SELECT `TABLE_NAME`, `TABLE_ROWS`
+FROM `information_schema`.`TABLES`
+WHERE `TABLE_SCHEMA` = DATABASE()
+ AND `TABLE_NAME` LIKE 'ob\_%'
+ORDER BY `TABLE_NAME`;
diff --git a/database/scripts/20260828_reset_onboarding_business_data_v2.sql b/database/scripts/20260828_reset_onboarding_business_data_v2.sql
new file mode 100644
index 0000000..c120e97
--- /dev/null
+++ b/database/scripts/20260828_reset_onboarding_business_data_v2.sql
@@ -0,0 +1,122 @@
+-- OnboardX 入职业务数据重置脚本 V2
+-- 自动读取当前数据库实际存在的 ob_ 表,兼容不同版本数据库缺少旧表的情况。
+-- 仅保留:ob_variable_definition(合同变量字典)、ob_material_type(上传资料类型)。
+-- 不处理 sys_file 表记录及阿里云 OSS 实际对象。
+
+USE `OnboardX`;
+
+DROP PROCEDURE IF EXISTS `ob_reset_business_data`;
+
+DELIMITER $$
+
+CREATE PROCEDURE `ob_reset_business_data`()
+BEGIN
+ DECLARE old_foreign_key_checks INT DEFAULT 1;
+ DECLARE variable_definition_count_before BIGINT DEFAULT 0;
+ DECLARE variable_definition_count_after BIGINT DEFAULT 0;
+ DECLARE material_type_count_before BIGINT DEFAULT 0;
+ DECLARE material_type_count_after BIGINT DEFAULT 0;
+ DECLARE finished INT DEFAULT 0;
+ DECLARE target_table_name VARCHAR(64);
+
+ DECLARE business_table_cursor CURSOR FOR
+ SELECT `TABLE_NAME`
+ FROM `information_schema`.`TABLES`
+ WHERE `TABLE_SCHEMA` = DATABASE()
+ AND `TABLE_TYPE` = 'BASE TABLE'
+ AND LEFT(`TABLE_NAME`, 3) = 'ob_'
+ AND `TABLE_NAME` NOT IN (
+ 'ob_variable_definition',
+ 'ob_material_type'
+ )
+ ORDER BY `TABLE_NAME`;
+
+ DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
+
+ DECLARE EXIT HANDLER FOR SQLEXCEPTION
+ BEGIN
+ ROLLBACK;
+ SET FOREIGN_KEY_CHECKS = old_foreign_key_checks;
+ RESIGNAL;
+ END;
+
+ IF LOWER(DATABASE()) <> 'onboardx' THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = '当前数据库不是 OnboardX,已拒绝执行入职业务数据清理';
+ END IF;
+
+ SELECT COUNT(*)
+ INTO variable_definition_count_before
+ FROM `ob_variable_definition`;
+
+ SELECT COUNT(*)
+ INTO material_type_count_before
+ FROM `ob_material_type`;
+
+ SET old_foreign_key_checks = @@FOREIGN_KEY_CHECKS;
+ SET FOREIGN_KEY_CHECKS = 0;
+
+ START TRANSACTION;
+
+ OPEN business_table_cursor;
+
+ delete_loop: LOOP
+ FETCH business_table_cursor INTO target_table_name;
+
+ IF finished = 1 THEN
+ LEAVE delete_loop;
+ END IF;
+
+ SET @delete_business_table_sql = CONCAT(
+ 'DELETE FROM `',
+ REPLACE(target_table_name, '`', '``'),
+ '`'
+ );
+
+ PREPARE delete_business_table_statement FROM @delete_business_table_sql;
+ EXECUTE delete_business_table_statement;
+ DEALLOCATE PREPARE delete_business_table_statement;
+ END LOOP;
+
+ CLOSE business_table_cursor;
+
+ SELECT COUNT(*)
+ INTO variable_definition_count_after
+ FROM `ob_variable_definition`;
+
+ SELECT COUNT(*)
+ INTO material_type_count_after
+ FROM `ob_material_type`;
+
+ IF variable_definition_count_before <> variable_definition_count_after THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = '合同变量字典数量发生变化,事务已回滚';
+ END IF;
+
+ IF material_type_count_before <> material_type_count_after THEN
+ SIGNAL SQLSTATE '45000'
+ SET MESSAGE_TEXT = '上传资料类型数量发生变化,事务已回滚';
+ END IF;
+
+ COMMIT;
+ SET FOREIGN_KEY_CHECKS = old_foreign_key_checks;
+END$$
+
+DELIMITER ;
+
+CALL `ob_reset_business_data`();
+DROP PROCEDURE IF EXISTS `ob_reset_business_data`;
+
+-- 校验保留表数据。
+SELECT 'ob_variable_definition' AS `PreservedTable`, COUNT(*) AS `RemainingRows`
+FROM `ob_variable_definition`
+UNION ALL
+SELECT 'ob_material_type', COUNT(*)
+FROM `ob_material_type`;
+
+-- 查看所有入职业务表;除上面两张保留表外,其余表应为空。
+SELECT `TABLE_NAME`, `TABLE_ROWS`
+FROM `information_schema`.`TABLES`
+WHERE `TABLE_SCHEMA` = DATABASE()
+ AND LEFT(`TABLE_NAME`, 3) = 'ob_'
+ORDER BY `TABLE_NAME`;