Compare commits

...

6 Commits

60 changed files with 5592 additions and 16 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
assets/专项核查.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -0,0 +1,13 @@
# 在专项核查页面开发员工家庭资产负债专项核查功能
- 范围为项目内的员工
- 家庭总年收入与家庭总负债之和,对比家庭总资产之和
- 家庭总年收入为员工本人年收入 + 员工配偶年收入(如有)
- 家庭总资产之和为员工与配偶关联的资产总和
- 家庭总负债为来源于员工和配偶的征信中的贷款与负债总和
- 总年收入+ 总负债 < 总资产 * 1.5 时,为正常
- 总资产 * 3 > 总年收入+ 总负债 > 总资产 * 1.5 时,提示存在风险
- 总年收入+ 总负债 > 总资产 * 3 时,提示高风险
- 在专项核查页面添加一个卡片,标题为员工家庭资产负债专项核查
- 卡片内展示项目内员工核查列表,展示每个员工家庭的总收入 总资产,总负债和风险情况。点开详情展示所有数据细项。
- 展示风格与结果总览其他组件的展示效果统一
- 使用frontend-design设计前端展示效果

View File

@@ -0,0 +1,51 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 项目专项核查控制器
*/
@RestController
@RequestMapping("/ccdi/project/special-check")
@Tag(name = "项目专项核查")
public class CcdiProjectSpecialCheckController extends BaseController {
@Resource
private ICcdiProjectSpecialCheckService specialCheckService;
/**
* 查询员工家庭资产负债列表
*/
@GetMapping("/family-asset-liability/list")
@Operation(summary = "查询员工家庭资产负债列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getFamilyAssetLiabilityList(@Validated CcdiProjectFamilyAssetLiabilityListQueryDTO queryDTO) {
CcdiProjectFamilyAssetLiabilityListVO result = specialCheckService.getFamilyAssetLiabilityList(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询员工家庭资产负债详情
*/
@GetMapping("/family-asset-liability/detail")
@Operation(summary = "查询员工家庭资产负债详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getFamilyAssetLiabilityDetail(@Validated CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO) {
CcdiProjectFamilyAssetLiabilityDetailVO result = specialCheckService.getFamilyAssetLiabilityDetail(queryDTO);
return AjaxResult.success(result);
}
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 员工家庭资产负债详情查询DTO
*/
@Data
public class CcdiProjectFamilyAssetLiabilityDetailQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 员工身份证号 */
@NotBlank(message = "员工身份证号不能为空")
private String staffIdCard;
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 员工家庭资产负债列表查询DTO
*/
@Data
public class CcdiProjectFamilyAssetLiabilityListQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import java.util.List;
import lombok.Data;
/**
* 员工家庭资产明细VO
*/
@Data
public class CcdiProjectFamilyAssetDetailVO {
/** 本人是否缺少资产信息 */
private Boolean missingSelfAssetInfo;
/** 本人资产小计 */
private BigDecimal selfTotalAsset;
/** 配偶资产小计 */
private BigDecimal spouseTotalAsset;
/** 家庭总资产 */
private BigDecimal totalAsset;
/** 资产明细 */
private List<CcdiProjectFamilyAssetItemVO> items;
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
/**
* 员工家庭资产明细项VO
*/
@Data
public class CcdiProjectFamilyAssetItemVO {
/** 资产名称 */
private String assetName;
/** 资产大类 */
private String assetMainType;
/** 资产小类 */
private String assetSubType;
/** 持有人姓名 */
private String holderName;
/** 持有人证件号 */
private String holderIdCard;
/** 当前估值 */
private BigDecimal currentValue;
/** 估值日期 */
private Date valuationDate;
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 员工家庭资产负债详情VO
*/
@Data
public class CcdiProjectFamilyAssetLiabilityDetailVO {
/** 收入明细 */
private CcdiProjectFamilyIncomeDetailVO incomeDetail;
/** 资产明细 */
private CcdiProjectFamilyAssetDetailVO assetDetail;
/** 负债明细 */
private CcdiProjectFamilyDebtDetailVO debtDetail;
/** 汇总信息 */
private CcdiProjectFamilyAssetLiabilityListItemVO summary;
}

View File

@@ -0,0 +1,41 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
* 员工家庭资产负债列表项VO
*/
@Data
public class CcdiProjectFamilyAssetLiabilityListItemVO {
/** 员工身份证号 */
private String staffIdCard;
/** 员工工号 */
private String staffCode;
/** 员工姓名 */
private String staffName;
/** 所属部门 */
private String deptName;
/** 家庭总年收入 */
private BigDecimal totalIncome;
/** 家庭总资产 */
private BigDecimal totalAsset;
/** 家庭总负债 */
private BigDecimal totalDebt;
/** 收入负债对比金额 */
private BigDecimal comparisonAmount;
/** 风险等级编码 */
private String riskLevelCode;
/** 风险等级名称 */
private String riskLevelName;
}

View File

@@ -0,0 +1,14 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 员工家庭资产负债列表VO
*/
@Data
public class CcdiProjectFamilyAssetLiabilityListVO {
/** 列表数据 */
private List<CcdiProjectFamilyAssetLiabilityListItemVO> rows;
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import java.util.List;
import lombok.Data;
/**
* 员工家庭负债明细VO
*/
@Data
public class CcdiProjectFamilyDebtDetailVO {
/** 本人是否缺少负债信息 */
private Boolean missingSelfDebtInfo;
/** 本人负债小计 */
private BigDecimal selfTotalDebt;
/** 配偶负债小计 */
private BigDecimal spouseTotalDebt;
/** 家庭总负债 */
private BigDecimal totalDebt;
/** 负债明细 */
private List<CcdiProjectFamilyDebtItemVO> items;
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
/**
* 员工家庭负债明细项VO
*/
@Data
public class CcdiProjectFamilyDebtItemVO {
/** 负债名称 */
private String debtName;
/** 负债大类 */
private String debtMainType;
/** 负债小类 */
private String debtSubType;
/** 债权人类型 */
private String creditorType;
/** 归属人姓名 */
private String ownerName;
/** 归属人证件号 */
private String ownerIdCard;
/** 本金余额 */
private BigDecimal principalBalance;
/** 查询日期 */
private Date queryDate;
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
* 员工家庭收入明细VO
*/
@Data
public class CcdiProjectFamilyIncomeDetailVO {
/** 本人年收入 */
private BigDecimal selfIncome;
/** 配偶年收入 */
private BigDecimal spouseIncome;
/** 家庭总年收入 */
private BigDecimal totalIncome;
}

View File

@@ -0,0 +1,64 @@
package com.ruoyi.ccdi.project.mapper;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtItemVO;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 项目专项核查Mapper
*/
@Mapper
public interface CcdiProjectSpecialCheckMapper {
/**
* 查询员工家庭资产负债列表
*
* @param projectId 项目ID
* @return 列表数据
*/
List<CcdiProjectFamilyAssetLiabilityListItemVO> selectFamilyAssetLiabilityList(@Param("projectId") Long projectId);
/**
* 查询员工家庭资产负债详情
*
* @param projectId 项目ID
* @param staffIdCard 员工身份证号
* @return 详情结果
*/
CcdiProjectFamilyAssetLiabilityDetailVO selectFamilyAssetLiabilityDetail(
@Param("projectId") Long projectId,
@Param("staffIdCard") String staffIdCard
);
/**
* 查询员工家庭资产明细
*
* @param projectId 项目ID
* @param staffIdCard 员工身份证号
* @param spouseIdCard 配偶身份证号
* @return 资产明细
*/
List<CcdiProjectFamilyAssetItemVO> selectFamilyAssetItemsByScope(
@Param("projectId") Long projectId,
@Param("staffIdCard") String staffIdCard,
@Param("spouseIdCard") String spouseIdCard
);
/**
* 查询员工家庭负债明细
*
* @param projectId 项目ID
* @param staffIdCard 员工身份证号
* @param spouseIdCard 配偶身份证号
* @return 负债明细
*/
List<CcdiProjectFamilyDebtItemVO> selectFamilyDebtItemsByScope(
@Param("projectId") Long projectId,
@Param("staffIdCard") String staffIdCard,
@Param("spouseIdCard") String spouseIdCard
);
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.ccdi.project.service;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
/**
* 项目专项核查服务接口
*/
public interface ICcdiProjectSpecialCheckService {
/**
* 查询员工家庭资产负债列表
*
* @param queryDTO 查询条件
* @return 列表结果
*/
CcdiProjectFamilyAssetLiabilityListVO getFamilyAssetLiabilityList(
CcdiProjectFamilyAssetLiabilityListQueryDTO queryDTO
);
/**
* 查询员工家庭资产负债详情
*
* @param queryDTO 查询条件
* @return 详情结果
*/
CcdiProjectFamilyAssetLiabilityDetailVO getFamilyAssetLiabilityDetail(
CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO
);
}

View File

@@ -0,0 +1,115 @@
package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyIncomeDetailVO;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper;
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
import com.ruoyi.common.exception.ServiceException;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.stereotype.Service;
/**
* 项目专项核查服务实现
*/
@Service
public class CcdiProjectSpecialCheckServiceImpl implements ICcdiProjectSpecialCheckService {
@Resource
private CcdiProjectSpecialCheckMapper specialCheckMapper;
@Resource
private CcdiProjectMapper projectMapper;
@Override
public CcdiProjectFamilyAssetLiabilityListVO getFamilyAssetLiabilityList(
CcdiProjectFamilyAssetLiabilityListQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
CcdiProjectFamilyAssetLiabilityListVO result = new CcdiProjectFamilyAssetLiabilityListVO();
result.setRows(defaultList(specialCheckMapper.selectFamilyAssetLiabilityList(queryDTO.getProjectId())));
return result;
}
@Override
public CcdiProjectFamilyAssetLiabilityDetailVO getFamilyAssetLiabilityDetail(
CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
CcdiProjectFamilyAssetLiabilityDetailVO detail = specialCheckMapper.selectFamilyAssetLiabilityDetail(
queryDTO.getProjectId(),
queryDTO.getStaffIdCard()
);
if (detail == null) {
throw new ServiceException("当前员工不属于该项目专项核查范围");
}
normalizeDetail(detail);
return detail;
}
private void ensureProjectExists(Long projectId) {
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {
throw new ServiceException("项目不存在");
}
}
private void normalizeDetail(CcdiProjectFamilyAssetLiabilityDetailVO detail) {
if (detail.getIncomeDetail() == null) {
CcdiProjectFamilyIncomeDetailVO incomeDetail = new CcdiProjectFamilyIncomeDetailVO();
incomeDetail.setSelfIncome(BigDecimal.ZERO);
incomeDetail.setSpouseIncome(BigDecimal.ZERO);
incomeDetail.setTotalIncome(BigDecimal.ZERO);
detail.setIncomeDetail(incomeDetail);
}
if (detail.getAssetDetail() == null) {
CcdiProjectFamilyAssetDetailVO assetDetail = new CcdiProjectFamilyAssetDetailVO();
assetDetail.setMissingSelfAssetInfo(false);
assetDetail.setSelfTotalAsset(BigDecimal.ZERO);
assetDetail.setSpouseTotalAsset(BigDecimal.ZERO);
assetDetail.setTotalAsset(BigDecimal.ZERO);
assetDetail.setItems(List.of());
detail.setAssetDetail(assetDetail);
} else if (detail.getAssetDetail().getItems() == null) {
detail.getAssetDetail().setItems(List.of());
}
if (detail.getAssetDetail().getMissingSelfAssetInfo() == null) {
detail.getAssetDetail().setMissingSelfAssetInfo(false);
}
if (detail.getDebtDetail() == null) {
CcdiProjectFamilyDebtDetailVO debtDetail = new CcdiProjectFamilyDebtDetailVO();
debtDetail.setMissingSelfDebtInfo(false);
debtDetail.setSelfTotalDebt(BigDecimal.ZERO);
debtDetail.setSpouseTotalDebt(BigDecimal.ZERO);
debtDetail.setTotalDebt(BigDecimal.ZERO);
debtDetail.setItems(List.of());
detail.setDebtDetail(debtDetail);
} else if (detail.getDebtDetail().getItems() == null) {
detail.getDebtDetail().setItems(List.of());
}
if (detail.getDebtDetail().getMissingSelfDebtInfo() == null) {
detail.getDebtDetail().setMissingSelfDebtInfo(false);
}
if (detail.getSummary() == null) {
detail.setSummary(new CcdiProjectFamilyAssetLiabilityListItemVO());
}
}
private <T> List<T> defaultList(List<T> list) {
return list == null ? List.of() : list;
}
}

View File

@@ -0,0 +1,465 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper">
<resultMap id="FamilyAssetLiabilityListItemResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO">
<id property="staffIdCard" column="staff_id_card"/>
<result property="staffCode" column="staff_code"/>
<result property="staffName" column="staff_name"/>
<result property="deptName" column="dept_name"/>
<result property="totalIncome" column="total_income"/>
<result property="totalAsset" column="total_asset"/>
<result property="totalDebt" column="total_debt"/>
<result property="comparisonAmount" column="comparison_amount"/>
<result property="riskLevelCode" column="risk_level_code"/>
<result property="riskLevelName" column="risk_level_name"/>
</resultMap>
<resultMap id="FamilyAssetItemResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetItemVO">
<result property="assetName" column="asset_name"/>
<result property="assetMainType" column="asset_main_type"/>
<result property="assetSubType" column="asset_sub_type"/>
<result property="holderName" column="holder_name"/>
<result property="holderIdCard" column="holder_id_card"/>
<result property="currentValue" column="current_value"/>
<result property="valuationDate" column="valuation_date"/>
</resultMap>
<resultMap id="FamilyDebtItemResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtItemVO">
<result property="debtName" column="debt_name"/>
<result property="debtMainType" column="debt_main_type"/>
<result property="debtSubType" column="debt_sub_type"/>
<result property="creditorType" column="creditor_type"/>
<result property="ownerName" column="owner_name"/>
<result property="ownerIdCard" column="owner_id_card"/>
<result property="principalBalance" column="principal_balance"/>
<result property="queryDate" column="query_date"/>
</resultMap>
<resultMap id="FamilyAssetLiabilityDetailResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO">
<association property="incomeDetail"
javaType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyIncomeDetailVO">
<result property="selfIncome" column="income_self_income"/>
<result property="spouseIncome" column="income_spouse_income"/>
<result property="totalIncome" column="income_total_income"/>
</association>
<association property="assetDetail"
javaType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetDetailVO">
<result property="missingSelfAssetInfo" column="asset_missing_self_asset_info"/>
<result property="selfTotalAsset" column="asset_self_total_asset"/>
<result property="spouseTotalAsset" column="asset_spouse_total_asset"/>
<result property="totalAsset" column="asset_total_asset"/>
<collection property="items"
column="{projectId=project_id,staffIdCard=staff_id_card,spouseIdCard=spouse_id_card}"
ofType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetItemVO"
select="selectFamilyAssetItemsByScope"/>
</association>
<association property="debtDetail"
javaType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtDetailVO">
<result property="missingSelfDebtInfo" column="debt_missing_self_debt_info"/>
<result property="selfTotalDebt" column="debt_self_total_debt"/>
<result property="spouseTotalDebt" column="debt_spouse_total_debt"/>
<result property="totalDebt" column="debt_total_debt"/>
<collection property="items"
column="{projectId=project_id,staffIdCard=staff_id_card,spouseIdCard=spouse_id_card}"
ofType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtItemVO"
select="selectFamilyDebtItemsByScope"/>
</association>
<association property="summary"
javaType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO">
<id property="staffIdCard" column="summary_staff_id_card"/>
<result property="staffCode" column="summary_staff_code"/>
<result property="staffName" column="summary_staff_name"/>
<result property="deptName" column="summary_dept_name"/>
<result property="totalIncome" column="summary_total_income"/>
<result property="totalAsset" column="summary_total_asset"/>
<result property="totalDebt" column="summary_total_debt"/>
<result property="comparisonAmount" column="summary_comparison_amount"/>
<result property="riskLevelCode" column="summary_risk_level_code"/>
<result property="riskLevelName" column="summary_risk_level_name"/>
</association>
</resultMap>
<sql id="projectEmployeeScopeSql">
select distinct
coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) as staff_id_card,
cast(coalesce(direct_staff.staff_id, statement_staff.staff_id, family_staff.staff_id) as char) as staff_code,
coalesce(direct_staff.name, statement_staff.name, family_staff.name) as staff_name,
dept.dept_name
from ccdi_bank_statement_tag_result tr
left join ccdi_base_staff direct_staff
on tr.object_type = 'STAFF_ID_CARD'
and tr.object_key = direct_staff.id_card
left join ccdi_bank_statement bs
on tr.bank_statement_id = bs.bank_statement_id
left join ccdi_base_staff statement_staff
on (tr.object_key is null or tr.object_key = '')
and bs.cret_no = statement_staff.id_card
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and (
((tr.object_key is null or tr.object_key = '') and bs.cret_no = relation.relation_cert_no)
or ((tr.object_key is not null and tr.object_key != '') and tr.object_type != 'STAFF_ID_CARD'
and tr.object_key = relation.relation_cert_no)
)
left join ccdi_base_staff family_staff
on relation.person_id = family_staff.id_card
left join sys_dept dept
on dept.dept_id = coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id)
where tr.project_id = #{projectId}
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
</sql>
<sql id="spouseRelationSql">
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
</sql>
<select id="selectFamilyAssetLiabilityList" resultMap="FamilyAssetLiabilityListItemResultMap">
select
aggregated.staff_id_card,
aggregated.staff_code,
aggregated.staff_name,
aggregated.dept_name,
aggregated.total_income,
aggregated.total_asset,
aggregated.total_debt,
aggregated.comparison_amount,
case
when aggregated.self_asset_record_count = 0 or aggregated.self_debt_record_count = 0 then 'MISSING_INFO'
when comparison_amount &lt;= total_asset * 1.5 then 'NORMAL'
when comparison_amount &gt; total_asset * 1.5 and comparison_amount &lt;= total_asset * 3 then 'RISK'
when comparison_amount &gt; total_asset * 3 then 'HIGH'
else 'HIGH'
end as risk_level_code,
case
when aggregated.self_asset_record_count = 0 or aggregated.self_debt_record_count = 0 then '缺少信息'
when comparison_amount &lt;= total_asset * 1.5 then '正常'
when comparison_amount &gt; total_asset * 1.5 and comparison_amount &lt;= total_asset * 3 then '存在风险'
when comparison_amount &gt; total_asset * 3 then '高风险'
else '高风险'
end as risk_level_name
from (
select
scope.staff_id_card,
scope.staff_code,
scope.staff_name,
scope.dept_name,
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) as self_asset_record_count,
coalesce((
select count(1)
from ccdi_debts_info debt
where debt.person_id = scope.staff_id_card
), 0) as self_debt_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
or (spouse.spouse_id_card is not null and asset.person_id = spouse.spouse_id_card)
)
), 0) as total_asset,
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((
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,
case
when 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
or coalesce((
select count(1)
from ccdi_debts_info debt
where debt.person_id = scope.staff_id_card
), 0) = 0 then 4
when (
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)
) &lt;= 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) * 1.5 then 1
when (
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)
) &lt;= 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) * 3 then 2
when (
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)
) &gt; 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) * 3 then 3
else 3
end as risk_level_sort
from (
<include refid="projectEmployeeScopeSql"/>
) scope
left join ccdi_base_staff base_staff
on base_staff.id_card = scope.staff_id_card
left join (
<include refid="spouseRelationSql"/>
) spouse
on spouse.person_id = scope.staff_id_card
) aggregated
order by risk_level_sort desc, comparison_amount desc, staff_name asc
</select>
<select id="selectFamilyAssetLiabilityDetail" resultMap="FamilyAssetLiabilityDetailResultMap">
select
aggregated.project_id,
aggregated.staff_id_card,
aggregated.spouse_id_card,
aggregated.staff_code,
aggregated.staff_name,
aggregated.dept_name,
aggregated.self_income as income_self_income,
aggregated.spouse_income as income_spouse_income,
aggregated.total_income as income_total_income,
aggregated.missing_self_asset_info as asset_missing_self_asset_info,
aggregated.self_total_asset as asset_self_total_asset,
aggregated.spouse_total_asset as asset_spouse_total_asset,
aggregated.total_asset as asset_total_asset,
aggregated.missing_self_debt_info as debt_missing_self_debt_info,
aggregated.self_total_debt as debt_self_total_debt,
aggregated.spouse_total_debt as debt_spouse_total_debt,
aggregated.total_debt as debt_total_debt,
aggregated.staff_id_card as summary_staff_id_card,
aggregated.staff_code as summary_staff_code,
aggregated.staff_name as summary_staff_name,
aggregated.dept_name as summary_dept_name,
aggregated.total_income as summary_total_income,
aggregated.total_asset as summary_total_asset,
aggregated.total_debt as summary_total_debt,
aggregated.comparison_amount as summary_comparison_amount,
case
when aggregated.missing_self_asset_info = 1 or aggregated.missing_self_debt_info = 1 then 'MISSING_INFO'
when comparison_amount &lt;= total_asset * 1.5 then 'NORMAL'
when comparison_amount &gt; total_asset * 1.5 and comparison_amount &lt;= total_asset * 3 then 'RISK'
when comparison_amount &gt; total_asset * 3 then 'HIGH'
else 'HIGH'
end as summary_risk_level_code,
case
when aggregated.missing_self_asset_info = 1 or aggregated.missing_self_debt_info = 1 then '缺少信息'
when comparison_amount &lt;= total_asset * 1.5 then '正常'
when comparison_amount &gt; total_asset * 1.5 and comparison_amount &lt;= total_asset * 3 then '存在风险'
when comparison_amount &gt; total_asset * 3 then '高风险'
else '高风险'
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,
case
when 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((
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((
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 (
<include refid="projectEmployeeScopeSql"/>
) scope
left join ccdi_base_staff base_staff
on base_staff.id_card = scope.staff_id_card
left join (
<include refid="spouseRelationSql"/>
) spouse
on spouse.person_id = scope.staff_id_card
where scope.staff_id_card = #{staffIdCard}
) aggregated
</select>
<select id="selectFamilyAssetItemsByScope" resultMap="FamilyAssetItemResultMap">
select
asset.asset_name,
asset.asset_main_type,
asset.asset_sub_type,
case
when asset.person_id = #{staffIdCard} then base_staff.name
else spouse.relation_name
end as holder_name,
asset.person_id as holder_id_card,
asset.current_value,
asset.valuation_date
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})
)
order by
case when asset.person_id = #{staffIdCard} then 1 else 2 end,
asset.valuation_date desc,
asset.asset_name asc
</select>
<select id="selectFamilyDebtItemsByScope" resultMap="FamilyDebtItemResultMap">
select
debt.debt_name,
debt.debt_main_type,
debt.debt_sub_type,
debt.creditor_type,
case
when debt.person_id = #{staffIdCard} then base_staff.name
else spouse.relation_name
end as owner_name,
debt.person_id as owner_id_card,
debt.principal_balance,
debt.query_date
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
where debt.person_id = #{staffIdCard}
or (#{spouseIdCard} is not null and debt.person_id = #{spouseIdCard})
order by
case when debt.person_id = #{staffIdCard} then 1 else 2 end,
debt.query_date desc,
debt.debt_name asc
</select>
</mapper>

View File

@@ -0,0 +1,143 @@
package com.ruoyi.ccdi.project.controller;
import io.swagger.v3.oas.annotations.Operation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectSpecialCheckControllerContractTest {
@Test
void shouldExposeFamilyAssetLiabilityListEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectSpecialCheckController");
Class<?> queryDtoClass = Class.forName(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO"
);
RequestMapping requestMapping = controllerClass.getAnnotation(RequestMapping.class);
Method method = controllerClass.getMethod("getFamilyAssetLiabilityList", queryDtoClass);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(requestMapping);
assertEquals("/ccdi/project/special-check", requestMapping.value()[0]);
assertNotNull(getMapping);
assertEquals("/family-asset-liability/list", getMapping.value()[0]);
assertNotNull(operation);
assertEquals(queryDtoClass, method.getParameterTypes()[0]);
}
@Test
void shouldExposeFamilyAssetLiabilityDetailEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectSpecialCheckController");
Class<?> queryDtoClass = Class.forName(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO"
);
Method method = controllerClass.getMethod("getFamilyAssetLiabilityDetail", queryDtoClass);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals("/family-asset-liability/detail", getMapping.value()[0]);
assertNotNull(operation);
assertEquals(queryDtoClass, method.getParameterTypes()[0]);
}
@Test
void shouldExposeFamilyAssetLiabilityDtoFields() throws Exception {
Class<?> listDtoClass = Class.forName(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO"
);
Class<?> detailDtoClass = Class.forName(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO"
);
List<String> listFields = Arrays.stream(listDtoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
List<String> detailFields = Arrays.stream(detailDtoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
assertTrue(listFields.contains("projectId"));
assertTrue(detailFields.contains("projectId"));
assertTrue(detailFields.contains("staffIdCard"));
}
@Test
void shouldExposeFamilyAssetLiabilityListVoFields() throws Exception {
Class<?> itemVoClass = Class.forName(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO"
);
Class<?> listVoClass = Class.forName(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO"
);
List<String> itemFields = Arrays.stream(itemVoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
List<String> listFields = Arrays.stream(listVoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
assertTrue(itemFields.contains("staffIdCard"));
assertTrue(itemFields.contains("staffCode"));
assertTrue(itemFields.contains("staffName"));
assertTrue(itemFields.contains("deptName"));
assertTrue(itemFields.contains("totalIncome"));
assertTrue(itemFields.contains("totalAsset"));
assertTrue(itemFields.contains("totalDebt"));
assertTrue(itemFields.contains("comparisonAmount"));
assertTrue(itemFields.contains("riskLevelCode"));
assertTrue(itemFields.contains("riskLevelName"));
assertTrue(listFields.contains("rows"));
}
@Test
void shouldExposeFamilyAssetLiabilityDetailVoFields() throws Exception {
Class<?> detailVoClass = Class.forName(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO"
);
Class<?> incomeDetailClass = Class.forName(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyIncomeDetailVO"
);
Class<?> assetDetailClass = Class.forName(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetDetailVO"
);
Class<?> debtDetailClass = Class.forName(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtDetailVO"
);
List<String> detailFields = Arrays.stream(detailVoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
List<String> incomeFields = Arrays.stream(incomeDetailClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
List<String> assetFields = Arrays.stream(assetDetailClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
List<String> debtFields = Arrays.stream(debtDetailClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
assertTrue(detailFields.contains("incomeDetail"));
assertTrue(detailFields.contains("assetDetail"));
assertTrue(detailFields.contains("debtDetail"));
assertTrue(detailFields.contains("summary"));
assertTrue(incomeFields.contains("selfIncome"));
assertTrue(incomeFields.contains("spouseIncome"));
assertTrue(assetFields.contains("items"));
assertTrue(debtFields.contains("items"));
}
}

View File

@@ -0,0 +1,90 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiProjectSpecialCheckControllerTest {
@InjectMocks
private CcdiProjectSpecialCheckController controller;
@Mock
private ICcdiProjectSpecialCheckService specialCheckService;
@Test
void shouldExposeFamilyAssetLiabilityListEndpoint() throws Exception {
CcdiProjectFamilyAssetLiabilityListQueryDTO queryDTO = new CcdiProjectFamilyAssetLiabilityListQueryDTO();
queryDTO.setProjectId(40L);
when(specialCheckService.getFamilyAssetLiabilityList(queryDTO)).thenReturn(new CcdiProjectFamilyAssetLiabilityListVO());
AjaxResult result = controller.getFamilyAssetLiabilityList(queryDTO);
assertEquals(200, result.get("code"));
verify(specialCheckService).getFamilyAssetLiabilityList(queryDTO);
RequestMapping mapping = CcdiProjectSpecialCheckController.class.getAnnotation(RequestMapping.class);
Method method = CcdiProjectSpecialCheckController.class.getMethod(
"getFamilyAssetLiabilityList",
CcdiProjectFamilyAssetLiabilityListQueryDTO.class
);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(mapping);
assertEquals("/ccdi/project/special-check", mapping.value()[0]);
assertNotNull(getMapping);
assertEquals("/family-asset-liability/list", getMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
assertNotNull(operation);
}
@Test
void shouldExposeFamilyAssetLiabilityDetailEndpoint() throws Exception {
CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO = new CcdiProjectFamilyAssetLiabilityDetailQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setStaffIdCard("330102199001011234");
when(specialCheckService.getFamilyAssetLiabilityDetail(queryDTO))
.thenReturn(new CcdiProjectFamilyAssetLiabilityDetailVO());
AjaxResult result = controller.getFamilyAssetLiabilityDetail(queryDTO);
assertEquals(200, result.get("code"));
verify(specialCheckService).getFamilyAssetLiabilityDetail(queryDTO);
Method method = CcdiProjectSpecialCheckController.class.getMethod(
"getFamilyAssetLiabilityDetail",
CcdiProjectFamilyAssetLiabilityDetailQueryDTO.class
);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals("/family-asset-liability/detail", getMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
assertNotNull(operation);
}
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.ccdi.project.mapper;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectSpecialCheckMapperDetailSqlTest {
@Test
void shouldBuildFamilyAssetLiabilityDetailStructureFromProjectScope() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml"));
assertTrue(xml.contains("select id=\"selectFamilyAssetLiabilityDetail\""));
assertTrue(xml.contains("select id=\"selectFamilyAssetItemsByScope\""));
assertTrue(xml.contains("select id=\"selectFamilyDebtItemsByScope\""));
assertTrue(xml.contains("scope.staff_id_card = #{staffIdCard}"));
assertTrue(xml.contains("incomeDetail"));
assertTrue(xml.contains("assetDetail"));
assertTrue(xml.contains("debtDetail"));
assertTrue(xml.contains("summary"));
assertTrue(xml.contains("self_total_asset"));
assertTrue(xml.contains("spouse_total_asset"));
assertTrue(xml.contains("self_total_debt"));
assertTrue(xml.contains("spouse_total_debt"));
assertTrue(xml.contains("asset_missing_self_asset_info"));
assertTrue(xml.contains("debt_missing_self_debt_info"));
assertTrue(xml.contains("asset_name"));
assertTrue(xml.contains("asset_main_type"));
assertTrue(xml.contains("asset_sub_type"));
assertTrue(xml.contains("holder_name"));
assertTrue(xml.contains("current_value"));
assertTrue(xml.contains("valuation_date"));
assertTrue(xml.contains("debt_name"));
assertTrue(xml.contains("debt_main_type"));
assertTrue(xml.contains("debt_sub_type"));
assertTrue(xml.contains("creditor_type"));
assertTrue(xml.contains("owner_name"));
assertTrue(xml.contains("principal_balance"));
assertTrue(xml.contains("query_date"));
assertFalse(xml.contains("ccdi_project_overview_employee_result"));
}
}

View File

@@ -0,0 +1,43 @@
package com.ruoyi.ccdi.project.mapper;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectSpecialCheckMapperListSqlTest {
@Test
void shouldAggregateFamilyAssetLiabilityListByProjectEmployeeScope() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml"));
String listSql = extractSelect(xml, "selectFamilyAssetLiabilityList");
assertTrue(listSql.contains("order by risk_level_sort desc, comparison_amount desc, staff_name asc"));
assertTrue(xml.contains("from ccdi_bank_statement_tag_result"));
assertTrue(xml.contains("ccdi_base_staff"));
assertTrue(xml.contains("ccdi_staff_fmy_relation"));
assertTrue(xml.contains("relation_type = '配偶'"));
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("self_debt_record_count"));
assertTrue(listSql.contains("then 'MISSING_INFO'"));
assertTrue(listSql.contains("then '缺少信息'"));
assertTrue(listSql.contains("comparison_amount"));
assertTrue(listSql.contains("&lt;= total_asset * 1.5"));
assertTrue(listSql.contains("&gt; total_asset * 1.5"));
assertTrue(listSql.contains("&lt;= total_asset * 3"));
assertTrue(listSql.contains("&gt; total_asset * 3"));
assertFalse(xml.contains("ccdi_project_overview_employee_result"));
}
private String extractSelect(String xml, String selectId) {
String start = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start);
int endIndex = xml.indexOf("</select>", startIndex);
return xml.substring(startIndex, endIndex);
}
}

View File

@@ -0,0 +1,159 @@
package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyIncomeDetailVO;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper;
import com.ruoyi.common.exception.ServiceException;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiProjectSpecialCheckServiceImplTest {
@InjectMocks
private CcdiProjectSpecialCheckServiceImpl service;
@Mock
private CcdiProjectSpecialCheckMapper specialCheckMapper;
@Mock
private CcdiProjectMapper projectMapper;
@Test
void shouldThrowWhenProjectDoesNotExist() {
when(projectMapper.selectById(99L)).thenReturn(null);
assertThrows(ServiceException.class, () -> service.getFamilyAssetLiabilityList(buildListQuery(99L)));
assertThrows(ServiceException.class, () -> service.getFamilyAssetLiabilityDetail(buildDetailQuery(99L, "3301")));
}
@Test
void shouldReturnEmptyRowsWhenListMapperReturnsNull() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L));
when(specialCheckMapper.selectFamilyAssetLiabilityList(40L)).thenReturn(null);
CcdiProjectFamilyAssetLiabilityListVO result = service.getFamilyAssetLiabilityList(buildListQuery(40L));
assertNotNull(result.getRows());
assertEquals(0, result.getRows().size());
}
@Test
void shouldThrowClearErrorWhenStaffDoesNotBelongToProjectScope() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L));
when(specialCheckMapper.selectFamilyAssetLiabilityDetail(40L, "330102199001011234")).thenReturn(null);
ServiceException exception = assertThrows(
ServiceException.class,
() -> service.getFamilyAssetLiabilityDetail(buildDetailQuery(40L, "330102199001011234"))
);
assertEquals("当前员工不属于该项目专项核查范围", exception.getMessage());
}
@Test
void shouldKeepDetailSummaryAmountsAlignedWithListCaliber() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L));
CcdiProjectFamilyAssetLiabilityListItemVO listItem = new CcdiProjectFamilyAssetLiabilityListItemVO();
listItem.setStaffIdCard("330102199001011234");
listItem.setTotalIncome(new BigDecimal("180000.00"));
listItem.setTotalAsset(new BigDecimal("520000.00"));
listItem.setTotalDebt(new BigDecimal("260000.00"));
listItem.setComparisonAmount(new BigDecimal("440000.00"));
listItem.setRiskLevelCode("RISK");
listItem.setRiskLevelName("存在风险");
when(specialCheckMapper.selectFamilyAssetLiabilityList(40L)).thenReturn(List.of(listItem));
CcdiProjectFamilyAssetLiabilityDetailVO detailVO = new CcdiProjectFamilyAssetLiabilityDetailVO();
detailVO.setIncomeDetail(new CcdiProjectFamilyIncomeDetailVO());
detailVO.setAssetDetail(new CcdiProjectFamilyAssetDetailVO());
detailVO.setDebtDetail(new CcdiProjectFamilyDebtDetailVO());
detailVO.setSummary(listItem);
when(specialCheckMapper.selectFamilyAssetLiabilityDetail(40L, "330102199001011234")).thenReturn(detailVO);
CcdiProjectFamilyAssetLiabilityListVO listResult = service.getFamilyAssetLiabilityList(buildListQuery(40L));
CcdiProjectFamilyAssetLiabilityDetailVO detailResult =
service.getFamilyAssetLiabilityDetail(buildDetailQuery(40L, "330102199001011234"));
assertEquals(listResult.getRows().getFirst().getTotalIncome(), detailResult.getSummary().getTotalIncome());
assertEquals(listResult.getRows().getFirst().getTotalAsset(), detailResult.getSummary().getTotalAsset());
assertEquals(listResult.getRows().getFirst().getTotalDebt(), detailResult.getSummary().getTotalDebt());
assertEquals(
listResult.getRows().getFirst().getComparisonAmount(),
detailResult.getSummary().getComparisonAmount()
);
assertNotNull(detailResult.getAssetDetail().getItems());
assertNotNull(detailResult.getDebtDetail().getItems());
verify(specialCheckMapper).selectFamilyAssetLiabilityList(40L);
verify(specialCheckMapper).selectFamilyAssetLiabilityDetail(40L, "330102199001011234");
}
@Test
void shouldPreserveMissingInfoFlagsForDetailCards() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L));
CcdiProjectFamilyAssetLiabilityListItemVO summary = new CcdiProjectFamilyAssetLiabilityListItemVO();
summary.setRiskLevelCode("MISSING_INFO");
summary.setRiskLevelName("缺少信息");
CcdiProjectFamilyAssetDetailVO assetDetail = new CcdiProjectFamilyAssetDetailVO();
assetDetail.setMissingSelfAssetInfo(true);
CcdiProjectFamilyDebtDetailVO debtDetail = new CcdiProjectFamilyDebtDetailVO();
debtDetail.setMissingSelfDebtInfo(true);
CcdiProjectFamilyAssetLiabilityDetailVO detailVO = new CcdiProjectFamilyAssetLiabilityDetailVO();
detailVO.setIncomeDetail(new CcdiProjectFamilyIncomeDetailVO());
detailVO.setAssetDetail(assetDetail);
detailVO.setDebtDetail(debtDetail);
detailVO.setSummary(summary);
when(specialCheckMapper.selectFamilyAssetLiabilityDetail(40L, "330102199001011234")).thenReturn(detailVO);
CcdiProjectFamilyAssetLiabilityDetailVO result =
service.getFamilyAssetLiabilityDetail(buildDetailQuery(40L, "330102199001011234"));
assertEquals("MISSING_INFO", result.getSummary().getRiskLevelCode());
assertEquals("缺少信息", result.getSummary().getRiskLevelName());
assertTrue(result.getAssetDetail().getMissingSelfAssetInfo());
assertTrue(result.getDebtDetail().getMissingSelfDebtInfo());
}
private CcdiProject buildProject(Long projectId) {
CcdiProject project = new CcdiProject();
project.setProjectId(projectId);
return project;
}
private CcdiProjectFamilyAssetLiabilityListQueryDTO buildListQuery(Long projectId) {
CcdiProjectFamilyAssetLiabilityListQueryDTO queryDTO = new CcdiProjectFamilyAssetLiabilityListQueryDTO();
queryDTO.setProjectId(projectId);
return queryDTO;
}
private CcdiProjectFamilyAssetLiabilityDetailQueryDTO buildDetailQuery(Long projectId, String staffIdCard) {
CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO = new CcdiProjectFamilyAssetLiabilityDetailQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setStaffIdCard(staffIdCard);
return queryDTO;
}
}

View File

@@ -0,0 +1,292 @@
# 项目列表归档功能设计文档
## 背景
项目管理列表中的“归档”按钮当前仅有前端入口,尚未接通真实后端归档逻辑。
现状包括:
- 项目列表页已在“已完成”项目上展示“归档”按钮
- 前端 API 已预留 `archiveProject(projectId)` 调用
- 项目详情页已经存在“上传数据”“参数配置”“结果总览”“专项排查”“流水明细查询”五个页签
- 后端项目状态常量已定义 `2-已归档`
但当前仍存在两个缺口:
1. 点击“归档”后不会真正更新项目状态
2. 已归档项目进入详情页后,仍可进入“上传数据”和“参数配置”页签
因此需要补齐“列表归档”到“详情只读导航”的完整闭环。
## 目标
- 让项目列表中的“归档”按钮真正触发项目归档
- 归档成功后将项目状态更新为 `2-已归档`
- 已归档项目在详情页中不可点击“上传数据”“参数配置”页签
- 已归档项目仍可查看结果总览、专项排查和流水明细查询
## 非目标
- 不新增“取消归档”或“恢复归档”能力
- 不新增“归档库”页面或独立归档路由
- 不实现“归档时删除项目相关数据”
- 不实现“归档时自动生成 PDF 报告”
- 不改动项目打标、结果计算、文件上传、参数保存的既有业务逻辑
## 方案对比
### 方案一:列表归档后,详情页禁用指定页签
- 做法:归档后将项目状态切换为 `2`,详情页基于项目状态禁用“上传数据”“参数配置”,并对旧链接做路由拦截
- 优点:完全贴合需求,复用现有状态体系,改动范围集中
- 缺点:需要同时覆盖列表入口、详情页导航和 URL 直达场景
### 方案二:只在上传页和参数页内部禁用操作
- 做法:详情页页签仍可点击,但进入页面后所有按钮和输入框禁用
- 优点:实现较快
- 缺点:与“页签本身不可点击”的需求不符,用户感知不一致
### 方案三:新增归档态详情壳子或独立归档路由
- 做法:已归档项目进入不同的详情结构
- 优点:后续扩展空间更大
- 缺点:明显超出当前需求,引入额外维护成本
## 最终方案
采用方案一。
在项目列表页完成真实归档后,后端将项目状态更新为 `2-已归档`,前端详情页根据项目状态禁用“上传数据”“参数配置”两个页签;若用户通过旧链接或手动拼接 URL 访问 `tab=upload``tab=config`,页面自动切换到 `overview`
## 现状分析
### 列表页现状
[`ProjectTable.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue) 已实现以下操作显示规则:
- `status === '1'` 时显示“查看结果”“重新分析”“归档”
- `status === '2'` 时只显示“查看结果”
这意味着列表操作层已经具备归档后的展示约束,本次无需新增额外状态分支,只需将归档动作接入真实后端。
### 归档弹窗现状
[`ArchiveConfirmDialog.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/ArchiveConfirmDialog.vue) 当前仍包含以下超范围内容:
- 删除项目相关数据
- 自动生成项目报告 PDF
- 归档库查看和恢复
这些内容不在本次需求内,且会让确认语义偏离“仅归档状态变更”,需要收敛。
### 后端现状
[`CcdiProjectController.java`](/Users/wkc/Desktop/ccdi/ccdi/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java) 当前没有归档接口。
[`CcdiProjectServiceImpl.java`](/Users/wkc/Desktop/ccdi/ccdi/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java) 已具备:
- 项目状态常量管理
- 项目状态更新能力
- 已归档项目禁止重新进入打标流程的约束
说明后端状态体系已经具备归档语义,本次只需补齐专门的归档入口和归档校验。
### 详情页现状
[`detail.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/detail.vue) 当前会:
- 通过 `route.query.tab` 决定默认页签
- 通过 `getProject(projectId)` 拉取项目详情
- 根据项目状态处理“打标中”轮询
但尚未基于“已归档”状态限制页签可达性,因此需要在导航层和路由初始化层同时补齐控制。
## 详细设计
## 1. 后端接口设计
新增归档接口:
- URL`POST /ccdi/project/{projectId}/archive`
- 入参:路径参数 `projectId`
- 返回:`AjaxResult.success("项目归档成功")`
设计原则:
- 不新增请求体
- 不复用“更新项目”接口承载归档语义
- 归档作为独立动作,单独由项目服务处理
## 2. 后端服务设计
在项目服务中新增 `archiveProject(Long projectId, String operator)` 方法。
执行逻辑:
1. 查询项目
2. 校验项目存在
3. 校验当前状态必须为 `1-已完成`
4. 写入归档状态
5. 更新操作人和更新时间
归档写入规则:
- `status = "2"`
- `isArchived = 1`
- `updateBy` 更新为当前操作人
- `updateTime` 更新为当前时间
### 状态校验规则
只允许“已完成”项目归档。
禁止归档的状态包括:
- `0-进行中`
- `2-已归档`
- `3-打标中`
这样可以保证:
- 与列表页“仅已完成项目展示归档按钮”的规则一致
- 避免通过绕过前端直接调用接口导致非法状态流转
## 3. 列表页交互设计
[`index.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/index.vue) 中的 `handleConfirmArchive` 改为真实调用归档接口。
执行顺序:
1. 用户在列表点击“归档”
2. 打开归档确认弹窗
3. 用户确认后调用 `archiveProject(projectId)`
4. 成功后关闭弹窗并刷新列表
5. 刷新后项目状态显示为“已归档”
6. 列表操作区自然收敛为仅显示“查看结果”
失败时:
- 保持弹窗打开
- 展示后端返回的业务错误或统一失败提示
- 不刷新列表
## 4. 归档确认弹窗设计
[`ArchiveConfirmDialog.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/ArchiveConfirmDialog.vue) 收敛为纯确认弹窗。
保留内容:
- 项目名称确认文案
- “确认后项目状态将变更为已归档”的说明
- 取消 / 确认归档按钮
- 提交中的 loading 状态
移除内容:
- “同时删除项目相关数据”
- “自动生成项目报告 PDF”
- “归档库查看和恢复”
这样可以确保弹窗所表达的业务含义与本次真实实现完全一致。
## 5. 详情页签禁用设计
[`detail.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/detail.vue) 增加“已归档”状态判断。
当项目为已归档时:
- “上传数据”页签禁用
- “参数配置”页签禁用
- “结果总览”页签可点击
- “专项排查”页签可点击
- “流水明细查询”页签可点击
本次控制点放在页签本身,而不是仅在子页面内部禁用操作,以满足“页签本身不可点击”的交互要求。
## 6. 路由初始化与地址直达拦截
为防止用户通过旧链接访问受限页签,需要在 `initActiveTabFromRoute()` 中增加归档态判断。
规则如下:
-`tab``upload``config` 且项目已归档,则目标页签改为 `overview`
- 其他页签保持原逻辑
该规则同时覆盖:
- 刷新页面
- 手动输入 URL
- 从外部链接进入详情页
这样可以保证归档态限制不仅体现在视觉层,也体现在路由可达性层。
## 7. 子组件保护设计
虽然本次核心要求是“页签不可点击”,但仍建议在以下组件中补充归档态保护:
- [`UploadData.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue)
- [`ParamConfig.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue)
保护方式:
- 在现有“打标中禁用”判断基础上,增加“已归档禁用”判断
- 若未来这些组件被其他入口复用,也不会绕过归档限制
这不是新增业务方案,而是对同一条归档约束链路的补强。
## 8. 异常处理设计
### 后端业务异常
后端直接返回明确业务提示,例如:
- 项目不存在
- 仅已完成项目允许归档
- 项目已归档,无需重复操作
前端优先展示后端消息,不自行改写业务文案。
### 前端请求异常
当请求失败但没有明确业务提示时,列表页统一提示:
- `项目归档失败,请稍后重试`
失败时不刷新列表,也不修改当前行本地状态。
## 9. 测试设计
### 后端验证
至少验证以下场景:
1. 已完成项目调用归档接口成功,状态更新为 `2``isArchived` 更新为 `1`
2. 进行中项目调用归档接口被拒绝
3. 打标中项目调用归档接口被拒绝
4. 已归档项目重复归档被拒绝
### 前端验证
至少验证以下场景:
1. 列表点击“归档”后会调用真实接口
2. 归档成功后关闭弹窗并刷新列表
3. 已归档项目列表中不再显示“归档”和“重新分析”
4. 已归档项目详情页中“上传数据”“参数配置”页签不可点击
5. 手动访问 `?tab=upload``?tab=config` 时自动切换到 `overview`
6. 结果总览、专项排查、流水明细查询在归档态下仍可正常进入
## 10. 风险与约束
- 本次不提供“取消归档”,因此归档操作一旦成功,只能按当前产品规则进入只读查看路径。
- 详情页禁用页签依赖项目详情接口返回正确的状态字段;若后端返回口径不一致,需要以前端当前统一使用的 `projectStatus/status` 兼容逻辑处理。
- 当前项目详情在首次进入时先按路由参数设置页签,再异步加载项目详情,因此实现时需要注意在拿到项目状态后再次校正受限页签,避免短暂展示错误页签。
## 验收标准
- 项目列表中“已完成”项目点击“归档”后,确认即可将项目状态改为“已归档”
- 项目归档成功后,列表刷新且该项目只保留“查看结果”操作
- 已归档项目进入详情页后,“上传数据”“参数配置”页签不可点击
- 用户通过 URL 访问受限页签时会自动跳转到“结果总览”
- 已归档项目仍可正常查看“结果总览”“专项排查”“流水明细查询”

View File

@@ -0,0 +1,365 @@
# 项目详情专项排查 Tab 设计文档
## 背景
当前项目详情页已经预留“专项排查”标签页入口:
- 页面入口位于 `ruoyi-ui/src/views/ccdiProject/detail.vue`
- 实际内容组件为 `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- 当前 `SpecialCheck.vue` 仅展示“专项排查功能开发中...”占位文案
同时,需求方已经提供了参考图 `assets/专项核查.png`,希望项目详情中的专项排查页在结构上贴近该参考图。
本次需求经过确认后,边界如下:
- 页面整体结构按参考图设计
- 图谱分析区域不开发图谱卡片能力
- 图谱区域改为轻量空态卡片,并提供外接链接入口
- 顶部“工具排查分析”和底部“扩展查询”本次仅做高拟真静态占位,不接真实接口
- 模块标题、按钮文案、表头文案尽量贴近参考图
## 目标
- 将项目详情页中的“专项排查”从单一空白占位升级为完整的静态专项排查页面
- 保持页面结构、模块命名和视觉层级与参考图一致
- 在不引入伪业务逻辑的前提下,为后续接入真实专项排查能力预留清晰的前端骨架
- 图谱区域只保留外链入口,不在本次范围内实现图谱能力
## 范围
### In Scope
- 重构 `SpecialCheck.vue`,输出完整专项排查页面结构
- 页面按三段式结构组织:
- 工具排查分析
- 图谱分析
- 扩展查询
- 复刻参考图中的主要标题、按钮、筛选区、预警卡、页签区和表格区外观
- 图谱分析区提供轻量空态卡片与外链按钮
- 按职责拆分前端展示子组件,避免 `SpecialCheck.vue` 继续膨胀
### Out of Scope
- 不新增后端接口
- 不接入真实专项排查查询逻辑
- 不实现图谱绘制、图谱节点、图谱筛选、图谱数据接口
- 不实现扩展查询页签切换联动、表格分页、导出、详情弹窗
- 不新增轮询、进度条、消息推送等异步能力
## 现状分析
### 项目详情页现状
`ruoyi-ui/src/views/ccdiProject/detail.vue` 当前已经具备稳定的详情页主导航结构:
- `upload` 对应上传数据
- `config` 对应参数配置
- `overview` 对应结果总览
- `special` 对应专项排查
- `detail` 对应流水明细查询
`special` 页签已经被纳入路由参数和动态组件切换逻辑,因此本次不需要变更项目详情页的导航机制,只需要补齐专项排查页面内容。
### 专项排查页现状
`ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue` 当前仅包含:
- 一个白底容器
- 一个搜索图标
- 一句“专项排查功能开发中...”
这意味着:
- 页面层级无法承接参考图要求
- 与项目详情其他 tab 的完成度不一致
- 后续即使要接真实专项排查,也缺少可直接替换的页面骨架
### 相关页面可复用模式
项目详情下已有两个可借鉴页面模式:
1. `PreliminaryCheck.vue`
- 适合复用其“页面由多个展示区块拼装而成”的组织方式
2. `DetailQuery.vue`
- 适合复用其“筛选区 + 结果区”的排版节奏和局部样式语言
结论:
- 页面骨架与视觉层级可参考参考图
- 组件组织方式与样式语义应延续现有项目详情页实现
## 方案选择
本次采用“高拟真静态专项排查页 + 图谱外链空态卡片”的方案。
### 推荐方案
专项排查页整体按参考图拆分为三个模块:
1. 工具排查分析
2. 图谱分析
3. 扩展查询
其中:
- 工具排查分析:展示静态筛选表单、按钮和红色预警摘要卡
- 图谱分析:展示轻量空态卡片和外链按钮
- 扩展查询:展示静态页签、筛选条和表格列表
这样可以同时满足:
- 页面完成度足够接近参考图
- 图谱能力不被误做成假的节点占位图
- 后续逐块替换真实接口时不需要推倒整个页面
### 不采用的方案
#### 方案一:继续维持单一“开发中”空白页
不采用原因:
- 与“按专项核查图设计”的目标不一致
- 无法承接后续业务评审和视觉确认
- 后续开发仍需重新搭建页面骨架
#### 方案二:把全部模块做成可点击但不接数据的伪交互页
不采用原因:
- 会引入大量没有真实业务含义的交互逻辑
- 容易让页面进入“像能用但其实不能用”的灰区
- 增加前端返工成本,不符合最短路径实现原则
## 详细设计
## 1. 页面结构设计
专项排查页采用纵向三段式布局,自上而下依次为:
### 1.1 工具排查分析
该区块位于页面顶部,承担“先看筛选条件和预警概览”的作用,包含:
- 模块标题:`工具排查分析`
- 一行或两行筛选输入区:
- 身份证号
- 开始日期
- 结束日期
- 操作按钮:
- `查询`
- `重置`
- 红色预警摘要卡:
- 预警标题
- 基本信息
- 本人分析
- 关联账户分析
- 风险提示摘要
本区块的所有内容均为静态展示:
- 不发起接口请求
- 不根据输入刷新卡片内容
- 输入控件主要用于还原参考图结构和信息密度
### 1.2 图谱分析
该区块位于页面中部,承担“承接未来图谱能力”的作用,包含:
- 模块标题:`图谱分析`
- 顶部页签外观:
- 关系人图谱
- 资金流图谱
- 家庭资产图谱
- 主体内容区:轻量空态卡片
- 空态卡片中的外链入口按钮
本区块明确不实现任何图谱占位图,不使用假节点或假连线替代真实能力。
空态卡片表达重点为:
- 当前图谱能力需跳转外部系统查看
- 若未配置外链地址,则展示禁用态或提示文案
### 1.3 扩展查询
该区块位于页面底部,承担“承接未来明细查询能力”的作用,包含:
- 模块标题:`扩展查询`
- 页签区:
- 异常查询
- 人员轨迹查询
- 招聘查询
- 筛选条:
- 时间范围
- 关键词输入
- 查询按钮
- 重置按钮
- 静态表格区:
- 表头
- 1~2 行静态示例数据
本区块本次不做:
- 页签切换
- 真实过滤
- 数据分页
- 详情弹窗
## 2. 组件拆分设计
为避免 `SpecialCheck.vue` 继续演变为大而全文件,建议拆分为以下职责边界:
### 2.1 `SpecialCheck.vue`
职责:
- 作为专项排查页主容器
- 接收 `projectId``projectInfo`
- 组织三个展示区块的排列顺序
- 维护图谱外链地址等轻量常量输入
### 2.2 工具排查分析子组件
职责:
- 渲染顶部筛选区
- 渲染查询/重置按钮
- 渲染红色预警摘要卡
约束:
- 不包含真实查询逻辑
- 不向外发起接口请求
### 2.3 图谱分析子组件
职责:
- 渲染图谱分析标题和页签外观
- 渲染空态卡片
- 承载外链按钮点击逻辑
约束:
- 不实现图谱画布
- 不引入第三方图谱库
### 2.4 扩展查询子组件
职责:
- 渲染底部页签区、筛选条和静态表格
- 输出接近参考图的查询区块外观
约束:
- 不发起真实查询
- 不弹出详情
## 3. 交互设计
本次只保留两类交互:
### 3.1 视觉交互
- 输入框聚焦样式
- 按钮 hover 样式
- 页签高亮样式
- 卡片悬停或层级区分样式
这些交互只服务于页面完成度,不驱动数据变化。
### 3.2 图谱外链交互
图谱分析区的外链按钮是本页唯一真实业务交互。
建议行为:
- 已配置地址时:
- 点击后新窗口打开外链
- 未配置地址时:
- 按钮禁用或提示“暂未配置图谱地址”
除该按钮外,其余“查询”“重置”“页签”等仅保留视觉层面的静态呈现。
## 4. 数据策略设计
本次页面采用前端静态 mock 数据策略。
### 4.1 顶部摘要数据
使用组件内常量或独立静态数据文件维护:
- 预警标题
- 身份证号
- 收入、支出、异常账户等描述
- 风险提示文案
### 4.2 扩展查询表格数据
使用固定数组维护 1~2 行示例数据,用于撑开表格结构与列宽。
### 4.3 图谱外链地址
可先在前端组件中定义常量,后续若需要配置化,再收口到独立常量文件。
## 5. 视觉与样式设计
样式上遵循“结构靠近参考图,视觉语言跟随现有项目详情页”的原则。
### 5.1 需要靠近参考图的部分
- 模块标题命名
- 页面三段式层级
- 红色预警摘要卡的视觉重心
- 查询区和表格区的分块关系
### 5.2 需要延续现有项目风格的部分
- 页面白底卡片容器
- 常规按钮风格
- 输入框、页签、表格的基础样式
- 留白、圆角、边框、阴影强度
这样可以避免专项排查页看起来像“另一套系统”。
## 6. 文件改动设计
预计涉及的前端文件边界如下:
- 继续使用:`ruoyi-ui/src/views/ccdiProject/detail.vue`
- 重点改造:`ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- 新增专项排查子组件:位于 `ruoyi-ui/src/views/ccdiProject/components/detail/`
后端文件本次不改。
## 7. 测试设计
### 7.1 前端验证
至少验证以下内容:
- 项目详情页切换到 `special` 页签后能正确显示专项排查页面
- 页面三大模块均正常渲染
- 图谱分析区显示为空态卡片而非图谱占位图
- 图谱外链按钮在有地址和无地址两种情况下表现正确
- 页面在常见桌面分辨率下不出现明显布局错乱
### 7.2 后端验证
本次无需新增后端测试,只需确认:
- 不存在为专项排查页新增的后端接口依赖
- 现有项目详情接口不受本次前端改动影响
## 8. 实施结论
本次最短路径实现为:
- 只开发前端静态专项排查页面
- 仅保留图谱外链作为真实交互
- 不开发图谱能力
- 不开发真实查询能力
- 不新增后端代码
该方案既满足参考图落位要求,也为后续真实专项排查能力接入保留了稳定骨架。

View File

@@ -0,0 +1,473 @@
# 专项核查页员工家庭资产负债专项核查设计文档
## 1. 背景
当前项目详情页已具备“专项排查”页签,但实际内容组件 `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue` 仍是占位页。
同时,当前仓库已经具备本次专项核查所需的核心数据基础:
- 项目内员工范围可沿用结果总览当前“项目内已入库流水命中的员工”口径
- 员工年收入可取 `ccdi_base_staff.annual_income`
- 配偶年收入可取 `ccdi_staff_fmy_relation.annual_income`
- 家庭资产可取 `ccdi_asset_info`
- 征信负债可取 `ccdi_debts_info`
本次需求希望在专项核查页新增一个真实业务卡片,对项目内员工的家庭收入、家庭资产、家庭负债进行聚合核查,并输出风险情况。
## 2. 已确认需求
- 范围仅覆盖项目内员工
- 家庭总年收入 = 员工本人年收入 + 员工配偶年收入(如有)
- 家庭总资产 = 员工与配偶关联的资产总和
- 家庭总负债 = 来源于员工与配偶征信中的贷款与负债总和
- 风险判断基于“家庭总年收入 + 家庭总负债”与“家庭总资产”的倍数关系
- 在专项核查页面新增卡片,标题固定为“员工家庭资产负债专项核查”
- 卡片内展示项目内员工核查列表
- 列表展示每个员工家庭的总收入、总资产、总负债和风险情况
- 点击详情后,在当前卡片内展开所有数据细项
- 展示风格需要与结果总览其他组件统一
## 3. 本次确认口径
### 3.1 员工范围
- 沿用结果总览口径,只展示当前项目中“已入库流水命中且可匹配员工主数据”的员工
- 不额外扩大到项目配置中的全部目标员工
### 3.2 缺失值处理
- 若员工存在配偶关系,但配偶收入、资产或征信负债未维护完整,则缺失值按 `0` 参与计算
- 不额外显示“待补数据”
- 不因缺失值阻断风险等级产出
### 3.3 资产口径
- 家庭总资产统一按 `ccdi_asset_info.current_value` 汇总
- 不使用 `original_value`
### 3.4 负债口径
- 家庭总负债统一按 `ccdi_debts_info.principal_balance` 汇总
- 不使用 `debt_total_amount`
### 3.5 配偶识别口径
- 配偶统一按 `ccdi_staff_fmy_relation.relation_type = '配偶'` 识别
### 3.6 风险等级边界
- `家庭总年收入 + 家庭总负债 <= 家庭总资产 * 1.5`:正常
- `家庭总年收入 + 家庭总负债 > 家庭总资产 * 1.5 且 <= 家庭总资产 * 3`:存在风险
- `家庭总年收入 + 家庭总负债 > 家庭总资产 * 3`:高风险
采用以上边界是为了避免 `1.5` 倍和 `3` 倍临界值出现重复命中或漏判。
## 4. 目标
- 将专项排查页从占位页升级为包含真实业务核查能力的页面
- 以最短路径新增“员工家庭资产负债专项核查”卡片
- 输出项目内员工家庭资产、收入、负债对比结果
- 在不引入新页面、抽屉、弹窗的前提下,支持明细下钻
- 保持视觉语言、卡片结构、表格节奏与结果总览一致
## 5. 非目标
- 不新增专项核查独立路由
- 不新增弹窗详情页、抽屉详情页
- 不将本次专项核查并入结果总览员工结果表
- 不扩展除配偶外的其他家庭成员收入、资产、负债口径
- 不补充兜底、降级或额外提示型方案
## 6. 方案对比
### 6.1 方案 A单卡列表 + 行内展开
- 在专项排查页新增一个独立白色卡片
- 卡片主区域直接展示项目内员工列表
- 点击某一行后,在当前行下方展开收入、资产、负债细项
优点:
- 与结果总览的卡片 + 表格阅读节奏最一致
- 前端实现边界最清晰
- 用户先看全量结果,再对单个员工下钻,路径最短
缺点:
- 专项层级的整体统计感相对弱一些
### 6.2 方案 B摘要统计 + 列表
- 卡片顶部先展示专项统计摘要
- 下方再展示员工列表与展开详情
优点:
- 更像专项看板
- 能先看到正常、风险、高风险分布
缺点:
- 会额外引入一层聚合展示
- 会让专项排查页与结果总览的边界变模糊
### 6.3 方案 C风险分层看板
- 左侧展示风险分层统计
- 右侧展示员工列表与详情
优点:
- 视觉冲击更强
缺点:
- 会把本次需求扩成新的 dashboard
- 与“新增统一风格卡片”的最短路径目标不一致
## 7. 最终方案
采用方案 A单卡列表 + 行内展开。
选择原因:
- 满足“专项排查页新增真实业务卡片”的目标
- 风格可直接对齐结果总览现有卡片体系
- 明细展开符合“点开详情展示所有数据细项”的要求
- 不需要新增平行页面或重型交互容器
## 8. 页面设计
## 8.1 页面整体结构
专项排查页继续作为项目详情页中的内容区,不改造顶部导航和页签切换方式。
本次页面结构调整为:
1. 保留专项排查主容器 `SpecialCheck.vue`
2. 在主容器中新增“员工家庭资产负债专项核查”主业务卡片
3. 页面其余静态骨架仍可保留,但本卡片作为当前页核心区块
## 8.2 卡片结构
卡片标题固定为:
- `员工家庭资产负债专项核查`
卡片副标题建议为:
- `展示项目内员工家庭收入、资产、负债对比结果`
卡片主体由两部分组成:
1. 员工家庭核查列表
2. 行内展开详情区
## 8.3 列表字段
列表建议固定展示以下列:
- 序号
- 员工姓名
- 身份证号
- 所属部门
- 家庭总年收入
- 家庭总资产
- 家庭总负债
- 风险情况
- 操作
其中:
- 风险情况使用与结果总览一致的标签式展示
- 操作列文案统一为 `查看详情`
## 8.4 行内展开详情
点击“查看详情”后,在当前行下方展开,不打开新页面、不弹窗、不抽屉。
展开区固定分为 3 个模块:
### 8.4.1 收入明细
- 本人年收入
- 配偶年收入
- 家庭总年收入
### 8.4.2 资产明细
- 本人资产合计
- 配偶资产合计
- 资产明细列表
- 家庭总资产
资产明细列表建议包含:
- 资产名称
- 资产大类
- 资产小类
- 持有人
- 当前估值
- 估值日期
### 8.4.3 负债明细
- 本人负债合计
- 配偶负债合计
- 负债明细列表
- 家庭总负债
负债明细列表建议包含:
- 负债名称
- 负债大类
- 负债小类
- 债权人类型
- 归属人
- 本金余额
- 查询日期
## 8.5 页面状态
### 加载态
- 卡片使用结果总览当前一致的白卡骨架态
- 列表区采用表格骨架或 `el-skeleton`
### 空态
- 当项目下无可核查员工时,卡片主体展示空态
- 空态文案建议为 `暂无员工家庭资产负债核查数据`
### 异常态
- 接口异常时,不跳出专项排查页
- 保持当前页卡片容器与空态风格一致
## 9. 前端组件设计
## 9.1 主容器
文件:
- `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
职责:
- 作为专项排查页主容器
- 接收 `projectId``projectInfo`
- 组织本次专项核查卡片渲染
- 维护页面加载态、空态
## 9.2 专项核查区块组件
建议新增一个独立区块组件,例如:
- `FamilyAssetLiabilitySection.vue`
职责:
- 负责请求列表数据
- 负责列表渲染
- 负责行展开开关
- 负责触发展开详情查询
## 9.3 展开详情子组件
建议新增轻量明细组件,例如:
- `FamilyAssetLiabilityDetail.vue`
职责:
- 展示收入、资产、负债三组细项
- 负责金额格式化与分组展示
说明:
- 明细组件仅服务本卡片,不抽象为全局通用组件
## 9.4 前端展示风格
实现时需遵循以下方向:
- 保持与结果总览现有 `section-card / block-header / el-table` 风格统一
- 不引入另一套专项核查视觉体系
- 页面细节可结合现有前端风格做适度精修
- 保持桌面端阅读密度稳定,移动端至少不出现明显布局错乱
## 10. 后端设计
## 10.1 总体原则
本次专项核查新增专项查询接口,不复用结果总览员工结果表。
原因如下:
- 结果总览员工结果表当前承载的是风险模型命中快照
- 本次专项核查依赖家庭收入、家庭资产、征信负债等明细聚合
- 若强行复用结果总览结果表,会把结果总览链路与专项核查链路混成一个表用途
因此,本次采用“专项核查专用查询链路”。
## 10.2 查询范围
查询员工集合沿用当前项目结果总览口径:
- 从项目内已入库流水中提取身份证号
- 与员工主数据匹配
- 以匹配成功的员工作为本次专项核查名单
## 10.3 聚合逻辑
对每个员工按以下步骤聚合:
1. 取员工本人年收入
2. 取员工配偶年收入
3. 取员工本人及配偶名下资产 `current_value`
4. 取员工本人及配偶征信 `principal_balance`
5. 计算家庭总年收入、家庭总资产、家庭总负债
6. 计算 `家庭总年收入 + 家庭总负债`
7. 输出风险等级
## 10.4 配偶与家庭数据关联
### 收入
- 本人收入:`ccdi_base_staff.annual_income`
- 配偶收入:`ccdi_staff_fmy_relation``relation_type = '配偶'` 对应记录的 `annual_income`
### 资产
- 以员工身份证号为 `family_id`
- 资产持有人范围限定为员工本人身份证号 + 配偶身份证号
- 资产金额按 `current_value` 汇总
### 负债
- 负债归属人范围限定为员工本人身份证号 + 配偶身份证号
- 负债金额按 `principal_balance` 汇总
## 10.5 明细返回要求
详情接口必须返回参与计算的原始明细,至少包括:
- 本人收入
- 配偶收入
- 本人资产汇总
- 配偶资产汇总
- 资产明细列表
- 本人负债汇总
- 配偶负债汇总
- 负债明细列表
这样前端展开后可直接展示风险判定依据,不需要再次拼装额外逻辑。
## 11. 接口设计
建议新增专项核查专用控制器或在专项查询控制器下新增以下两个只读接口。
### 11.1 列表接口
- 路径建议:`GET /ccdi/project/special-check/family-asset-liability/list`
- 入参:`projectId`
- 出参:员工家庭核查列表
列表项建议字段:
- `staffIdCard`
- `staffCode`
- `staffName`
- `deptName`
- `totalIncome`
- `totalAsset`
- `totalDebt`
- `comparisonAmount`
- `riskLevelCode`
- `riskLevelName`
### 11.2 详情接口
- 路径建议:`GET /ccdi/project/special-check/family-asset-liability/detail`
- 入参:`projectId``staffIdCard`
- 出参:单个员工家庭明细
详情结构建议分为:
- `incomeDetail`
- `assetDetail`
- `debtDetail`
- `summary`
## 12. 代码边界建议
### 后端
建议涉及:
- `controller`
- `service`
- `mapper`
- `dto`
- `vo`
不建议涉及:
- 结果总览结果表结构
- 银行流水打标链路
- 现有结果总览接口
### 前端
建议涉及:
- `SpecialCheck.vue`
- 专项核查 API 文件
- 专项核查卡片组件
- 专项核查详情组件
不建议涉及:
- `PreliminaryCheck.vue`
- 结果总览已有接口文件
- 项目详情页顶部导航逻辑
## 13. 测试设计
### 13.1 后端验证
至少验证以下内容:
- 项目范围仅包含当前项目已入库流水命中的员工
- 存在配偶但数据缺失时,缺失值按 `0` 参与计算
- 家庭总资产按 `current_value` 汇总
- 家庭总负债按 `principal_balance` 汇总
- 风险边界值按已确认规则判定
- 列表接口与详情接口在无数据场景下返回空结果
### 13.2 前端验证
至少验证以下内容:
- 项目详情切换到专项排查页后能正常看到专项核查卡片
- 卡片风格与结果总览保持一致
- 列表字段与需求一致
- 点击“查看详情”在当前行内展开
- 展开后能看到收入、资产、负债全部细项
- 空态、加载态、异常态表现稳定
## 14. 实施结论
本次采用“专项排查页新增统一风格业务卡片 + 专项后端查询接口 + 行内展开详情”的最短路径方案:
1. 不复用结果总览员工结果表
2. 不引入新页面、弹窗或抽屉
3. 不扩大到配偶之外的家庭成员
4. 直接在专项排查页承接第一块真实业务能力
## 15. 后续动作
待用户审阅本设计文档后,按仓库规范输出两份实施计划:
- `docs/plans/backend/` 下的后端实施计划
- `docs/plans/frontend/` 下的前端实施计划

View File

@@ -0,0 +1,121 @@
# Project Detail Special Check Tab Backend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 明确项目详情“专项排查”标签页本次无需新增后端接口或服务逻辑,确保前端静态页面开发不会错误依赖后端改造。
**Architecture:** 本次后端实施遵循最短路径原则,不引入任何新的专项排查接口、不扩展项目详情聚合接口、不补做图谱中台代理。实施工作仅包含边界核验、现有接口影响确认和文档沉淀,确保前端可独立完成静态页面落位。
**Tech Stack:** Java 21, Spring Boot 3, Maven, Swagger/OpenAPI
---
### Task 1: 核验本次专项排查页不依赖新增后端接口
**Files:**
- Reference: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java`
- Reference: `ruoyi-ui/src/views/ccdiProject/detail.vue`
- Modify: `docs/plans/backend/2026-03-24-project-detail-special-check-tab-backend-implementation.md`
- [ ] **Step 1: Write the backend boundary checklist**
先在实施计划中锁定以下边界:
- 专项排查页本次是前端静态页面
- 图谱分析只提供外链入口,不依赖后端图谱接口
- 扩展查询本次不接真实数据接口
- 项目详情现有 `getProject` 接口继续只服务页头和基础项目信息
- [ ] **Step 2: Verify current repository does not require a new API**
Run:
```bash
rg -n "getProject\\(|/ccdi/project/|专项排查|special" ruoyi-ui/src/views ccdi-project/src/main/java -S
```
Expected:
- 能确认专项排查页当前没有真实接口接线
- 能确认项目详情已有接口足够支撑页面基础上下文
- [ ] **Step 3: Keep implementation minimal**
若仓库核验结果与设计一致,则明确记录:
- 本次后端不新增 Controller
- 本次后端不新增 Service
- 本次后端不新增 DTO/VO
- 本次后端不新增 SQL
- [ ] **Step 4: Commit**
```bash
git add docs/plans/backend/2026-03-24-project-detail-special-check-tab-backend-implementation.md
git commit -m "新增专项排查后端实施计划"
```
### Task 2: 验证项目详情既有接口不会被本次前端页面改造误伤
**Files:**
- Reference: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java`
- Reference: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java`
- Reference: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
- [ ] **Step 1: Verify the project detail contract**
Run:
```bash
rg -n "查询项目详情|getProject\\(|select.*project" ccdi-project/src/main/java/com/ruoyi/ccdi/project -S
```
Expected:
- 能定位到项目详情控制器与服务实现
- 能确认本次前端专项排查页无需扩充该接口返回字段
- [ ] **Step 2: Record the verification conclusion**
将核验结论写入实施计划:
- 当前项目详情接口已足够支撑专项排查页获取 `projectId` 与基础项目信息
- 专项排查静态页面无需新增后端字段
- 图谱外链地址本次由前端常量承载,不由后端下发
- [ ] **Step 3: Commit**
```bash
git status --short
```
Expected:
- 若只有计划文档变更,则无需新增代码类提交
### Task 3: 为后续真实专项排查能力预留后端边界说明
**Files:**
- Modify: `docs/plans/backend/2026-03-24-project-detail-special-check-tab-backend-implementation.md`
- [ ] **Step 1: Document future backend entry points**
在计划中补充说明:若后续专项排查需要真实化,应优先按能力拆分,而不是一次性扩展项目详情接口。后续候选能力包括:
- 专项排查筛选查询接口
- 图谱地址配置接口或配置项下发
- 扩展查询分页接口
- [ ] **Step 2: Keep the current scope explicit**
明确写入当前结论:
- 上述能力全部不在本次实施范围内
- 本次后端工作到“边界确认”即结束
- [ ] **Step 3: Commit**
```bash
git add docs/plans/backend/2026-03-24-project-detail-special-check-tab-backend-implementation.md
git commit -m "补充专项排查后端边界说明"
```

View File

@@ -0,0 +1,309 @@
# Special Check Family Asset Liability Backend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为专项排查页新增“员工家庭资产负债专项核查”后端查询能力,支持项目内员工列表出数与单个员工家庭收入、资产、负债明细展开。
**Architecture:** 采用专项核查专用查询链路,不复用结果总览员工结果表。新增 `CcdiProjectSpecialCheckController + Service + Mapper`,按项目内已入库流水命中的员工范围聚合本人与配偶的收入、资产、征信本金余额,并拆成列表接口与详情接口两条只读查询。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis XML, Maven, JUnit 5
---
### Task 1: 定义专项核查接口契约、DTO 与 VO
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckController.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectSpecialCheckService.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectFamilyAssetLiabilityListQueryDTO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectFamilyAssetLiabilityDetailQueryDTO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityListItemVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityListVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyIncomeDetailVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetItemVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetDetailVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyDebtItemVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyDebtDetailVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityDetailVO.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckControllerContractTest.java`
- [ ] **Step 1: Write the failing test**
为控制器契约补静态/反射测试,锁定以下路径和方法:
- `GET /ccdi/project/special-check/family-asset-liability/list`
- `GET /ccdi/project/special-check/family-asset-liability/detail`
并锁定 DTO / VO 基本字段:
- 列表入参:`projectId`
- 详情入参:`projectId``staffIdCard`
- 列表项:`staffIdCard``staffCode``staffName``deptName``totalIncome``totalAsset``totalDebt``comparisonAmount``riskLevelCode``riskLevelName`
- 详情:`incomeDetail``assetDetail``debtDetail``summary`
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckControllerContractTest
```
Expected:
- `FAIL`
- 原因是控制器、DTO、VO 尚未创建
- [ ] **Step 3: Write minimal implementation**
补齐控制器、服务接口、DTO 与 VO。
控制器要求:
- 继续使用 `@Tag``@Operation`
- 权限沿用 `@PreAuthorize("@ss.hasPermi('ccdi:project:query')")`
- 返回统一使用 `AjaxResult.success(...)`
DTO 要求:
- 使用独立 DTO不直接裸接 `Long`
- `projectId``staffIdCard` 做基础非空约束
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckControllerContractTest
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckController.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectSpecialCheckService.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectFamilyAssetLiabilityListQueryDTO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectFamilyAssetLiabilityDetailQueryDTO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityListItemVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityListVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyIncomeDetailVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetItemVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetDetailVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyDebtItemVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyDebtDetailVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityDetailVO.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckControllerContractTest.java
git commit -m "定义专项核查家庭资产负债接口契约"
```
### Task 2: 实现项目员工范围与列表聚合 SQL
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapper.java`
- Create: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperListSqlTest.java`
- [ ] **Step 1: Write the failing test**
新增 Mapper XML 测试,锁定以下核心 SQL 口径:
- 项目员工范围沿用“项目内已入库流水命中且能匹配员工主数据”的口径
- 配偶通过 `relation_type = '配偶'` 识别
- 收入使用 `annual_income`
- 资产使用 `current_value`
- 负债使用 `principal_balance`
- 风险等级边界为 `<= 1.5``> 1.5 and <= 3``> 3`
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckMapperListSqlTest
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
`CcdiProjectSpecialCheckMapper.xml` 中实现列表查询。
实现要求:
- 先抽取项目员工基础集合,避免明细聚合时丢失“无资产/无负债/无配偶”的员工
- 家庭总年收入 = 本人年收入 + 配偶年收入,均需 `COALESCE(..., 0)`
- 家庭总资产需限定持有人为“员工本人 + 配偶”
- 家庭总负债需限定归属人为“员工本人 + 配偶”
- 输出 `comparisonAmount = totalIncome + totalDebt`
- 风险等级同时输出 `riskLevelCode``riskLevelName`
- 排序建议先按风险等级倒序,再按 `comparisonAmount desc`,最后按 `staff_name asc`
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckMapperListSqlTest
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperListSqlTest.java
git commit -m "补充专项核查家庭资产负债列表查询"
```
### Task 3: 实现详情聚合 SQL 与明细结构
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapper.java`
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperDetailSqlTest.java`
- [ ] **Step 1: Write the failing test**
新增详情查询 XML 测试,锁定以下内容:
- 详情必须返回收入、资产、负债三组结构
- 资产明细包含:资产名称、大类、小类、持有人、当前估值、估值日期
- 负债明细包含:负债名称、大类、小类、债权人类型、归属人、本金余额、查询日期
- 本人与配偶小计必须拆开
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckMapperDetailSqlTest
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
在 Mapper 中新增详情查询与结果映射。
实现要求:
- 详情接口按 `projectId + staffIdCard` 查询
- 先校验该员工属于当前项目员工范围,再返回详情
- 资产、负债列表都要显式带出“归属人”名称或证件号映射
- 详情返回值中保留 `summary`,避免前端展开区自行重复计算
- 对空列表返回空数组,不返回 `null`
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckMapperDetailSqlTest
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperDetailSqlTest.java
git commit -m "补充专项核查家庭资产负债详情查询"
```
### Task 4: 完成服务组装与控制器返回
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectSpecialCheckServiceImpl.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectSpecialCheckServiceImplTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckControllerTest.java`
- [ ] **Step 1: Write the failing test**
补服务层与控制器测试,覆盖:
- 项目不存在时返回统一异常
- 列表接口返回空列表而不是 `null`
- 详情接口在员工不属于当前项目时返回明确错误
- 详情 `summary` 中金额与列表同口径
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckServiceImplTest,CcdiProjectSpecialCheckControllerTest
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
在服务层:
- 校验项目存在性
- 调用 Mapper 查询列表与详情
- 空数据场景统一封装为空列表或空明细结构
- 不在服务层新增与本次需求无关的缓存、异步、预计算逻辑
在控制器层:
- 直接接收 DTO
- 维持 GET 查询模式
- 返回 `AjaxResult.success(...)`
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckServiceImplTest,CcdiProjectSpecialCheckControllerTest
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectSpecialCheckServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectSpecialCheckServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckControllerTest.java
git commit -m "完成专项核查家庭资产负债服务组装"
```
### Task 5: 写实施记录与验证记录
**Files:**
- Create: `docs/reports/implementation/2026-03-24-special-check-family-asset-liability-backend-record.md`
- Create: `docs/tests/records/2026-03-24-special-check-family-asset-liability-backend-verification.md`
- [ ] **Step 1: Write implementation record**
`docs/reports/implementation/2026-03-24-special-check-family-asset-liability-backend-record.md` 中记录:
- 本次新增的接口、Mapper、DTO、VO、测试文件
- 项目员工范围口径
- 收入、资产、负债、风险等级的计算口径
- 未扩展到其他家庭成员的范围说明
- [ ] **Step 2: Run targeted verification and record output**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckControllerContractTest,CcdiProjectSpecialCheckMapperListSqlTest,CcdiProjectSpecialCheckMapperDetailSqlTest,CcdiProjectSpecialCheckServiceImplTest,CcdiProjectSpecialCheckControllerTest
```
Expected:
- `PASS`
将实际执行命令、结果和结论记录到 `docs/tests/records/2026-03-24-special-check-family-asset-liability-backend-verification.md`
- [ ] **Step 3: Commit**
```bash
git add docs/reports/implementation/2026-03-24-special-check-family-asset-liability-backend-record.md docs/tests/records/2026-03-24-special-check-family-asset-liability-backend-verification.md
git commit -m "补充专项核查家庭资产负债后端记录"
```

View File

@@ -0,0 +1,357 @@
# Project Detail Special Check Tab Frontend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将项目详情页中的“专项排查”标签页从单一开发中占位升级为贴近参考图的静态专项排查页面,并在图谱分析区提供外链入口。
**Architecture:** 保持 `ruoyi-ui/src/views/ccdiProject/detail.vue` 的现有标签切换机制不变,只改造 `SpecialCheck.vue` 及其局部子组件。页面拆为“工具排查分析”“图谱分析”“扩展查询”三个纯展示区块,其中图谱区只保留空态卡片和外链按钮,其他区域全部使用前端静态数据渲染,不接真实接口。
**Tech Stack:** Vue 2, Element UI, SCSS, Node, npm
---
### Task 1: 先锁定专项排查页的页面骨架与组件边界
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckAnalysisPanel.vue`
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckGraphPanel.vue`
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckQueryPanel.vue`
- Test: `ruoyi-ui/tests/unit/project-detail-special-check-structure.test.js`
- [ ] **Step 1: Write the failing test**
新增结构型测试,至少锁定以下约束:
```javascript
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const pagePath = path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/SpecialCheck.vue");
const source = fs.readFileSync(pagePath, "utf8");
assert(source.includes("SpecialCheckAnalysisPanel"), "应拆分工具排查分析子组件");
assert(source.includes("SpecialCheckGraphPanel"), "应拆分图谱分析子组件");
assert(source.includes("SpecialCheckQueryPanel"), "应拆分扩展查询子组件");
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/project-detail-special-check-structure.test.js
```
Expected:
- `FAIL`
- 原因是当前 `SpecialCheck.vue` 仍然只有单一占位内容
- [ ] **Step 3: Write minimal implementation**
`SpecialCheck.vue` 改造成页面容器,只负责:
- 接收 `projectId``projectInfo`
- 组装三个展示区块
- 透传轻量静态数据或图谱外链地址
新增三个子组件:
- `SpecialCheckAnalysisPanel.vue`
- `SpecialCheckGraphPanel.vue`
- `SpecialCheckQueryPanel.vue`
约束:
- 不在主组件中堆叠大段模板
- 不新增接口调用
- 不在本任务引入真实查询逻辑
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/project-detail-special-check-structure.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckAnalysisPanel.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckGraphPanel.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckQueryPanel.vue ruoyi-ui/tests/unit/project-detail-special-check-structure.test.js
git commit -m "拆分项目详情专项排查页面骨架"
```
### Task 2: 实现工具排查分析静态展示区
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckAnalysisPanel.vue`
- Test: `ruoyi-ui/tests/unit/project-detail-special-check-analysis.test.js`
- [ ] **Step 1: Write the failing test**
新增工具排查分析展示测试,锁定以下内容:
```javascript
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const filePath = path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/SpecialCheckAnalysisPanel.vue");
const source = fs.readFileSync(filePath, "utf8");
assert(source.includes("工具排查分析"), "应展示工具排查分析标题");
assert(source.includes("查询"), "应展示查询按钮");
assert(source.includes("重置"), "应展示重置按钮");
assert(source.includes("身份证号"), "应展示身份证号输入项");
assert(source.includes("开始日期"), "应展示开始日期输入项");
assert(source.includes("结束日期"), "应展示结束日期输入项");
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/project-detail-special-check-analysis.test.js
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
`SpecialCheckAnalysisPanel.vue` 中:
- 使用 Element UI 表单组件渲染静态筛选区
- 使用参考图文案渲染 `查询``重置`
- 用静态数据渲染预警摘要卡
- 不给查询按钮绑定真实数据请求
实现约束:
- 输入项只服务于页面还原,不驱动状态切换
- 预警摘要卡视觉上要成为页面主焦点
- 样式延续项目详情页现有白底卡片风格
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/project-detail-special-check-analysis.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckAnalysisPanel.vue ruoyi-ui/tests/unit/project-detail-special-check-analysis.test.js
git commit -m "新增专项排查工具排查分析静态区"
```
### Task 3: 实现图谱分析空态卡片与外链入口
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckGraphPanel.vue`
- Test: `ruoyi-ui/tests/unit/project-detail-special-check-graph.test.js`
- [ ] **Step 1: Write the failing test**
新增图谱分析测试,至少锁定以下内容:
```javascript
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const filePath = path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/SpecialCheckGraphPanel.vue");
const source = fs.readFileSync(filePath, "utf8");
assert(source.includes("图谱分析"), "应展示图谱分析标题");
assert(source.includes("关系人图谱"), "应展示图谱页签文案");
assert(source.includes("资金流图谱"), "应展示图谱页签文案");
assert(source.includes("家庭资产图谱"), "应展示图谱页签文案");
assert(source.includes("查看图谱"), "应提供图谱外链按钮");
assert(!source.includes("echarts"), "本次不应引入图谱或图表依赖");
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/project-detail-special-check-graph.test.js
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
`SpecialCheckGraphPanel.vue` 中:
- 渲染图谱分析标题
- 渲染静态页签外观
- 渲染空态卡片和说明文案
- 提供“查看图谱”按钮
行为约束:
- 有图谱地址时,点击新窗口打开链接
- 无图谱地址时,按钮禁用或提示未配置
- 不画任何假图谱节点或连线
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/project-detail-special-check-graph.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckGraphPanel.vue ruoyi-ui/tests/unit/project-detail-special-check-graph.test.js
git commit -m "新增专项排查图谱分析外链空态卡片"
```
### Task 4: 实现扩展查询静态展示区
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckQueryPanel.vue`
- Test: `ruoyi-ui/tests/unit/project-detail-special-check-query.test.js`
- [ ] **Step 1: Write the failing test**
新增扩展查询结构测试,至少锁定以下内容:
```javascript
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const filePath = path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/SpecialCheckQueryPanel.vue");
const source = fs.readFileSync(filePath, "utf8");
assert(source.includes("扩展查询"), "应展示扩展查询标题");
assert(source.includes("异常查询"), "应展示异常查询页签");
assert(source.includes("人员轨迹查询"), "应展示人员轨迹查询页签");
assert(source.includes("招聘查询"), "应展示招聘查询页签");
assert(source.includes("查询"), "应展示查询按钮");
assert(source.includes("重置"), "应展示重置按钮");
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/project-detail-special-check-query.test.js
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
`SpecialCheckQueryPanel.vue` 中:
- 渲染扩展查询标题
- 渲染静态页签区
- 渲染筛选条
- 渲染静态表格头和示例行
约束:
- 不实现页签切换逻辑
- 不发起查询请求
- 不新增导出和详情弹窗
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/project-detail-special-check-query.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckQueryPanel.vue ruoyi-ui/tests/unit/project-detail-special-check-query.test.js
git commit -m "新增专项排查扩展查询静态区"
```
### Task 5: 联调样式并完成页面验收
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckAnalysisPanel.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckGraphPanel.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckQueryPanel.vue`
- Test: `ruoyi-ui/src/views/ccdiProject/detail.vue`
- [ ] **Step 1: Run local page verification**
Run:
```bash
cd ruoyi-ui
npm run dev
```
打开项目详情页并切换到 `special` 标签,人工核对:
- 顶部分析区、图谱区、扩展查询区顺序正确
- 预警摘要卡为视觉焦点
- 图谱区为空态卡片而不是假图谱
- 页面宽度变化时没有明显断裂
- [ ] **Step 2: Fix style issues with minimal changes**
若发现问题,只做最小化样式修正:
- 调整模块间距
- 调整卡片边框与留白
- 调整标题、页签、表格头视觉层级
- [ ] **Step 3: Stop the dev server after verification**
完成验证后停止 `npm run dev` 启动的前端进程,避免端口残留。
- [ ] **Step 4: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckAnalysisPanel.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckGraphPanel.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheckQueryPanel.vue
git commit -m "完成项目详情专项排查静态页面"
```

View File

@@ -0,0 +1,328 @@
# Special Check Family Asset Liability Frontend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将项目详情专项排查页从占位页升级为真实页面,新增“员工家庭资产负债专项核查”卡片,支持项目内员工列表展示与行内展开详情。
**Architecture:** 保持 `SpecialCheck.vue` 作为专项排查页入口,不新增路由或平行页面。前端拆成“主容器 + 专项核查列表区块 + 行内展开详情”三层结构,列表接口与详情接口分开请求,样式沿用结果总览已有卡片、表格、标签和空态语言。
**Tech Stack:** Vue 2, Element UI, Axios (`@/utils/request`), Node.js
---
### Task 1: 补专项核查 API 封装
**Files:**
- Create: `ruoyi-ui/src/api/ccdi/projectSpecialCheck.js`
- Test: `ruoyi-ui/tests/unit/special-check-family-asset-liability-api.test.js`
- [ ] **Step 1: Write the failing test**
新增接口契约静态断言,锁定以下方法与路径:
- `getFamilyAssetLiabilityList`
- `getFamilyAssetLiabilityDetail`
- `/ccdi/project/special-check/family-asset-liability/list`
- `/ccdi/project/special-check/family-asset-liability/detail`
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-family-asset-liability-api.test.js
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
在 API 模块中新增:
- `getFamilyAssetLiabilityList(projectId)`
- `getFamilyAssetLiabilityDetail(projectId, staffIdCard)`
要求:
- 统一使用 `@/utils/request`
- 详情接口透传 `projectId``staffIdCard`
- 不和 `projectOverview.js` 混写
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-family-asset-liability-api.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/api/ccdi/projectSpecialCheck.js ruoyi-ui/tests/unit/special-check-family-asset-liability-api.test.js
git commit -m "补充专项核查家庭资产负债前端接口"
```
### Task 2: 将 SpecialCheck.vue 从占位页升级为页面主容器
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/specialCheck.mock.js`
- Test: `ruoyi-ui/tests/unit/special-check-layout.test.js`
- Test: `ruoyi-ui/tests/unit/special-check-states.test.js`
- [ ] **Step 1: Write the failing test**
补页面主容器静态断言,锁定以下内容:
- `SpecialCheck.vue` 不再渲染“功能开发中...”
- 页面包含专项核查卡片标题“员工家庭资产负债专项核查”
- 页面保留 `loading / empty / loaded` 三态结构
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-layout.test.js
node tests/unit/special-check-states.test.js
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
`SpecialCheck.vue` 中:
- 引入真实列表接口
- 维持页面级 `loading / empty / loaded` 状态
- 页面主卡片采用与结果总览一致的白卡结构
- `specialCheck.mock.js` 仅保留空态和骨架所需结构,不额外扩展伪业务数据
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-layout.test.js
node tests/unit/special-check-states.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue ruoyi-ui/src/views/ccdiProject/components/detail/specialCheck.mock.js ruoyi-ui/tests/unit/special-check-layout.test.js ruoyi-ui/tests/unit/special-check-states.test.js
git commit -m "升级专项排查页主容器"
```
### Task 3: 实现员工家庭核查列表与风险标签
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue`
- Test: `ruoyi-ui/tests/unit/special-check-family-table.test.js`
- Test: `ruoyi-ui/tests/unit/special-check-risk-tag.test.js`
- [ ] **Step 1: Write the failing test**
新增区块组件静态断言,锁定:
- 列表字段包含姓名、身份证号、所属部门、家庭总年收入、家庭总资产、家庭总负债、风险情况、操作
- 风险情况使用标签展示
- 操作列文案为“查看详情”
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-family-table.test.js
node tests/unit/special-check-risk-tag.test.js
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
`FamilyAssetLiabilitySection.vue` 中:
- 接收列表数据与加载状态
- 使用 `el-table` 渲染员工家庭核查列表
- 风险标签颜色与结果总览现有标签语义保持一致
- 金额统一格式化为元或万元显示,前后一致即可,不增加额外切换控件
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-family-table.test.js
node tests/unit/special-check-risk-tag.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue ruoyi-ui/tests/unit/special-check-family-table.test.js ruoyi-ui/tests/unit/special-check-risk-tag.test.js
git commit -m "补充专项核查家庭资产负债列表展示"
```
### Task 4: 实现行内展开详情与按需查询
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue`
- Test: `ruoyi-ui/tests/unit/special-check-detail-expand.test.js`
- Test: `ruoyi-ui/tests/unit/special-check-detail-layout.test.js`
- [ ] **Step 1: Write the failing test**
新增详情展开断言,锁定:
- 点击“查看详情”后在当前行内展开
- 不出现弹窗、抽屉或路由跳转
- 详情固定分为收入、资产、负债三组
- 资产和负债明细均以表格或列表形式展开
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-detail-expand.test.js
node tests/unit/special-check-detail-layout.test.js
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
`FamilyAssetLiabilitySection.vue` 中:
- 管理当前展开员工 `expandedStaffIdCard`
- 首次展开时调用详情接口
- 再次点击同一行时收起
- 切换项目 `projectId` 时清空展开状态和详情缓存
`FamilyAssetLiabilityDetail.vue` 中:
- 展示收入明细、本人与配偶小计
- 展示资产合计与资产明细列表
- 展示负债合计与负债明细列表
- 对空明细显示局部空态,不额外跳页
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-detail-expand.test.js
node tests/unit/special-check-detail-layout.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue ruoyi-ui/tests/unit/special-check-detail-expand.test.js ruoyi-ui/tests/unit/special-check-detail-layout.test.js
git commit -m "补充专项核查家庭资产负债详情展开"
```
### Task 5: 对齐结果总览视觉并补记录
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue`
- Test: `ruoyi-ui/tests/unit/special-check-visual-alignment.test.js`
- Create: `docs/reports/implementation/2026-03-24-special-check-family-asset-liability-frontend-record.md`
- Create: `docs/tests/records/2026-03-24-special-check-family-asset-liability-frontend-verification.md`
- [ ] **Step 1: Write the failing test**
新增样式/结构静态断言,锁定:
- 使用结果总览同类白卡容器结构
- 标题、副标题、表格头、风险标签与页面留白节奏统一
- 页面不再出现占位图标与“功能开发中...”文案
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-visual-alignment.test.js
```
Expected:
- `FAIL`
- [ ] **Step 3: Write minimal implementation**
收口样式与文档:
- 对齐结果总览现有 `section-card / block-header / el-table` 风格
- 保持桌面端阅读密度稳定
- 确认空态、加载态、异常态都在当前卡片内解决
- 写前端实施记录与验证记录
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd ruoyi-ui
node tests/unit/special-check-family-asset-liability-api.test.js
node tests/unit/special-check-layout.test.js
node tests/unit/special-check-states.test.js
node tests/unit/special-check-family-table.test.js
node tests/unit/special-check-risk-tag.test.js
node tests/unit/special-check-detail-expand.test.js
node tests/unit/special-check-detail-layout.test.js
node tests/unit/special-check-visual-alignment.test.js
```
Expected:
- `PASS`
将执行命令与结果记录到 `docs/tests/records/2026-03-24-special-check-family-asset-liability-frontend-verification.md`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue docs/reports/implementation/2026-03-24-special-check-family-asset-liability-frontend-record.md docs/tests/records/2026-03-24-special-check-family-asset-liability-frontend-verification.md ruoyi-ui/tests/unit/special-check-visual-alignment.test.js
git commit -m "完成专项核查家庭资产负债前端实现"
```

View File

@@ -0,0 +1,84 @@
# 专项核查家庭资产负债测试数据实施计划
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
>
> 按仓库 `AGENTS.md` 约定,本次在当前会话直接执行,不启用 subagent。
**Goal:**`project_id = 51` 的“员工家庭资产负债专项核查”补充一组可重复执行的测试数据,覆盖正常、风险、高风险和缺失信息等典型情况。
**Architecture:** 复用现有测试项目 `51`,直接向 `ccdi_base_staff``ccdi_staff_fmy_relation``ccdi_asset_info``ccdi_debts_info``ccdi_bank_statement_tag_result` 补充专项核查专用样本,不新增新的项目、流水文件或前端入口。测试数据脚本采用“先删后插”的幂等方式,确保重复执行不会堆积脏数据,并通过与专项核查 SQL 同口径的查询完成验证。
**Tech Stack:** MySQL 8、仓库脚本 `bin/mysql_utf8_exec.sh`、专项核查 Mapper SQL、Markdown 实施与验证文档
---
### Task 1: 设计覆盖场景与金额口径
**Files:**
- Modify: `docs/plans/misc/2026-03-24-special-check-family-asset-liability-test-data-implementation.md`
- Create: `sql/migration/2026-03-24-add-special-check-family-asset-liability-test-data.sql`
- [x] **Step 1: 明确测试项目与入围方式**
复用 `project_id = 51`,通过 `ccdi_bank_statement_tag_result.object_type = 'STAFF_ID_CARD'` + `object_key = 员工身份证号` 的对象命中方式将新增员工纳入专项核查范围。
- [x] **Step 2: 设计 6 类样本**
1. 正常边界:`收入 + 负债 = 资产 * 1.5`
2. 风险边界:`收入 + 负债 = 资产 * 3`
3. 高风险:`收入 + 负债 > 资产 * 3`
4. 缺少本人负债:本人无负债记录,但家庭仍有配偶负债
5. 缺少本人资产:本人无资产记录,但家庭仍有配偶资产
6. 单身完整:无配偶,但本人资产与负债完整
- [x] **Step 3: 固化金额口径**
所有资产均使用 `current_value`,所有负债均使用 `principal_balance`,缺失判断严格沿用专项核查现有逻辑:
- 本人资产记录数为 `0` 时输出 `MISSING_INFO`
- 本人负债记录数为 `0` 时输出 `MISSING_INFO`
### Task 2: 编写幂等测试数据脚本
**Files:**
- Create: `sql/migration/2026-03-24-add-special-check-family-asset-liability-test-data.sql`
- [x] **Step 1: 先清理专用样本数据**
删除本次样本对应的员工、配偶关系、资产、负债和项目命中记录,确保脚本可重复执行。
- [x] **Step 2: 插入专项核查员工与配偶关系**
新增 6 名专项核查专用员工,其中 5 名带配偶关系,并补充本人/配偶年收入。
- [x] **Step 3: 插入资产与负债明细**
按场景写入本人与配偶资产、负债,确保边界值和缺失值可直接命中现有风险判断。
- [x] **Step 4: 插入项目命中记录**
`ccdi_bank_statement_tag_result` 写入对象命中数据,使样本员工进入 `project_id = 51` 的专项核查范围。
### Task 3: 执行脚本并验证结果
**Files:**
- Modify: `docs/reports/implementation/2026-03-24-special-check-family-asset-liability-test-data-record.md`
- Create: `docs/tests/records/2026-03-24-special-check-family-asset-liability-test-data-verification.md`
- [x] **Step 1: 执行脚本**
Run: `bin/mysql_utf8_exec.sh sql/migration/2026-03-24-add-special-check-family-asset-liability-test-data.sql`
Expected: 无报错返回,事务成功提交。
- [x] **Step 2: 查询列表结果**
按专项核查同口径 SQL 查询 `project_id = 51` 下名称为 `专项核查%` 的员工,验证金额汇总和风险等级是否符合预期。
- [x] **Step 3: 查询缺失信息详情**
验证“缺少本人负债样本”“缺少本人资产样本”的详情缺失标记是否分别返回 `missingSelfDebtInfo = 1``missingSelfAssetInfo = 1`
- [x] **Step 4: 补实施与验证记录**
将脚本路径、覆盖场景、执行结果与验证摘要沉淀到实施记录和验证文档中。

View File

@@ -0,0 +1,25 @@
# 专项核查详情卡片顺序调整实施记录
## 基本信息
- 日期2026-03-24
- 范围:专项核查查看详情区域卡片顺序
- 类型:前端展示调整
## 修改内容
1. 调整 `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue` 中详情卡片顺序。
2. 卡片顺序由“收入明细、资产明细、负债明细”调整为“收入明细、负债明细、资产明细”。
3. 未修改卡片中的字段内容、日期格式化逻辑和响应式样式,仅调整展示顺序。
## 测试与验证
- 执行:`node tests/unit/special-check-detail-layout.test.js`
- 执行:`node tests/unit/special-check-detail-date-display.test.js`
- 执行:`node tests/unit/special-check-family-table.test.js`
- 执行:`node tests/unit/special-check-detail-expand.test.js`
- 结果:通过
## 影响说明
- 仅影响专项核查详情区域三张卡片的展示顺序,不影响接口返回、字段取值和展开交互。

View File

@@ -0,0 +1,25 @@
# 专项核查详情日期时间展示优化实施记录
## 基本信息
- 日期2026-03-24
- 范围:专项核查列表中“查看详情”展开区域
- 类型:前端展示优化
## 修改内容
1. 调整 `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue` 中资产明细、负债明细的日期列展示方式。
2. 新增 `formatDetailDateTime` 方法,统一格式化详情中的 `valuationDate``queryDate`
3. 当日期值包含非零时间时,展示 `yyyy-MM-dd HH:mm:ss`;当时间为 `00:00:00` 时,仅展示 `yyyy-MM-dd`,避免出现无意义的零点时间。
4. 空值或不可解析值统一展示为 `-`
## 测试与验证
- 执行:`node tests/unit/special-check-detail-date-display.test.js`
- 执行:`node tests/unit/special-check-detail-layout.test.js`
- 执行:`node tests/unit/special-check-detail-expand.test.js`
- 结果:通过
## 影响说明
- 仅影响专项核查详情展开区的日期显示,不涉及接口、后端逻辑和数据结构调整。

View File

@@ -0,0 +1,94 @@
# 2026-03-24 专项核查页员工家庭资产负债专项核查后端实施记录
## 本次新增文件
- 控制器
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckController.java`
- 服务
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectSpecialCheckService.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectSpecialCheckServiceImpl.java`
- Mapper
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapper.java`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml`
- DTO
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectFamilyAssetLiabilityListQueryDTO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectFamilyAssetLiabilityDetailQueryDTO.java`
- VO
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityListItemVO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityListVO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyIncomeDetailVO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetItemVO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetDetailVO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyDebtItemVO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyDebtDetailVO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectFamilyAssetLiabilityDetailVO.java`
- 测试
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckControllerContractTest.java`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectSpecialCheckControllerTest.java`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperListSqlTest.java`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectSpecialCheckMapperDetailSqlTest.java`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectSpecialCheckServiceImplTest.java`
## 接口与查询口径
### 查询范围
- 员工集合沿用项目内已入库流水命中且能匹配员工主数据的口径
- 员工范围由 `ccdi_bank_statement_tag_result` 结合 `ccdi_base_staff``ccdi_bank_statement``ccdi_staff_fmy_relation` 反查得到
- 仅对匹配成功的员工输出专项核查结果
### 收入口径
- 本人收入取自 `ccdi_base_staff.annual_income`
- 配偶收入取自 `ccdi_staff_fmy_relation``relation_type = '配偶'``annual_income`
- 家庭总年收入 = 本人收入 + 配偶收入,空值按 `0` 处理
### 资产口径
- 资产来源表为 `ccdi_asset_info`
- 以员工身份证号为 `family_id`
- 持有人范围仅统计员工本人身份证号与配偶身份证号
- 家庭总资产按 `current_value` 汇总
### 负债口径
- 负债来源表为 `ccdi_debts_info`
- 归属人范围仅统计员工本人身份证号与配偶身份证号
- 家庭总负债按 `principal_balance` 汇总
### 风险等级口径
- `comparisonAmount = totalIncome + totalDebt`
- `comparisonAmount <= totalAsset * 1.5` 判定为 `NORMAL / 正常`
- `comparisonAmount > totalAsset * 1.5 and <= totalAsset * 3` 判定为 `RISK / 存在风险`
- `comparisonAmount > totalAsset * 3` 判定为 `HIGH / 高风险`
## 详情返回结构
- `incomeDetail` 返回本人收入、配偶收入、家庭总收入
- `assetDetail` 返回本人资产小计、配偶资产小计、家庭总资产、资产明细列表
- `debtDetail` 返回本人负债小计、配偶负债小计、家庭总负债、负债明细列表
- `summary` 返回与列表同口径的汇总金额与风险等级
## 范围说明
- 本次实现仅覆盖员工本人及配偶
- 未扩展父母、子女、兄弟姐妹等其他家庭成员收入、资产、负债口径
- 未复用结果总览员工结果表,也未改动流水打标链路
## 增量调整
### 本人信息缺失口径
- 若员工本人名下不存在资产记录,则标记为“缺少资产信息”
- 若员工本人名下不存在负债记录,则标记为“缺少负债信息”
- 列表与详情 `summary` 统一处理为:
- 只要本人缺少资产信息或负债信息任一项
- 风险等级直接输出 `MISSING_INFO / 缺少信息`
- 不再进入正常、存在风险、高风险三档计算
### 详情结构补充
- `assetDetail` 新增 `missingSelfAssetInfo`
- `debtDetail` 新增 `missingSelfDebtInfo`
- 供前端在详情卡片中控制资产/负债小计显隐

View File

@@ -0,0 +1,29 @@
# 2026-03-24 专项核查页员工家庭资产负债专项核查设计记录
## 本次产出
- 新增设计文档 `docs/design/2026-03-24-special-check-family-asset-liability-design.md`
- 明确专项排查页首个真实业务卡片为“员工家庭资产负债专项核查”
## 关键确认结论
- 员工范围沿用结果总览当前“项目内已入库流水命中员工”口径
- 配偶缺失数据按 `0` 参与计算
- 家庭总资产按 `current_value` 汇总
- 家庭总负债按 `principal_balance` 汇总
- 详情展示采用当前卡片内行展开
- 风险等级边界采用:
- `<= 1.5 倍` 正常
- `> 1.5 倍且 <= 3 倍` 存在风险
- `> 3 倍` 高风险
## 设计结论
- 专项排查页不再停留在占位态,需要新增真实业务卡片
- 前端采用“单卡列表 + 行内展开”方案,与结果总览风格统一
- 后端新增专项核查专用查询接口,不复用结果总览员工结果表
- 详情接口需返回收入、资产、负债三组细项,直接支撑前端展开区展示
## 后续动作
- 待用户审阅设计文档后,继续输出前端与后端实施计划

View File

@@ -0,0 +1,82 @@
# 2026-03-24 专项核查页员工家庭资产负债专项核查前端实施记录
## 本次新增与修改文件
### API
- 新增 `ruoyi-ui/src/api/ccdi/projectSpecialCheck.js`
### 页面与组件
- 修改 `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- 新增 `ruoyi-ui/src/views/ccdiProject/components/detail/specialCheck.mock.js`
- 新增 `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue`
- 新增 `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue`
### 测试
- 新增 `ruoyi-ui/tests/unit/special-check-family-asset-liability-api.test.js`
- 新增 `ruoyi-ui/tests/unit/special-check-layout.test.js`
- 新增 `ruoyi-ui/tests/unit/special-check-states.test.js`
- 新增 `ruoyi-ui/tests/unit/special-check-family-table.test.js`
- 新增 `ruoyi-ui/tests/unit/special-check-risk-tag.test.js`
- 新增 `ruoyi-ui/tests/unit/special-check-detail-expand.test.js`
- 新增 `ruoyi-ui/tests/unit/special-check-detail-layout.test.js`
- 新增 `ruoyi-ui/tests/unit/special-check-visual-alignment.test.js`
## 实现内容
### 1. 专项核查 API 封装
- 新增独立 API 文件 `projectSpecialCheck.js`
- 提供 `getFamilyAssetLiabilityList(projectId)``getFamilyAssetLiabilityDetail(projectId, staffIdCard)`
- 请求统一走 `@/utils/request`
### 2. 专项排查页主容器升级
- `SpecialCheck.vue` 从占位页改为真实三态容器
- 页面保留 `loading / empty / loaded` 三态
- 页面初始化与项目切换时都会重新触发专项核查列表加载
- `specialCheck.mock.js` 仅保留页面状态切换所需的最小结构
### 3. 员工家庭核查列表区块
- 新增 `FamilyAssetLiabilitySection.vue`
- 使用 `el-table` 展示姓名、身份证号、所属部门、家庭总年收入、家庭总资产、家庭总负债、风险情况、操作
- 风险标签统一使用 `el-tag`
- 风险码与标签类型映射:
- `NORMAL -> success`
- `RISK -> warning`
- `HIGH -> danger`
- 金额统一格式化为元并保留两位小数
### 4. 行内展开详情
- 新增 `FamilyAssetLiabilityDetail.vue`
- 详情固定拆成收入、资产、负债三组
- 详情使用当前表格行内展开,不引入弹窗、抽屉和路由跳转
- 首次点击“查看详情”时按需请求详情接口
- 详情结果按员工身份证号缓存
- 项目切换时清空展开状态与详情缓存,避免串项目数据
### 5. 视觉对齐收口
- 列表区块沿用结果总览已有的 `section-card / block-header / block-title / block-subtitle` 结构
- 表格头背景与标题区留白延续结果总览节奏
- 详情区块的标题层级统一为 `block-title / block-subtitle`
- 页面彻底移除“功能开发中”占位文案与占位图标
## 增量调整
### 缺少信息展示
- 风险标签新增 `MISSING_INFO` 映射,展示为“缺少信息”
- 当后端返回本人缺少资产信息时:
- 资产卡片隐藏小计区
- 当后端返回本人缺少负债信息时:
- 负债卡片隐藏小计区
### 详情卡片布局
- 详情区三个卡片改为桌面端横向三列均分
- 中小屏保留响应式回落为单列,避免表格挤压

View File

@@ -0,0 +1,31 @@
# 2026-03-24 专项核查页员工家庭资产负债专项核查实施计划记录
## 本次产出
- 新增后端实施计划 `docs/plans/backend/2026-03-24-special-check-family-asset-liability-backend-implementation.md`
- 新增前端实施计划 `docs/plans/frontend/2026-03-24-special-check-family-asset-liability-frontend-implementation.md`
- 新增后端实施记录 `docs/reports/implementation/2026-03-24-special-check-family-asset-liability-backend-record.md`
- 新增后端验证记录 `docs/tests/records/2026-03-24-special-check-family-asset-liability-backend-verification.md`
- 新增前端实施记录 `docs/reports/implementation/2026-03-24-special-check-family-asset-liability-frontend-record.md`
- 新增前端验证记录 `docs/tests/records/2026-03-24-special-check-family-asset-liability-frontend-verification.md`
## 计划拆分结论
- 后端计划聚焦专项核查专用接口、聚合 SQL、服务组装和后端验证记录
- 前端计划聚焦专项排查页容器升级、专项核查卡片、行内展开详情和前端验证记录
- 两端均不复用结果总览员工结果表,也不新增平行页面或弹窗详情
## 关键执行约束
- 员工范围沿用项目内已入库流水命中的员工
- 配偶缺失数据按 `0` 参与计算
- 家庭总资产按 `current_value` 汇总
- 家庭总负债按 `principal_balance` 汇总
- 详情交互固定为当前卡片内行展开
## 后续动作
- 已按实施计划顺序完成后端与前端任务
- 后端已完成接口、Mapper SQL、服务组装与计划内目标验证
- 前端已完成 API、主容器、列表区块、行内展开详情与计划内目标验证
- 后续可按需要执行联调验证或整理提交

View File

@@ -0,0 +1,94 @@
# 2026-03-24 专项核查页员工家庭资产负债专项核查测试数据实施记录
## 本次新增文件
- SQL 脚本
- `sql/migration/2026-03-24-add-special-check-family-asset-liability-test-data.sql`
- 计划文档
- `docs/plans/misc/2026-03-24-special-check-family-asset-liability-test-data-implementation.md`
## 数据落点
- `ccdi_base_staff`
- 新增 6 名专项核查专用员工
- `ccdi_staff_fmy_relation`
- 新增 5 条配偶关系,补充配偶收入
- `ccdi_asset_info`
- 新增资产明细,覆盖本人资产、配偶资产、单身资产
- `ccdi_debts_info`
- 新增负债明细,覆盖本人负债、配偶负债、单身负债
- `ccdi_bank_statement_tag_result`
-`project_id = 51` 新增对象命中记录,使样本员工进入专项核查范围
## 覆盖场景
### 1. 正常边界
- 员工:`专项核查正常边界样本`
- 目标:验证 `comparisonAmount = totalAsset * 1.5` 时命中 `NORMAL / 正常`
- 设计值:
- 家庭总收入:`300000.00`
- 家庭总资产:`1000000.00`
- 家庭总负债:`1200000.00`
### 2. 风险边界
- 员工:`专项核查风险边界样本`
- 目标:验证 `comparisonAmount = totalAsset * 3` 时命中 `RISK / 存在风险`
- 设计值:
- 家庭总收入:`300000.00`
- 家庭总资产:`500000.00`
- 家庭总负债:`1200000.00`
### 3. 高风险
- 员工:`专项核查高风险样本`
- 目标:验证 `comparisonAmount > totalAsset * 3` 时命中 `HIGH / 高风险`
- 设计值:
- 家庭总收入:`240000.00`
- 家庭总资产:`500000.00`
- 家庭总负债:`1400100.00`
### 4. 缺少本人负债
- 员工:`专项核查缺少负债样本`
- 目标:验证本人无负债记录时优先输出 `MISSING_INFO / 缺少信息`
- 设计值:
- 家庭总收入:`300000.00`
- 家庭总资产:`800000.00`
- 家庭总负债:`200000.00`
### 5. 缺少本人资产
- 员工:`专项核查缺少资产样本`
- 目标:验证本人无资产记录时优先输出 `MISSING_INFO / 缺少信息`
- 设计值:
- 家庭总收入:`350000.00`
- 家庭总资产:`600000.00`
- 家庭总负债:`400000.00`
### 6. 单身完整
- 员工:`专项核查单身完整样本`
- 目标:验证无配偶情况下仍可正常汇总本人资产与负债
- 设计值:
- 家庭总收入:`180000.00`
- 家庭总资产:`400000.00`
- 家庭总负债:`300000.00`
## 脚本策略
- 使用固定 `staff_id` 与身份证号作为专用测试样本主键
- 先删除后插入,确保脚本支持重复执行
- 不创建新的项目或路由,不改动现有专项核查逻辑
- 复用已有项目 `51`,避免引入额外项目初始化成本
## 执行与验证
- 执行脚本:
- `bin/mysql_utf8_exec.sh sql/migration/2026-03-24-add-special-check-family-asset-liability-test-data.sql`
- 验证记录:
- `docs/tests/records/2026-03-24-special-check-family-asset-liability-test-data-verification.md`
- 验证结论:
- 6 名专用样本均已进入 `project_id = 51` 的专项核查范围
- 正常边界、风险边界、高风险、缺少本人负债、缺少本人资产、单身完整 6 类场景均已命中预期结果

View File

@@ -0,0 +1,23 @@
# 专项排查图谱占位卡片前端实施记录
## 改动日期
2026-03-24
## 改动范围
- `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- `docs/tests/scripts/check-special-check-graph-placeholder.js`
## 实施内容
1. 在专项排查页面的“专项核查”区域下方新增一张静态占位卡片。
2. 卡片用于预留后续图谱外链入口,当前展示标题、说明文案、占位状态和禁用按钮。
3. 保持现有页面结构不变,不新增接口、不调整路由、不影响已有家庭资产负债专项核查卡片。
4. 新增轻量校验脚本,用于检查占位卡片关键结构与文案是否存在。
5. 根据页面占位展示需要,将图谱占位卡片的最小高度调整为 `500px`,增强视觉占位效果。
## 结果说明
- 专项排查页面现在包含“员工家庭资产负债专项核查”和“图谱外链展示”两张卡片。
- 图谱卡片当前仅作为占位入口,后续接入真实外链时可直接在现有卡片基础上扩展。

View File

@@ -0,0 +1,24 @@
# 专项核查汇总展示顺序调整实施记录
## 基本信息
- 日期2026-03-24
- 范围:专项核查列表汇总列展示顺序
- 类型:前端展示调整
## 修改内容
1. 调整 `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue` 中专项核查列表汇总列顺序。
2. 汇总字段展示顺序由“家庭总年收入、家庭总资产、家庭总负债”调整为“家庭总年收入、家庭总负债、家庭总资产”。
3. 未修改接口返回字段、排序逻辑和详情展开逻辑,仅调整前端表格列位置。
## 测试与验证
- 执行:`node tests/unit/special-check-family-table.test.js`
- 执行:`node tests/unit/special-check-detail-expand.test.js`
- 执行:`node tests/unit/special-check-detail-date-display.test.js`
- 结果:通过
## 影响说明
- 仅影响专项核查列表汇总列的前端展示顺序,不影响数据内容和后端接口。

View File

@@ -0,0 +1,54 @@
# 2026-03-24 专项核查页员工家庭资产负债专项核查后端验证记录
## 验证命令
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckControllerContractTest,CcdiProjectSpecialCheckMapperListSqlTest,CcdiProjectSpecialCheckMapperDetailSqlTest,CcdiProjectSpecialCheckServiceImplTest,CcdiProjectSpecialCheckControllerTest
```
## 验证结果
- 执行时间2026-03-24 17:38:26 +08:00
- 执行结果:`PASS`
- 测试统计:`Tests run: 13, Failures: 0, Errors: 0, Skipped: 0`
## 输出摘要
```text
[INFO] Running com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapperListSqlTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] Running com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapperDetailSqlTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] Running com.ruoyi.ccdi.project.controller.CcdiProjectSpecialCheckControllerContractTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO] Running com.ruoyi.ccdi.project.controller.CcdiProjectSpecialCheckControllerTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] Running com.ruoyi.ccdi.project.service.impl.CcdiProjectSpecialCheckServiceImplTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
```
## 结论
- 后端专项核查接口契约、列表 SQL、详情 SQL、服务组装与控制器返回均已通过计划内目标验证
- 验证过程中出现 Mockito 动态加载 agent 的 JDK 警告,但不影响本次测试通过结论
## 增量验证
### 验证命令
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectSpecialCheckControllerContractTest,CcdiProjectSpecialCheckMapperListSqlTest,CcdiProjectSpecialCheckMapperDetailSqlTest,CcdiProjectSpecialCheckServiceImplTest,CcdiProjectSpecialCheckControllerTest
```
### 验证结果
- 执行时间2026-03-24 17:58:10 +08:00
- 执行结果:`PASS`
- 测试统计:`Tests run: 14, Failures: 0, Errors: 0, Skipped: 0`
### 变更关注点
- 新增“本人缺少资产信息 / 本人缺少负债信息”口径
- 列表与详情 `summary` 风险等级可返回 `MISSING_INFO / 缺少信息`
- 详情结构新增缺失标记,供前端控制资产/负债小计显隐

View File

@@ -0,0 +1,67 @@
# 2026-03-24 专项核查页员工家庭资产负债专项核查前端验证记录
## 验证命令
```bash
cd ruoyi-ui
node tests/unit/special-check-family-asset-liability-api.test.js
node tests/unit/special-check-layout.test.js
node tests/unit/special-check-states.test.js
node tests/unit/special-check-family-table.test.js
node tests/unit/special-check-risk-tag.test.js
node tests/unit/special-check-detail-expand.test.js
node tests/unit/special-check-detail-layout.test.js
node tests/unit/special-check-visual-alignment.test.js
```
## 验证结果
- 执行时间2026-03-24 17:46:34 +0800
- 执行结果:`PASS`
- 测试统计8 个前端静态验证脚本全部通过
## 输出摘要
```text
node tests/unit/special-check-family-asset-liability-api.test.js
node tests/unit/special-check-layout.test.js
node tests/unit/special-check-states.test.js
node tests/unit/special-check-family-table.test.js
node tests/unit/special-check-risk-tag.test.js
node tests/unit/special-check-detail-expand.test.js
node tests/unit/special-check-detail-layout.test.js
node tests/unit/special-check-visual-alignment.test.js
```
## 结论
- 前端 API、主容器三态、列表区块、风险标签、行内展开详情与视觉对齐约束均通过计划内验证
- 专项排查页已不再保留“功能开发中”占位实现
## 增量验证
### 验证命令
```bash
cd ruoyi-ui
node tests/unit/special-check-family-asset-liability-api.test.js
node tests/unit/special-check-layout.test.js
node tests/unit/special-check-states.test.js
node tests/unit/special-check-family-table.test.js
node tests/unit/special-check-risk-tag.test.js
node tests/unit/special-check-detail-expand.test.js
node tests/unit/special-check-detail-layout.test.js
node tests/unit/special-check-visual-alignment.test.js
```
### 验证结果
- 执行时间2026-03-24 17:58:10 +08:00
- 执行结果:`PASS`
- 测试统计8 个前端静态验证脚本全部通过
### 变更关注点
- 风险标签新增 `MISSING_INFO`
- 详情资产/负债卡片支持按缺失标记隐藏小计
- 三张详情卡片保持横向均分布局

View File

@@ -0,0 +1,66 @@
# 2026-03-24 专项核查页员工家庭资产负债专项核查测试数据验证记录
## 执行命令
```bash
bin/mysql_utf8_exec.sh sql/migration/2026-03-24-add-special-check-family-asset-liability-test-data.sql
```
## 执行结果
- 执行时间2026-03-24 20:58:39 +0800
- 执行结果:`PASS`
- 说明:脚本执行无报错,事务成功提交
## 列表口径验证
### 验证 SQL
```sql
SELECT
bs.name AS staff_name,
COALESCE(bs.annual_income, 0) + COALESCE(spouse.spouse_income, 0) AS total_income,
COALESCE(asset.total_asset, 0) AS total_asset,
COALESCE(debt.total_debt, 0) AS total_debt,
COALESCE(bs.annual_income, 0) + COALESCE(spouse.spouse_income, 0) + COALESCE(debt.total_debt, 0) AS comparison_amount,
CASE
WHEN COALESCE(asset.self_asset_count, 0) = 0 OR COALESCE(debt.self_debt_count, 0) = 0 THEN 'MISSING_INFO'
WHEN COALESCE(bs.annual_income, 0) + COALESCE(spouse.spouse_income, 0) + COALESCE(debt.total_debt, 0) <= COALESCE(asset.total_asset, 0) * 1.5 THEN 'NORMAL'
WHEN COALESCE(bs.annual_income, 0) + COALESCE(spouse.spouse_income, 0) + COALESCE(debt.total_debt, 0) <= COALESCE(asset.total_asset, 0) * 3 THEN 'RISK'
ELSE 'HIGH'
END AS risk_level_code
FROM ccdi_base_staff bs
...
WHERE bs.name LIKE '专项核查%'
ORDER BY bs.staff_id;
```
### 验证结果
| 员工 | 家庭总收入 | 家庭总资产 | 家庭总负债 | 对比金额 | 风险结果 |
|------|------------|------------|------------|----------|----------|
| 专项核查正常边界样本 | 300000.00 | 1000000.00 | 1200000.00 | 1500000.00 | NORMAL |
| 专项核查风险边界样本 | 300000.00 | 500000.00 | 1200000.00 | 1500000.00 | RISK |
| 专项核查高风险样本 | 240000.00 | 500000.00 | 1400100.00 | 1640100.00 | HIGH |
| 专项核查缺少负债样本 | 300000.00 | 800000.00 | 200000.00 | 500000.00 | MISSING_INFO |
| 专项核查缺少资产样本 | 350000.00 | 600000.00 | 400000.00 | 750000.00 | MISSING_INFO |
| 专项核查单身完整样本 | 180000.00 | 400000.00 | 300000.00 | 480000.00 | NORMAL |
## 缺失标记验证
### 验证结果
| 员工 | missing_self_asset_info | missing_self_debt_info | self_total_asset | self_total_debt |
|------|-------------------------|------------------------|------------------|-----------------|
| 专项核查缺少负债样本 | 0 | 1 | 450000.00 | 0.00 |
| 专项核查缺少资产样本 | 1 | 0 | 0.00 | 250000.00 |
## 项目入围验证
- `project_id = 51` 下新增专项核查样本命中数:`6`
- 说明6 名专用员工均已进入专项核查范围
## 结论
- 新增测试数据已覆盖正常边界、风险边界、高风险、缺少本人负债、缺少本人资产、单身完整 6 类情况
- 当前专项核查风险判断口径与缺失标记口径均能被这组样本稳定命中

View File

@@ -0,0 +1,38 @@
# 专项排查图谱占位卡片前端验证记录
## 验证日期
2026-03-24
## 验证内容
### 1. 轻量校验脚本红绿验证
- 首次执行命令:
```bash
node docs/tests/scripts/check-special-check-graph-placeholder.js
```
- 初始结果:失败
- 失败原因:`SpecialCheck.vue` 中缺少图谱占位卡片的关键结构与文案
- 修改后再次执行命令:
```bash
node docs/tests/scripts/check-special-check-graph-placeholder.js
```
- 结果:通过
- 追加校验:脚本已覆盖 `min-height: 500px` 的高度断言
### 2. 前端构建验证
- 执行命令:
```bash
cd ruoyi-ui && npm run build:prod
```
- 结果:通过
- 备注构建输出存在既有的资源体积告警asset size limit / entrypoint size limit未出现与本次改动相关的编译错误

View File

@@ -0,0 +1,31 @@
const fs = require("fs");
const path = require("path");
const componentPath = path.resolve(
__dirname,
"../../../ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue"
);
const componentContent = fs.readFileSync(componentPath, "utf8");
const requiredSnippets = [
'class="graph-placeholder-card"',
"图谱外链展示",
"用于后续接入外链图谱页面",
"待接入",
"min-height: 500px",
];
const missingSnippets = requiredSnippets.filter(
(snippet) => !componentContent.includes(snippet)
);
if (missingSnippets.length) {
console.error("专项核查图谱占位卡片校验失败,缺少以下内容:");
missingSnippets.forEach((snippet) => {
console.error(`- ${snippet}`);
});
process.exit(1);
}
console.log("专项核查图谱占位卡片校验通过");

View File

@@ -0,0 +1,20 @@
import request from "@/utils/request";
export function getFamilyAssetLiabilityList(projectId) {
return request({
url: "/ccdi/project/special-check/family-asset-liability/list",
method: "get",
params: { projectId },
});
}
export function getFamilyAssetLiabilityDetail(projectId, staffIdCard) {
return request({
url: "/ccdi/project/special-check/family-asset-liability/detail",
method: "get",
params: {
projectId,
staffIdCard,
},
});
}

View File

@@ -0,0 +1,246 @@
<template>
<div class="family-asset-liability-detail" v-loading="loading">
<div class="detail-grid">
<section class="detail-block">
<div class="block-header">
<div>
<div class="block-title">收入明细</div>
<div class="block-subtitle">展示本人配偶与家庭收入汇总</div>
</div>
</div>
<div class="detail-metric-list">
<div class="detail-metric-item">
<span class="metric-label">本人收入</span>
<span class="metric-value">{{ formatAmount(incomeDetail.selfIncome) }}</span>
</div>
<div class="detail-metric-item">
<span class="metric-label">配偶收入</span>
<span class="metric-value">{{ formatAmount(incomeDetail.spouseIncome) }}</span>
</div>
<div class="detail-metric-item">
<span class="metric-label">家庭总收入</span>
<span class="metric-value">{{ formatAmount(incomeDetail.totalIncome) }}</span>
</div>
</div>
</section>
<section class="detail-block">
<div class="block-header">
<div>
<div class="block-title">负债明细</div>
<div class="block-subtitle">展示本人及配偶负债小计与明细列表</div>
</div>
</div>
<div v-if="!debtDetail.missingSelfDebtInfo" class="detail-summary">
<span>本人负债小计{{ formatAmount(debtDetail.selfTotalDebt) }}</span>
<span>配偶负债小计{{ formatAmount(debtDetail.spouseTotalDebt) }}</span>
</div>
<el-table v-if="debtItems.length" :data="debtItems" size="mini" class="detail-table">
<el-table-column prop="debtName" label="负债名称" min-width="140" />
<el-table-column prop="debtMainType" label="负债大类" min-width="100" />
<el-table-column prop="debtSubType" label="负债小类" min-width="120" />
<el-table-column prop="creditorType" label="债权人类型" min-width="120" />
<el-table-column prop="ownerName" label="归属人" min-width="100" />
<el-table-column label="本金余额" min-width="120">
<template slot-scope="scope">
<span>{{ formatAmount(scope.row.principalBalance) }}</span>
</template>
</el-table-column>
<el-table-column label="查询日期" min-width="160">
<template slot-scope="scope">
<span>{{ formatDetailDateTime(scope.row.queryDate) }}</span>
</template>
</el-table-column>
</el-table>
<el-empty v-else :image-size="64" description="暂无负债明细" />
</section>
<section class="detail-block">
<div class="block-header">
<div>
<div class="block-title">资产明细</div>
<div class="block-subtitle">展示本人及配偶资产小计与明细列表</div>
</div>
</div>
<div v-if="!assetDetail.missingSelfAssetInfo" class="detail-summary">
<span>本人资产小计{{ formatAmount(assetDetail.selfTotalAsset) }}</span>
<span>配偶资产小计{{ formatAmount(assetDetail.spouseTotalAsset) }}</span>
</div>
<el-table v-if="assetItems.length" :data="assetItems" size="mini" class="detail-table">
<el-table-column prop="assetName" label="资产名称" min-width="140" />
<el-table-column prop="assetMainType" label="资产大类" min-width="100" />
<el-table-column prop="assetSubType" label="资产小类" min-width="120" />
<el-table-column prop="holderName" label="持有人" min-width="100" />
<el-table-column label="当前估值" min-width="120">
<template slot-scope="scope">
<span>{{ formatAmount(scope.row.currentValue) }}</span>
</template>
</el-table-column>
<el-table-column label="估值日期" min-width="160">
<template slot-scope="scope">
<span>{{ formatDetailDateTime(scope.row.valuationDate) }}</span>
</template>
</el-table-column>
</el-table>
<el-empty v-else :image-size="64" description="暂无资产明细" />
</section>
</div>
</div>
</template>
<script>
import { parseTime } from "@/utils/ruoyi";
export default {
name: "FamilyAssetLiabilityDetail",
props: {
detail: {
type: Object,
default: () => ({}),
},
loading: {
type: Boolean,
default: false,
},
},
computed: {
incomeDetail() {
return this.detail && this.detail.incomeDetail
? this.detail.incomeDetail
: {
selfIncome: 0,
spouseIncome: 0,
totalIncome: 0,
};
},
assetDetail() {
return this.detail && this.detail.assetDetail
? this.detail.assetDetail
: {
missingSelfAssetInfo: false,
selfTotalAsset: 0,
spouseTotalAsset: 0,
totalAsset: 0,
items: [],
};
},
debtDetail() {
return this.detail && this.detail.debtDetail
? this.detail.debtDetail
: {
missingSelfDebtInfo: false,
selfTotalDebt: 0,
spouseTotalDebt: 0,
totalDebt: 0,
items: [],
};
},
assetItems() {
return Array.isArray(this.assetDetail.items) ? this.assetDetail.items : [];
},
debtItems() {
return Array.isArray(this.debtDetail.items) ? this.debtDetail.items : [];
},
},
methods: {
formatAmount(value) {
const amount = Number(value || 0);
return `${amount.toLocaleString("zh-CN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})} 元`;
},
formatDetailDateTime(value) {
if (!value) {
return "-";
}
const formatted = parseTime(value, "{y}-{m}-{d} {h}:{i}:{s}");
if (!formatted) {
return "-";
}
const hasTime = !formatted.endsWith(" 00:00:00");
return parseTime(value, hasTime ? "{y}-{m}-{d} {h}:{i}:{s}" : "{y}-{m}-{d}") || "-";
},
},
};
</script>
<style lang="scss" scoped>
.family-asset-liability-detail {
padding: 8px 0 4px;
}
.detail-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.detail-block {
padding: 16px;
background: #f8fafc;
border: 1px solid #e2e8f0;
min-width: 0;
}
.block-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.block-title {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.block-subtitle {
margin-top: 4px;
font-size: 12px;
color: #94a3b8;
}
.detail-metric-list {
display: grid;
gap: 8px;
}
.detail-metric-item {
display: flex;
justify-content: space-between;
gap: 16px;
font-size: 13px;
color: #475569;
}
.metric-value {
font-weight: 600;
color: #111827;
}
.detail-summary {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 12px;
font-size: 13px;
color: #475569;
}
.detail-table {
border-radius: 12px;
overflow: hidden;
}
:deep(.detail-table .el-table__body-wrapper) {
overflow-x: auto;
}
@media (max-width: 1200px) {
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,211 @@
<template>
<section class="family-asset-liability-section">
<div class="section-card">
<div class="block-header">
<div>
<div class="block-title">{{ title }}</div>
<div class="block-subtitle">{{ subtitle }}</div>
</div>
</div>
<el-table
ref="familyTable"
v-loading="loading"
:data="rows"
class="family-table"
row-key="staffIdCard"
:expand-row-keys="expandedRowKeys"
>
<template slot="empty">
<el-empty :image-size="80" description="暂无员工家庭资产负债核查数据" />
</template>
<el-table-column type="expand" width="1">
<template slot-scope="scope">
<family-asset-liability-detail
:detail="detailCache[scope.row.staffIdCard]"
:loading="Boolean(detailLoadingMap[scope.row.staffIdCard])"
/>
</template>
</el-table-column>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="staffName" label="姓名" min-width="100" />
<el-table-column prop="staffIdCard" label="身份证号" min-width="180" />
<el-table-column prop="deptName" label="所属部门" min-width="140" />
<el-table-column label="家庭总年收入" min-width="140">
<template slot-scope="scope">
<span>{{ formatAmount(scope.row.totalIncome) }}</span>
</template>
</el-table-column>
<el-table-column label="家庭总负债" min-width="140">
<template slot-scope="scope">
<span>{{ formatAmount(scope.row.totalDebt) }}</span>
</template>
</el-table-column>
<el-table-column label="家庭总资产" min-width="140">
<template slot-scope="scope">
<span>{{ formatAmount(scope.row.totalAsset) }}</span>
</template>
</el-table-column>
<el-table-column label="风险情况" min-width="120">
<template slot-scope="scope">
<el-tag size="mini" effect="plain" :type="resolveRiskTagType(scope.row.riskLevelCode)">
{{ scope.row.riskLevelName || "-" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="right">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="handleToggleDetail(scope.row)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</section>
</template>
<script>
import { getFamilyAssetLiabilityDetail } from "@/api/ccdi/projectSpecialCheck";
import FamilyAssetLiabilityDetail from "./FamilyAssetLiabilityDetail";
export default {
name: "FamilyAssetLiabilitySection",
components: {
FamilyAssetLiabilityDetail,
},
props: {
rows: {
type: Array,
default: () => [],
},
loading: {
type: Boolean,
default: false,
},
projectId: {
type: [String, Number],
default: null,
},
title: {
type: String,
default: "员工家庭资产负债专项核查",
},
subtitle: {
type: String,
default: "展示项目内员工家庭收入、资产、负债与风险情况",
},
},
data() {
return {
expandedStaffIdCard: "",
detailCache: {},
detailLoadingMap: {},
};
},
computed: {
expandedRowKeys() {
return this.expandedStaffIdCard ? [this.expandedStaffIdCard] : [];
},
},
watch: {
projectId() {
this.resetDetailState();
},
},
methods: {
resolveRiskTagType(riskLevelCode) {
const riskTagTypeMap = {
NORMAL: "success",
RISK: "warning",
HIGH: "danger",
MISSING_INFO: "info",
};
return riskTagTypeMap[riskLevelCode] || "info";
},
formatAmount(value) {
const amount = Number(value || 0);
return `${amount.toLocaleString("zh-CN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})} 元`;
},
async handleToggleDetail(row) {
if (!row || !row.staffIdCard) {
return;
}
if (this.expandedStaffIdCard === row.staffIdCard) {
this.expandedStaffIdCard = "";
return;
}
this.expandedStaffIdCard = row.staffIdCard;
if (!this.detailCache[row.staffIdCard]) {
await this.loadFamilyDetail(row);
}
},
async loadFamilyDetail(row) {
const staffIdCard = row.staffIdCard;
this.$set(this.detailLoadingMap, staffIdCard, true);
try {
const response = await getFamilyAssetLiabilityDetail(this.projectId, staffIdCard);
const detail = (response && response.data) || {};
this.$set(this.detailCache, staffIdCard, detail);
} catch (error) {
this.$set(this.detailCache, staffIdCard, null);
console.error("加载员工家庭资产负债详情失败", error);
} finally {
this.$set(this.detailLoadingMap, staffIdCard, false);
}
},
resetDetailState() {
this.expandedStaffIdCard = "";
this.detailCache = {};
this.detailLoadingMap = {};
},
},
};
</script>
<style lang="scss" scoped>
.family-asset-liability-section {
margin-bottom: 16px;
}
.section-card {
padding: 20px;
border-radius: 0;
background: #fff;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.block-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.block-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.block-subtitle {
margin-top: 4px;
font-size: 12px;
color: #94a3b8;
}
.family-table {
border-radius: 12px;
overflow: hidden;
}
:deep(.family-table th) {
background: #f8fafc;
color: #64748b;
}
</style>

View File

@@ -1,15 +1,56 @@
<template>
<div class="special-check-container">
<div class="placeholder-content">
<i class="el-icon-search"></i>
<p>专项排查功能开发中...</p>
<div v-if="pageState === 'loading'" class="special-check-state">
<div class="state-card">
<el-skeleton animated :rows="6" />
</div>
</div>
<div v-else-if="pageState === 'empty'" class="special-check-state">
<div class="state-card">
<el-empty description="暂无员工家庭资产负债核查数据" />
</div>
</div>
<div v-else class="special-check-page">
<family-asset-liability-section
:rows="currentData.rows"
:loading="false"
:project-id="projectId"
:title="sectionTitle"
:subtitle="sectionSubtitle"
/>
<section class="graph-placeholder-card">
<div class="graph-placeholder-header">
<div>
<div class="graph-placeholder-title">图谱外链展示</div>
<div class="graph-placeholder-subtitle">用于后续接入外链图谱页面</div>
</div>
<el-tag size="mini" type="info" effect="plain">占位中</el-tag>
</div>
<div class="graph-placeholder-body">
<div class="graph-placeholder-text">
当前卡片用于预留专项核查图谱入口后续接入外链地址后可在此直接跳转展示
</div>
<el-button type="primary" size="small" disabled>待接入</el-button>
</div>
</section>
</div>
</div>
</template>
<script>
import { createSpecialCheckLoadedData, specialCheckStateData } from "./specialCheck.mock";
import { getFamilyAssetLiabilityList } from "@/api/ccdi/projectSpecialCheck";
import FamilyAssetLiabilitySection from "./FamilyAssetLiabilitySection";
export default {
name: "SpecialCheck",
components: {
FamilyAssetLiabilitySection,
},
props: {
projectId: {
type: [String, Number],
@@ -24,28 +65,129 @@ export default {
}),
},
},
data() {
return {
pageState: "loading",
realData: specialCheckStateData.loading,
sectionTitle: "员工家庭资产负债专项核查",
sectionSubtitle: "按项目员工范围聚合本人及配偶的收入、资产与负债情况",
};
},
computed: {
currentData() {
if (this.pageState === "loaded") {
return this.realData;
}
return specialCheckStateData[this.pageState] || this.realData;
},
},
watch: {
projectId(newVal) {
if (newVal) {
this.loadSpecialCheckData();
return;
}
this.realData = specialCheckStateData.empty;
this.pageState = "empty";
},
},
created() {
if (this.projectId) {
this.loadSpecialCheckData();
return;
}
this.realData = specialCheckStateData.empty;
this.pageState = "empty";
},
methods: {
async loadSpecialCheckData() {
if (!this.projectId) {
this.realData = specialCheckStateData.empty;
this.pageState = "empty";
return;
}
this.pageState = "loading";
try {
const response = await getFamilyAssetLiabilityList(this.projectId);
const listData = (response && response.data) || {};
this.realData = createSpecialCheckLoadedData({
projectId: this.projectId,
listData,
});
this.pageState = this.realData.rows.length ? "loaded" : "empty";
} catch (error) {
this.realData = specialCheckStateData.empty;
this.pageState = "empty";
console.error("加载专项核查数据失败", error);
}
},
},
};
</script>
<style lang="scss" scoped>
.special-check-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
padding: 0 0 24px;
}
.special-check-state {
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
.state-card {
padding: 32px 24px;
border-radius: 0;
background: #fff;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
i {
font-size: 48px;
margin-bottom: 16px;
}
.special-check-page {
min-height: 400px;
}
p {
font-size: 14px;
margin: 0;
}
.graph-placeholder-card {
margin-top: 16px;
min-height: 500px;
padding: 20px;
background: #fff;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.graph-placeholder-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.graph-placeholder-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.graph-placeholder-subtitle {
margin-top: 4px;
font-size: 12px;
color: #94a3b8;
}
.graph-placeholder-body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 18px;
padding: 16px 18px;
border: 1px dashed #dbeafe;
background: #f8fbff;
}
.graph-placeholder-text {
font-size: 14px;
line-height: 22px;
color: #475569;
}
</style>

View File

@@ -0,0 +1,24 @@
const baseLoadedData = {
projectId: null,
rows: [],
};
export function createSpecialCheckLoadedData({ projectId, listData } = {}) {
return {
...baseLoadedData,
projectId,
rows: Array.isArray(listData && listData.rows) ? listData.rows : [],
};
}
export const specialCheckStateData = {
loaded: baseLoadedData,
empty: {
...baseLoadedData,
rows: [],
},
loading: {
...baseLoadedData,
rows: [],
},
};

View File

@@ -0,0 +1,25 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const componentPath = path.resolve(
__dirname,
"../../src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue"
);
const source = fs.readFileSync(componentPath, "utf8");
[
"formatDetailDateTime(value)",
'return parseTime(value, hasTime ? "{y}-{m}-{d} {h}:{i}:{s}" : "{y}-{m}-{d}") || "-";',
"{{ formatDetailDateTime(scope.row.valuationDate) }}",
"{{ formatDetailDateTime(scope.row.queryDate) }}",
].forEach((token) => {
assert(source.includes(token), `专项核查详情日期展示缺少关键实现: ${token}`);
});
assert(
source.includes("const hasTime = !formatted.endsWith(\" 00:00:00\");"),
"专项核查详情应隐藏无意义的零点时间"
);
console.log("special-check-detail-date-display test passed");

View File

@@ -0,0 +1,16 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const sectionSource = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue"),
"utf8"
);
assert(sectionSource.includes("expandedStaffIdCard"), "缺少当前展开员工状态");
assert(sectionSource.includes("detailCache"), "缺少详情缓存");
assert(sectionSource.includes("getFamilyAssetLiabilityDetail"), "首次展开应请求详情接口");
assert(sectionSource.includes("查看详情"), "缺少详情操作入口");
assert(!sectionSource.includes("el-dialog"), "不应使用弹窗详情");
assert(!sectionSource.includes("el-drawer"), "不应使用抽屉详情");
assert(!sectionSource.includes("$router"), "不应通过路由跳转查看详情");

View File

@@ -0,0 +1,42 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const source = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue"),
"utf8"
);
[
"收入明细",
"负债明细",
"资产明细",
"本人收入",
"配偶收入",
"本人资产小计",
"配偶资产小计",
"本人负债小计",
"配偶负债小计",
"el-table",
"el-empty",
].forEach((token) => assert(source.includes(token), token));
assert(source.includes("missingSelfAssetInfo"), "资产卡片应支持缺少信息判断");
assert(source.includes("missingSelfDebtInfo"), "负债卡片应支持缺少信息判断");
assert(source.includes('v-if="!assetDetail.missingSelfAssetInfo"'), "资产小计应可隐藏");
assert(source.includes('v-if="!debtDetail.missingSelfDebtInfo"'), "负债小计应可隐藏");
const incomeIndex = source.indexOf("收入明细");
const debtIndex = source.indexOf("负债明细");
const assetIndex = source.indexOf("资产明细");
assert(incomeIndex > -1, "缺少收入明细卡片");
assert(debtIndex > -1, "缺少负债明细卡片");
assert(assetIndex > -1, "缺少资产明细卡片");
assert(incomeIndex < debtIndex && debtIndex < assetIndex, "详情卡片顺序应为收入、负债、资产");
assert(
source.includes("grid-template-columns: repeat(3, minmax(0, 1fr));"),
"三个详情卡片应横向均分"
);
assert(source.includes("@media (max-width: 1200px)"), "中小屏应保留响应式回落");

View File

@@ -0,0 +1,22 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const source = fs.readFileSync(
path.resolve(__dirname, "../../src/api/ccdi/projectSpecialCheck.js"),
"utf8"
);
[
"getFamilyAssetLiabilityList",
"getFamilyAssetLiabilityDetail",
"/ccdi/project/special-check/family-asset-liability/list",
"/ccdi/project/special-check/family-asset-liability/detail",
].forEach((token) => assert(source.includes(token), token));
const detailStart = source.indexOf("export function getFamilyAssetLiabilityDetail(projectId, staffIdCard)");
assert(detailStart >= 0, "缺少详情接口函数定义");
const detailBlock = source.slice(detailStart, detailStart + 260);
assert(detailBlock.includes("projectId"), "详情接口缺少 projectId");
assert(detailBlock.includes("staffIdCard"), "详情接口缺少 staffIdCard");

View File

@@ -0,0 +1,37 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const source = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue"),
"utf8"
);
[
"姓名",
"身份证号",
"所属部门",
"家庭总年收入",
"家庭总负债",
"家庭总资产",
"风险情况",
"操作",
"查看详情",
"el-table",
"el-table-column",
].forEach((token) => assert(source.includes(token), token));
assert(source.includes("loading"), "列表区块应接收加载状态");
assert(source.includes("rows"), "列表区块应消费列表数据");
const incomeIndex = source.indexOf('label="家庭总年收入"');
const debtIndex = source.indexOf('label="家庭总负债"');
const assetIndex = source.indexOf('label="家庭总资产"');
assert(incomeIndex > -1, "缺少家庭总年收入列");
assert(debtIndex > -1, "缺少家庭总负债列");
assert(assetIndex > -1, "缺少家庭总资产列");
assert(
incomeIndex < debtIndex && debtIndex < assetIndex,
"汇总列顺序应为家庭总年收入、家庭总负债、家庭总资产"
);

View File

@@ -0,0 +1,13 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const source = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/SpecialCheck.vue"),
"utf8"
);
assert(!source.includes("专项排查功能开发中"), "不应继续保留占位文案");
assert(!source.includes("功能开发中..."), "不应继续保留功能开发中提示");
assert(source.includes("员工家庭资产负债专项核查"), "缺少专项核查卡片标题");
assert(source.includes("getFamilyAssetLiabilityList"), "主容器应接入真实列表接口");

View File

@@ -0,0 +1,15 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const source = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue"),
"utf8"
);
assert(source.includes("el-tag"), "风险情况应使用标签展示");
assert(source.includes("resolveRiskTagType"), "缺少风险标签类型映射");
assert(source.includes("NORMAL"), "缺少正常风险码映射");
assert(source.includes("RISK"), "缺少存在风险码映射");
assert(source.includes("HIGH"), "缺少高风险码映射");
assert(source.includes("MISSING_INFO"), "缺少缺少信息风险码映射");

View File

@@ -0,0 +1,20 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const vueSource = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/SpecialCheck.vue"),
"utf8"
);
const mockSource = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/specialCheck.mock.js"),
"utf8"
);
["loading", "empty", "loaded"].forEach((state) => {
assert(vueSource.includes(`pageState === "${state}"`) || vueSource.includes(`pageState === '${state}'`), state);
assert(mockSource.includes(`${state}:`), `mock 中缺少 ${state} 状态`);
});
assert(vueSource.includes("el-skeleton"), "加载态应保留骨架屏");
assert(vueSource.includes("el-empty"), "空态应保留空态组件");

View File

@@ -0,0 +1,25 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const specialCheckSource = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/SpecialCheck.vue"),
"utf8"
);
const sectionSource = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue"),
"utf8"
);
const detailSource = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue"),
"utf8"
);
assert(!specialCheckSource.includes("功能开发中"), "不应再出现占位文案");
assert(sectionSource.includes("section-card"), "列表区块应沿用白卡容器结构");
assert(sectionSource.includes("block-header"), "列表区块应沿用标题头结构");
assert(sectionSource.includes("block-title"), "列表区块缺少标题样式");
assert(sectionSource.includes("block-subtitle"), "列表区块缺少副标题样式");
assert(sectionSource.includes(":deep(.family-table th)"), "表格头样式应与结果总览统一");
assert(sectionSource.includes("el-tag"), "风险标签应保留标签形态");
assert(detailSource.includes("block-title"), "详情区块标题应与结果总览标题层级统一");

View File

@@ -0,0 +1,231 @@
START TRANSACTION;
-- 1. 清理本次专项核查专用样本
DELETE FROM ccdi_bank_statement_tag_result
WHERE project_id = 51
AND object_type = 'STAFF_ID_CARD'
AND object_key IN (
'330101199003010101',
'330101199003010102',
'330101199003010103',
'330101199003010104',
'330101199003010105',
'330101199003010106'
);
DELETE FROM ccdi_debts_info
WHERE person_id IN (
'330101199003010101',
'330101199003010102',
'330101199003010103',
'330101199003010104',
'330101199003010105',
'330101199003010106',
'330101199104010101',
'330101199104010102',
'330101199104010103',
'330101199104010104',
'330101199104010105'
);
DELETE FROM ccdi_asset_info
WHERE family_id IN (
'330101199003010101',
'330101199003010102',
'330101199003010103',
'330101199003010104',
'330101199003010105',
'330101199003010106'
)
OR person_id IN (
'330101199003010101',
'330101199003010102',
'330101199003010103',
'330101199003010104',
'330101199003010105',
'330101199003010106',
'330101199104010101',
'330101199104010102',
'330101199104010103',
'330101199104010104',
'330101199104010105'
);
DELETE FROM ccdi_staff_fmy_relation
WHERE person_id IN (
'330101199003010101',
'330101199003010102',
'330101199003010103',
'330101199003010104',
'330101199003010105'
)
AND relation_cert_no IN (
'330101199104010101',
'330101199104010102',
'330101199104010103',
'330101199104010104',
'330101199104010105'
);
DELETE FROM ccdi_base_staff
WHERE staff_id IN (1900001, 1900002, 1900003, 1900004, 1900005, 1900006)
OR id_card IN (
'330101199003010101',
'330101199003010102',
'330101199003010103',
'330101199003010104',
'330101199003010105',
'330101199003010106'
);
-- 2. 新增专项核查员工
INSERT INTO ccdi_base_staff (
staff_id,
name,
dept_id,
id_card,
phone,
annual_income,
hire_date,
status,
create_by,
create_time,
update_by,
update_time
) VALUES
(1900001, '专项核查正常边界样本', 100, '330101199003010101', '13900000001', 200000.00, '2020-01-10', '0', 'admin', NOW(), 'admin', NOW()),
(1900002, '专项核查风险边界样本', 100, '330101199003010102', '13900000002', 180000.00, '2020-01-10', '0', 'admin', NOW(), 'admin', NOW()),
(1900003, '专项核查高风险样本', 100, '330101199003010103', '13900000003', 150000.00, '2020-01-10', '0', 'admin', NOW(), 'admin', NOW()),
(1900004, '专项核查缺少负债样本', 100, '330101199003010104', '13900000004', 160000.00, '2020-01-10', '0', 'admin', NOW(), 'admin', NOW()),
(1900005, '专项核查缺少资产样本', 100, '330101199003010105', '13900000005', 220000.00, '2020-01-10', '0', 'admin', NOW(), 'admin', NOW()),
(1900006, '专项核查单身完整样本', 100, '330101199003010106', '13900000006', 180000.00, '2020-01-10', '0', 'admin', NOW(), 'admin', NOW());
-- 3. 新增配偶关系
INSERT INTO ccdi_staff_fmy_relation (
person_id,
relation_type,
relation_name,
gender,
birth_date,
relation_cert_type,
relation_cert_no,
mobile_phone1,
contact_address,
relation_desc,
status,
effective_date,
remark,
data_source,
is_emp_family,
is_cust_family,
created_by,
updated_by,
create_time,
update_time,
annual_income
) VALUES
('330101199003010101', '配偶', '边界配偶甲', 'F', '1991-04-01', '身份证', '330101199104010101', '13800000001', '专项核查测试地址1', '专项核查正常边界配偶', 1, NOW(), '专项核查测试数据', 'MANUAL', 1, 0, 'admin', 'admin', NOW(), NOW(), 100000.00),
('330101199003010102', '配偶', '边界配偶乙', 'F', '1991-04-01', '身份证', '330101199104010102', '13800000002', '专项核查测试地址2', '专项核查风险边界配偶', 1, NOW(), '专项核查测试数据', 'MANUAL', 1, 0, 'admin', 'admin', NOW(), NOW(), 120000.00),
('330101199003010103', '配偶', '高风险配偶', 'F', '1991-04-01', '身份证', '330101199104010103', '13800000003', '专项核查测试地址3', '专项核查高风险配偶', 1, NOW(), '专项核查测试数据', 'MANUAL', 1, 0, 'admin', 'admin', NOW(), NOW(), 90000.00),
('330101199003010104', '配偶', '负债缺失配偶', 'F', '1991-04-01', '身份证', '330101199104010104', '13800000004', '专项核查测试地址4', '专项核查缺少本人负债配偶', 1, NOW(), '专项核查测试数据', 'MANUAL', 1, 0, 'admin', 'admin', NOW(), NOW(), 140000.00),
('330101199003010105', '配偶', '资产缺失配偶', 'F', '1991-04-01', '身份证', '330101199104010105', '13800000005', '专项核查测试地址5', '专项核查缺少本人资产配偶', 1, NOW(), '专项核查测试数据', 'MANUAL', 1, 0, 'admin', 'admin', NOW(), NOW(), 130000.00);
-- 4. 新增资产明细
INSERT INTO ccdi_asset_info (
family_id,
person_id,
asset_main_type,
asset_sub_type,
asset_name,
ownership_ratio,
purchase_eval_date,
original_value,
current_value,
valuation_date,
asset_status,
remarks,
create_by,
update_by
) VALUES
('330101199003010101', '330101199003010101', '房产', '住宅', '专项核查正常边界样本-本人住宅', 100.00, '2024-01-01', 400000.00, 400000.00, '2026-03-20', '正常', '专项核查测试数据-正常边界', 'admin', 'admin'),
('330101199003010101', '330101199003010101', '金融资产', '理财', '专项核查正常边界样本-本人理财', 100.00, '2024-06-01', 200000.00, 200000.00, '2026-03-18', '正常', '专项核查测试数据-正常边界', 'admin', 'admin'),
('330101199003010101', '330101199104010101', '车辆', '家用汽车', '专项核查正常边界样本-配偶车辆', 100.00, '2024-02-01', 250000.00, 250000.00, '2026-03-17', '正常', '专项核查测试数据-正常边界', 'admin', 'admin'),
('330101199003010101', '330101199104010101', '金融资产', '基金', '专项核查正常边界样本-配偶基金', 100.00, '2024-07-01', 150000.00, 150000.00, '2026-03-16', '正常', '专项核查测试数据-正常边界', 'admin', 'admin'),
('330101199003010102', '330101199003010102', '房产', '公寓', '专项核查风险边界样本-本人公寓', 100.00, '2024-01-01', 200000.00, 200000.00, '2026-03-20', '正常', '专项核查测试数据-风险边界', 'admin', 'admin'),
('330101199003010102', '330101199104010102', '车辆', '新能源车', '专项核查风险边界样本-配偶车辆', 100.00, '2024-04-01', 300000.00, 300000.00, '2026-03-19', '正常', '专项核查测试数据-风险边界', 'admin', 'admin'),
('330101199003010103', '330101199003010103', '房产', '住宅', '专项核查高风险样本-本人住宅', 100.00, '2024-01-01', 200000.00, 200000.00, '2026-03-20', '正常', '专项核查测试数据-高风险', 'admin', 'admin'),
('330101199003010103', '330101199104010103', '金融资产', '股票', '专项核查高风险样本-配偶股票', 100.00, '2024-05-01', 300000.00, 300000.00, '2026-03-18', '正常', '专项核查测试数据-高风险', 'admin', 'admin'),
('330101199003010104', '330101199003010104', '房产', '住宅', '专项核查缺少负债样本-本人住宅', 100.00, '2024-01-01', 450000.00, 450000.00, '2026-03-20', '正常', '专项核查测试数据-缺少本人负债', 'admin', 'admin'),
('330101199003010104', '330101199104010104', '金融资产', '理财', '专项核查缺少负债样本-配偶理财', 100.00, '2024-06-01', 350000.00, 350000.00, '2026-03-18', '正常', '专项核查测试数据-缺少本人负债', 'admin', 'admin'),
('330101199003010105', '330101199104010105', '房产', '住宅', '专项核查缺少资产样本-配偶住宅', 100.00, '2024-01-01', 600000.00, 600000.00, '2026-03-20', '正常', '专项核查测试数据-缺少本人资产', 'admin', 'admin'),
('330101199003010106', '330101199003010106', '房产', '住宅', '专项核查单身完整样本-本人住宅', 100.00, '2024-01-01', 250000.00, 250000.00, '2026-03-20', '正常', '专项核查测试数据-单身完整', 'admin', 'admin'),
('330101199003010106', '330101199003010106', '金融资产', '存款', '专项核查单身完整样本-本人存款', 100.00, '2024-05-01', 150000.00, 150000.00, '2026-03-18', '正常', '专项核查测试数据-单身完整', 'admin', 'admin');
-- 5. 新增负债明细
INSERT INTO ccdi_debts_info (
person_id,
person_name,
query_date,
debt_main_type,
debt_sub_type,
creditor_type,
debt_name,
principal_balance,
debt_total_amount,
debt_status,
create_by,
update_by
) VALUES
('330101199003010101', '专项核查正常边界样本', '2026-03-24', '贷款', '房贷', '银行', '专项核查正常边界样本-本人房贷', 700000.00, 730000.00, '正常', 'admin', 'admin'),
('330101199104010101', '边界配偶甲', '2026-03-24', '贷款', '消费贷', '银行', '专项核查正常边界样本-配偶消费贷', 500000.00, 520000.00, '正常', 'admin', 'admin'),
('330101199003010102', '专项核查风险边界样本', '2026-03-24', '贷款', '经营贷', '银行', '专项核查风险边界样本-本人经营贷', 600000.00, 620000.00, '正常', 'admin', 'admin'),
('330101199003010102', '专项核查风险边界样本', '2026-03-23', '信用卡', '分期', '银行', '专项核查风险边界样本-本人信用卡分期', 100000.00, 102000.00, '正常', 'admin', 'admin'),
('330101199104010102', '边界配偶乙', '2026-03-24', '贷款', '房贷', '银行', '专项核查风险边界样本-配偶房贷', 500000.00, 530000.00, '正常', 'admin', 'admin'),
('330101199003010103', '专项核查高风险样本', '2026-03-24', '贷款', '房贷', '银行', '专项核查高风险样本-本人房贷', 900000.00, 930000.00, '正常', 'admin', 'admin'),
('330101199104010103', '高风险配偶', '2026-03-24', '贷款', '网贷', '消费金融', '专项核查高风险样本-配偶网贷', 500100.00, 510000.00, '正常', 'admin', 'admin'),
('330101199104010104', '负债缺失配偶', '2026-03-24', '贷款', '消费贷', '银行', '专项核查缺少负债样本-配偶消费贷', 200000.00, 205000.00, '正常', 'admin', 'admin'),
('330101199003010105', '专项核查缺少资产样本', '2026-03-24', '贷款', '信用贷', '银行', '专项核查缺少资产样本-本人信用贷', 250000.00, 255000.00, '正常', 'admin', 'admin'),
('330101199104010105', '资产缺失配偶', '2026-03-24', '贷款', '消费贷', '银行', '专项核查缺少资产样本-配偶消费贷', 150000.00, 152000.00, '正常', 'admin', 'admin'),
('330101199003010106', '专项核查单身完整样本', '2026-03-24', '贷款', '信用贷', '银行', '专项核查单身完整样本-本人信用贷', 180000.00, 182000.00, '正常', 'admin', 'admin'),
('330101199003010106', '专项核查单身完整样本', '2026-03-23', '信用卡', '分期', '银行', '专项核查单身完整样本-本人信用卡分期', 120000.00, 121500.00, '正常', 'admin', 'admin');
-- 6. 新增项目命中对象,使样本进入专项核查范围
INSERT INTO ccdi_bank_statement_tag_result (
project_id,
model_code,
model_name,
rule_code,
rule_name,
indicator_code,
result_type,
risk_level,
bank_statement_id,
object_type,
object_key,
group_id,
log_id,
reason_detail,
business_caliber_snapshot,
hit_value_snapshot,
create_by,
update_by,
remark
) VALUES
(51, 'ABNORMAL_TRANSACTION', '异常交易', 'LOW_INCOME_RELATIVE_LARGE_TRANSACTION', '低收入亲属大额交易', NULL, 'OBJECT', 'GENERAL', NULL, 'STAFF_ID_CARD', '330101199003010101', NULL, NULL, '专项核查测试数据-正常边界样本入围', '专项核查测试数据', '专项核查正常边界样本', 'admin', 'admin', '专项核查测试数据'),
(51, 'ABNORMAL_TRANSACTION', '异常交易', 'LOW_INCOME_RELATIVE_LARGE_TRANSACTION', '低收入亲属大额交易', NULL, 'OBJECT', 'GENERAL', NULL, 'STAFF_ID_CARD', '330101199003010102', NULL, NULL, '专项核查测试数据-风险边界样本入围', '专项核查测试数据', '专项核查风险边界样本', 'admin', 'admin', '专项核查测试数据'),
(51, 'ABNORMAL_TRANSACTION', '异常交易', 'LOW_INCOME_RELATIVE_LARGE_TRANSACTION', '低收入亲属大额交易', NULL, 'OBJECT', 'GENERAL', NULL, 'STAFF_ID_CARD', '330101199003010103', NULL, NULL, '专项核查测试数据-高风险样本入围', '专项核查测试数据', '专项核查高风险样本', 'admin', 'admin', '专项核查测试数据'),
(51, 'ABNORMAL_TRANSACTION', '异常交易', 'LOW_INCOME_RELATIVE_LARGE_TRANSACTION', '低收入亲属大额交易', NULL, 'OBJECT', 'GENERAL', NULL, 'STAFF_ID_CARD', '330101199003010104', NULL, NULL, '专项核查测试数据-缺少负债样本入围', '专项核查测试数据', '专项核查缺少负债样本', 'admin', 'admin', '专项核查测试数据'),
(51, 'ABNORMAL_TRANSACTION', '异常交易', 'LOW_INCOME_RELATIVE_LARGE_TRANSACTION', '低收入亲属大额交易', NULL, 'OBJECT', 'GENERAL', NULL, 'STAFF_ID_CARD', '330101199003010105', NULL, NULL, '专项核查测试数据-缺少资产样本入围', '专项核查测试数据', '专项核查缺少资产样本', 'admin', 'admin', '专项核查测试数据'),
(51, 'ABNORMAL_TRANSACTION', '异常交易', 'LOW_INCOME_RELATIVE_LARGE_TRANSACTION', '低收入亲属大额交易', NULL, 'OBJECT', 'GENERAL', NULL, 'STAFF_ID_CARD', '330101199003010106', NULL, NULL, '专项核查测试数据-单身完整样本入围', '专项核查测试数据', '专项核查单身完整样本', 'admin', 'admin', '专项核查测试数据');
COMMIT;