diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml
index 9b286cb1..22377b6b 100644
--- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml
+++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml
@@ -82,7 +82,7 @@
@@ -93,7 +93,7 @@
@@ -144,15 +144,39 @@
select
- person_id,
- max(relation_name) as spouse_name,
- min(relation_cert_no) as spouse_id_card,
- max(annual_income) as spouse_income
- from ccdi_staff_fmy_relation
- where status = 1
- and is_emp_family = 1
- and relation_type = '配偶'
- group by person_id
+ relation_pair.person_id,
+ max(relation_pair.spouse_name) as spouse_name,
+ min(relation_pair.spouse_id_card) as spouse_id_card,
+ max(coalesce(spouse_staff.annual_income, relation_pair.spouse_relation_income, 0)) as spouse_income,
+ max(case when spouse_staff.id_card is not null then 1 else 0 end) as spouse_is_staff
+ from (
+ select
+ relation.person_id,
+ relation.relation_name as spouse_name,
+ relation.relation_cert_no as spouse_id_card,
+ relation.annual_income as spouse_relation_income
+ from ccdi_staff_fmy_relation relation
+ where relation.status = 1
+ and relation.is_emp_family = 1
+ and relation.relation_type = '配偶'
+ union all
+ select
+ relation.relation_cert_no as person_id,
+ base_staff.name as spouse_name,
+ relation.person_id as spouse_id_card,
+ null as spouse_relation_income
+ from ccdi_staff_fmy_relation relation
+ inner join ccdi_base_staff current_staff
+ on current_staff.id_card = relation.relation_cert_no
+ left join ccdi_base_staff base_staff
+ on base_staff.id_card = relation.person_id
+ where relation.status = 1
+ and relation.is_emp_family = 1
+ and relation.relation_type = '配偶'
+ ) relation_pair
+ left join ccdi_base_staff spouse_staff
+ on spouse_staff.id_card = relation_pair.spouse_id_card
+ group by relation_pair.person_id
@@ -306,6 +314,7 @@
aggregated.project_id,
aggregated.staff_id_card,
aggregated.spouse_id_card,
+ aggregated.spouse_is_staff,
aggregated.staff_code,
aggregated.staff_name,
aggregated.dept_name,
@@ -344,89 +353,136 @@
end as summary_risk_level_name
from (
select
- #{projectId} as project_id,
- scope.staff_id_card,
- scope.staff_code,
- scope.staff_name,
- scope.dept_name,
- spouse.spouse_id_card,
- coalesce(base_staff.annual_income, 0) as self_income,
- coalesce(spouse.spouse_income, 0) as spouse_income,
- coalesce(base_staff.annual_income, 0) + coalesce(spouse.spouse_income, 0) as total_income,
+ source.*,
case
- when coalesce((
+ when source.self_asset_record_count = 0
+ or source.spouse_staff_asset_record_count = 0 then 1
+ else 0
+ end as missing_self_asset_info,
+ case
+ when source.self_debt_record_count = 0
+ or source.spouse_staff_debt_record_count = 0 then 1
+ else 0
+ end as missing_self_debt_info
+ from (
+ select
+ #{projectId} as project_id,
+ scope.staff_id_card,
+ scope.staff_code,
+ scope.staff_name,
+ scope.dept_name,
+ spouse.spouse_id_card,
+ spouse.spouse_is_staff,
+ coalesce(base_staff.annual_income, 0) as self_income,
+ coalesce(spouse.spouse_income, 0) as spouse_income,
+ coalesce(base_staff.annual_income, 0) + coalesce(spouse.spouse_income, 0) as total_income,
+ coalesce((
select count(1)
from ccdi_asset_info asset
where asset.family_id = scope.staff_id_card
and asset.person_id = scope.staff_id_card
- ), 0) = 0 then 1
- else 0
- end as missing_self_asset_info,
- coalesce((
- select sum(coalesce(asset.current_value, 0))
- from ccdi_asset_info asset
- where asset.family_id = scope.staff_id_card
- and asset.person_id = scope.staff_id_card
- ), 0) as self_total_asset,
- coalesce((
- select sum(coalesce(asset.current_value, 0))
- from ccdi_asset_info asset
- where asset.family_id = scope.staff_id_card
- and spouse.spouse_id_card is not null
- and asset.person_id = spouse.spouse_id_card
- ), 0) as spouse_total_asset,
- coalesce((
- select sum(coalesce(asset.current_value, 0))
- from ccdi_asset_info asset
- where asset.family_id = scope.staff_id_card
- and (
- asset.person_id = scope.staff_id_card
- or (spouse.spouse_id_card is not null and asset.person_id = spouse.spouse_id_card)
- )
- ), 0) as total_asset,
- case
- when coalesce((
+ ), 0) as self_asset_record_count,
+ case
+ when spouse.spouse_is_staff = 1 then coalesce((
+ select count(1)
+ from ccdi_asset_info asset
+ where asset.family_id = spouse.spouse_id_card
+ and asset.person_id = spouse.spouse_id_card
+ ), 0)
+ else 1
+ end as spouse_staff_asset_record_count,
+ coalesce((
+ select sum(coalesce(asset.current_value, 0))
+ from ccdi_asset_info asset
+ where asset.family_id = scope.staff_id_card
+ and asset.person_id = scope.staff_id_card
+ ), 0) as self_total_asset,
+ coalesce((
+ select sum(coalesce(asset.current_value, 0))
+ from ccdi_asset_info asset
+ where spouse.spouse_id_card is not null
+ and (
+ (
+ spouse.spouse_is_staff = 1
+ and asset.family_id = spouse.spouse_id_card
+ and asset.person_id = spouse.spouse_id_card
+ )
+ or (
+ (spouse.spouse_is_staff is null or spouse.spouse_is_staff != 1)
+ and asset.family_id = scope.staff_id_card
+ and asset.person_id = spouse.spouse_id_card
+ )
+ )
+ ), 0) as spouse_total_asset,
+ coalesce((
+ select sum(coalesce(asset.current_value, 0))
+ from ccdi_asset_info asset
+ where (asset.family_id = scope.staff_id_card and asset.person_id = scope.staff_id_card)
+ or (
+ spouse.spouse_id_card is not null
+ and (
+ (
+ spouse.spouse_is_staff = 1
+ and asset.family_id = spouse.spouse_id_card
+ and asset.person_id = spouse.spouse_id_card
+ )
+ or (
+ (spouse.spouse_is_staff is null or spouse.spouse_is_staff != 1)
+ and asset.family_id = scope.staff_id_card
+ and asset.person_id = spouse.spouse_id_card
+ )
+ )
+ )
+ ), 0) as total_asset,
+ coalesce((
select count(1)
from ccdi_debts_info debt
where debt.person_id = scope.staff_id_card
- ), 0) = 0 then 1
- else 0
- end as missing_self_debt_info,
- coalesce((
- select sum(coalesce(debt.principal_balance, 0))
- from ccdi_debts_info debt
- where debt.person_id = scope.staff_id_card
- ), 0) as self_total_debt,
- coalesce((
- select sum(coalesce(debt.principal_balance, 0))
- from ccdi_debts_info debt
- where spouse.spouse_id_card is not null
- and debt.person_id = spouse.spouse_id_card
- ), 0) as spouse_total_debt,
- coalesce((
- select sum(coalesce(debt.principal_balance, 0))
- from ccdi_debts_info debt
- where debt.person_id = scope.staff_id_card
- or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
- ), 0) as total_debt,
- coalesce(base_staff.annual_income, 0)
- + coalesce(spouse.spouse_income, 0)
- + coalesce((
+ ), 0) as self_debt_record_count,
+ case
+ when spouse.spouse_is_staff = 1 then coalesce((
+ select count(1)
+ from ccdi_debts_info debt
+ where debt.person_id = spouse.spouse_id_card
+ ), 0)
+ else 1
+ end as spouse_staff_debt_record_count,
+ coalesce((
+ select sum(coalesce(debt.principal_balance, 0))
+ from ccdi_debts_info debt
+ where debt.person_id = scope.staff_id_card
+ ), 0) as self_total_debt,
+ coalesce((
+ select sum(coalesce(debt.principal_balance, 0))
+ from ccdi_debts_info debt
+ where spouse.spouse_id_card is not null
+ and debt.person_id = spouse.spouse_id_card
+ ), 0) as spouse_total_debt,
+ coalesce((
select sum(coalesce(debt.principal_balance, 0))
from ccdi_debts_info debt
where debt.person_id = scope.staff_id_card
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
- ), 0) as comparison_amount
- from (
-
- ) scope
- left join ccdi_base_staff base_staff
- on base_staff.id_card = scope.staff_id_card
- left join (
-
- ) spouse
- on spouse.person_id = scope.staff_id_card
- where scope.staff_id_card = #{staffIdCard}
+ ), 0) as total_debt,
+ coalesce(base_staff.annual_income, 0)
+ + coalesce(spouse.spouse_income, 0)
+ + coalesce((
+ select sum(coalesce(debt.principal_balance, 0))
+ from ccdi_debts_info debt
+ where debt.person_id = scope.staff_id_card
+ or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
+ ), 0) as comparison_amount
+ from (
+
+ ) scope
+ left join ccdi_base_staff base_staff
+ on base_staff.id_card = scope.staff_id_card
+ left join (
+
+ ) spouse
+ on spouse.person_id = scope.staff_id_card
+ where scope.staff_id_card = #{staffIdCard}
+ ) source
) aggregated
@@ -437,7 +493,15 @@
asset.asset_sub_type,
case
when asset.person_id = #{staffIdCard} then base_staff.name
- else spouse.relation_name
+ else coalesce(holder_staff.name, (
+ select max(relation.relation_name)
+ from ccdi_staff_fmy_relation relation
+ where relation.person_id = #{staffIdCard}
+ and relation.relation_cert_no = asset.person_id
+ and relation.status = 1
+ and relation.is_emp_family = 1
+ and relation.relation_type = '配偶'
+ ))
end as holder_name,
asset.person_id as holder_id_card,
asset.current_value,
@@ -445,16 +509,24 @@
from ccdi_asset_info asset
left join ccdi_base_staff base_staff
on base_staff.id_card = #{staffIdCard}
- left join ccdi_staff_fmy_relation spouse
- on spouse.person_id = #{staffIdCard}
- and spouse.status = 1
- and spouse.relation_type = '配偶'
- and spouse.relation_cert_no = asset.person_id
- where asset.family_id = #{staffIdCard}
- and (
- asset.person_id = #{staffIdCard}
- or (#{spouseIdCard} is not null and asset.person_id = #{spouseIdCard})
- )
+ left join ccdi_base_staff holder_staff
+ on holder_staff.id_card = asset.person_id
+ where (asset.family_id = #{staffIdCard} and asset.person_id = #{staffIdCard})
+ or (
+ #{spouseIdCard} is not null
+ and (
+ (
+ #{spouseIsStaff} = 1
+ and asset.family_id = #{spouseIdCard}
+ and asset.person_id = #{spouseIdCard}
+ )
+ or (
+ (#{spouseIsStaff} is null or #{spouseIsStaff} != 1)
+ and asset.family_id = #{staffIdCard}
+ and asset.person_id = #{spouseIdCard}
+ )
+ )
+ )
order by
case when asset.person_id = #{staffIdCard} then 1 else 2 end,
asset.valuation_date desc,
@@ -469,7 +541,15 @@
debt.creditor_type,
case
when debt.person_id = #{staffIdCard} then base_staff.name
- else spouse.relation_name
+ else coalesce(owner_staff.name, (
+ select max(relation.relation_name)
+ from ccdi_staff_fmy_relation relation
+ where relation.person_id = #{staffIdCard}
+ and relation.relation_cert_no = debt.person_id
+ and relation.status = 1
+ and relation.is_emp_family = 1
+ and relation.relation_type = '配偶'
+ ))
end as owner_name,
debt.person_id as owner_id_card,
debt.principal_balance,
@@ -477,11 +557,8 @@
from ccdi_debts_info debt
left join ccdi_base_staff base_staff
on base_staff.id_card = #{staffIdCard}
- left join ccdi_staff_fmy_relation spouse
- on spouse.person_id = #{staffIdCard}
- and spouse.status = 1
- and spouse.relation_type = '配偶'
- and spouse.relation_cert_no = debt.person_id
+ left join ccdi_base_staff owner_staff
+ on owner_staff.id_card = debt.person_id
where debt.person_id = #{staffIdCard}
or (#{spouseIdCard} is not null and debt.person_id = #{spouseIdCard})
order by
diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperDetailSqlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperDetailSqlTest.java
index c892e87a..923b66dc 100644
--- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperDetailSqlTest.java
+++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperDetailSqlTest.java
@@ -17,6 +17,13 @@ class CcdiProjectSpecialCheckMapperDetailSqlTest {
assertTrue(xml.contains("select id=\"selectFamilyAssetItemsByScope\""));
assertTrue(xml.contains("select id=\"selectFamilyDebtItemsByScope\""));
assertTrue(xml.contains("scope.staff_id_card = #{staffIdCard}"));
+ assertTrue(xml.contains("spouseIsStaff=spouse_is_staff"));
+ assertTrue(xml.contains("relation.relation_cert_no as person_id"));
+ assertTrue(xml.contains("spouse_staff.annual_income"));
+ assertTrue(xml.contains("spouse.spouse_is_staff = 1"));
+ assertTrue(xml.contains("asset.family_id = spouse.spouse_id_card"));
+ assertTrue(xml.contains("source.spouse_staff_asset_record_count = 0"));
+ assertTrue(xml.contains("source.spouse_staff_debt_record_count = 0"));
assertTrue(xml.contains("incomeDetail"));
assertTrue(xml.contains("assetDetail"));
assertTrue(xml.contains("debtDetail"));
@@ -31,6 +38,9 @@ class CcdiProjectSpecialCheckMapperDetailSqlTest {
assertTrue(xml.contains("asset_main_type"));
assertTrue(xml.contains("asset_sub_type"));
assertTrue(xml.contains("holder_name"));
+ assertTrue(xml.contains("holder_staff.name"));
+ assertTrue(xml.contains("select max(relation.relation_name)"));
+ assertTrue(xml.contains("relation.relation_cert_no = asset.person_id"));
assertTrue(xml.contains("current_value"));
assertTrue(xml.contains("valuation_date"));
assertTrue(xml.contains("debt_name"));
@@ -38,6 +48,8 @@ class CcdiProjectSpecialCheckMapperDetailSqlTest {
assertTrue(xml.contains("debt_sub_type"));
assertTrue(xml.contains("creditor_type"));
assertTrue(xml.contains("owner_name"));
+ assertTrue(xml.contains("owner_staff.name"));
+ assertTrue(xml.contains("relation.relation_cert_no = debt.person_id"));
assertTrue(xml.contains("principal_balance"));
assertTrue(xml.contains("query_date"));
assertFalse(xml.contains("ccdi_project_overview_employee_result"));
diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperListSqlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperListSqlTest.java
index b885b18d..48b85373 100644
--- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperListSqlTest.java
+++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperListSqlTest.java
@@ -19,11 +19,25 @@ class CcdiProjectSpecialCheckMapperListSqlTest {
assertTrue(xml.contains("ccdi_base_staff"));
assertTrue(xml.contains("ccdi_staff_fmy_relation"));
assertTrue(xml.contains("relation_type = '配偶'"));
+ assertTrue(xml.contains("union all"));
+ assertTrue(xml.contains("relation.relation_cert_no as person_id"));
+ assertTrue(xml.contains("inner join ccdi_base_staff current_staff"));
+ assertTrue(xml.contains("spouse_staff.annual_income"));
+ assertTrue(xml.contains("spouse_is_staff"));
assertTrue(xml.contains("annual_income"));
assertTrue(xml.contains("current_value"));
assertTrue(xml.contains("principal_balance"));
assertTrue(listSql.contains("self_asset_record_count"));
+ assertTrue(listSql.contains("spouse_staff_asset_record_count"));
assertTrue(listSql.contains("self_debt_record_count"));
+ assertTrue(listSql.contains("spouse_staff_debt_record_count"));
+ assertTrue(listSql.contains("source.self_asset_record_count = 0"));
+ assertTrue(listSql.contains("source.spouse_staff_asset_record_count = 0"));
+ assertTrue(listSql.contains("source.self_debt_record_count = 0"));
+ assertTrue(listSql.contains("source.spouse_staff_debt_record_count = 0"));
+ assertTrue(listSql.contains("asset.family_id = spouse.spouse_id_card"));
+ assertTrue(listSql.contains("asset.person_id = spouse.spouse_id_card"));
+ assertTrue(listSql.contains("debt.person_id = spouse.spouse_id_card"));
assertTrue(listSql.contains("then 'MISSING_INFO'"));
assertTrue(listSql.contains("then '缺少信息'"));
assertTrue(listSql.contains("comparison_amount"));
diff --git a/docs/reports/implementation/2026-05-26-double-staff-spouse-family-special-check.md b/docs/reports/implementation/2026-05-26-double-staff-spouse-family-special-check.md
new file mode 100644
index 00000000..bbc4456b
--- /dev/null
+++ b/docs/reports/implementation/2026-05-26-double-staff-spouse-family-special-check.md
@@ -0,0 +1,57 @@
+# 双员工夫妻家庭专项核查实施记录
+
+- 保存路径:`docs/reports/implementation/2026-05-26-double-staff-spouse-family-special-check.md`
+- 实施日期:2026-05-26
+- 变更范围:后端专项核查家庭资产负债聚合 SQL 与对应 Mapper SQL 结构测试
+
+## 修改内容
+
+1. 调整 `CcdiProjectSpecialCheckMapper.xml` 的配偶识别口径:
+ - 支持 `本人 -> 配偶` 的直接关系。
+ - 支持 `配偶员工 -> 本人` 的反向推导,覆盖只维护单向配偶关系但双方都是员工的情况。
+ - 增加 `spouse_is_staff` 标识,配偶为员工时收入优先取 `ccdi_base_staff.annual_income`。
+
+2. 调整家庭资产、负债与缺少信息口径:
+ - 双员工夫妻家庭中,本人资产取 `family_id = 本人身份证号 AND person_id = 本人身份证号`。
+ - 配偶为员工时,配偶资产取 `family_id = 配偶身份证号 AND person_id = 配偶身份证号`。
+ - 配偶不是员工时,配偶资产仍取 `family_id = 本人身份证号 AND person_id = 配偶身份证号`。
+ - 负债继续按本人和配偶证件号从 `ccdi_debts_info` 汇总。
+ - 配偶为员工时,任一公司员工成员缺少本人资产或本人负债记录,风险结果按“缺少信息”处理。
+
+3. 调整详情明细展示查询:
+ - 资产明细按 `spouse_is_staff` 选择员工本人资产或亲属资产,避免把员工配偶作为亲属资产重复计入。
+ - 资产持有人和负债归属人名称优先取员工主档,非员工配偶再从亲属关系中取名,避免双向配偶关系导致明细行重复。
+
+4. 补充 Mapper SQL 结构测试断言:
+ - 校验双向配偶识别、员工配偶收入来源、员工配偶资产归属、员工配偶负债缺失判断和明细查询参数传递。
+
+## 影响范围
+
+- 接口路径和返回结构不变:
+ - `GET /ccdi/project/special-check/family-asset-liability/list`
+ - `GET /ccdi/project/special-check/family-asset-liability/detail`
+- 前端页面无需调整,仍按员工展示列表。
+- 不新增数据库表和字段。
+
+## 验证结果
+
+1. 主代码编译:
+ - 命令:`mvn -DskipTests compile`
+ - 目录:`ccdi-project`
+ - 结果:通过。
+
+2. Mapper XML 格式校验:
+ - 命令:`xmllint --noout ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml`
+ - 结果:通过。
+
+3. 本次变更空白检查:
+ - 命令:`git diff --check -- ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperListSqlTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperDetailSqlTest.java`
+ - 结果:通过。
+
+4. 双员工夫妻 Mapper 关键口径文本校验:
+ - 命令:Ruby 读取 Mapper XML 并检查反向配偶、员工配偶收入、员工配偶资产、员工配偶负债和明细参数关键片段。
+ - 结果:通过,输出 `double-staff spouse mapper checks passed`。
+
+5. 专项 JUnit:
+ - 命令:`mvn test -Dtest=CcdiProjectSpecialCheckMapperListSqlTest,CcdiProjectSpecialCheckMapperDetailSqlTest`
+ - 结果:未完成执行。当前模块测试编译阶段被既有无关测试错误阻断,错误集中在 `CcdiBankStatementTest` 与 `CcdiFileUploadServiceImplTest` 中旧接口签名不匹配,本次未修改这些文件。