新增项目证据库一期功能
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
package com.ruoyi.ccdi.project.controller;
|
||||||
|
|
||||||
|
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceQueryDTO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceSaveDTO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiEvidenceVO;
|
||||||
|
import com.ruoyi.ccdi.project.service.ICcdiEvidenceService;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
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.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目证据Controller
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/ccdi/evidence")
|
||||||
|
@Tag(name = "项目证据")
|
||||||
|
public class CcdiEvidenceController extends BaseController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ICcdiEvidenceService evidenceService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存证据
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "保存证据")
|
||||||
|
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||||
|
public AjaxResult saveEvidence(@Validated @RequestBody CcdiEvidenceSaveDTO dto) {
|
||||||
|
CcdiEvidenceVO vo = evidenceService.saveEvidence(dto, SecurityUtils.getUsername());
|
||||||
|
return AjaxResult.success("证据入库成功", vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询项目证据列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "查询项目证据列表")
|
||||||
|
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||||
|
public AjaxResult listEvidence(CcdiEvidenceQueryDTO queryDTO) {
|
||||||
|
List<CcdiEvidenceVO> list = evidenceService.listEvidence(queryDTO);
|
||||||
|
return AjaxResult.success(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询证据详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{evidenceId}")
|
||||||
|
@Operation(summary = "查询证据详情")
|
||||||
|
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||||
|
public AjaxResult getEvidence(@PathVariable Long evidenceId) {
|
||||||
|
CcdiEvidenceVO vo = evidenceService.getEvidence(evidenceId);
|
||||||
|
return AjaxResult.success(vo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目证据查询入参
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiEvidenceQueryDTO {
|
||||||
|
|
||||||
|
/** 项目ID */
|
||||||
|
private Long projectId;
|
||||||
|
|
||||||
|
/** 证据类型:FLOW/MODEL/ASSET */
|
||||||
|
private String evidenceType;
|
||||||
|
|
||||||
|
/** 关键词:姓名、标题、摘要、证据编号 */
|
||||||
|
private String keyword;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存项目证据入参
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiEvidenceSaveDTO {
|
||||||
|
|
||||||
|
/** 项目ID */
|
||||||
|
@NotNull(message = "项目ID不能为空")
|
||||||
|
private Long projectId;
|
||||||
|
|
||||||
|
/** 证据类型:FLOW/MODEL/ASSET */
|
||||||
|
@NotBlank(message = "证据类型不能为空")
|
||||||
|
private String evidenceType;
|
||||||
|
|
||||||
|
/** 关联人员姓名 */
|
||||||
|
@NotBlank(message = "关联人员不能为空")
|
||||||
|
private String relatedPersonName;
|
||||||
|
|
||||||
|
/** 关联人员标识 */
|
||||||
|
private String relatedPersonId;
|
||||||
|
|
||||||
|
/** 证据标题 */
|
||||||
|
@NotBlank(message = "证据标题不能为空")
|
||||||
|
private String evidenceTitle;
|
||||||
|
|
||||||
|
/** 证据摘要 */
|
||||||
|
@NotBlank(message = "证据摘要不能为空")
|
||||||
|
private String evidenceSummary;
|
||||||
|
|
||||||
|
/** 来源类型 */
|
||||||
|
@NotBlank(message = "来源类型不能为空")
|
||||||
|
private String sourceType;
|
||||||
|
|
||||||
|
/** 来源记录ID */
|
||||||
|
private String sourceRecordId;
|
||||||
|
|
||||||
|
/** 来源页面名称 */
|
||||||
|
private String sourcePage;
|
||||||
|
|
||||||
|
/** 证据快照JSON */
|
||||||
|
private String snapshotJson;
|
||||||
|
|
||||||
|
/** 确认理由/备注 */
|
||||||
|
@NotBlank(message = "确认理由/备注不能为空")
|
||||||
|
private String confirmReason;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目证据对象 ccdi_evidence
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("ccdi_evidence")
|
||||||
|
public class CcdiEvidence implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 证据ID */
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long evidenceId;
|
||||||
|
|
||||||
|
/** 项目ID */
|
||||||
|
private Long projectId;
|
||||||
|
|
||||||
|
/** 证据类型:FLOW/MODEL/ASSET */
|
||||||
|
private String evidenceType;
|
||||||
|
|
||||||
|
/** 关联人员姓名 */
|
||||||
|
private String relatedPersonName;
|
||||||
|
|
||||||
|
/** 关联人员标识,优先存身份证号或员工号 */
|
||||||
|
private String relatedPersonId;
|
||||||
|
|
||||||
|
/** 证据标题 */
|
||||||
|
private String evidenceTitle;
|
||||||
|
|
||||||
|
/** 证据摘要 */
|
||||||
|
private String evidenceSummary;
|
||||||
|
|
||||||
|
/** 来源类型:BANK_STATEMENT/MODEL_DETAIL/ASSET_DETAIL */
|
||||||
|
private String sourceType;
|
||||||
|
|
||||||
|
/** 来源记录ID */
|
||||||
|
private String sourceRecordId;
|
||||||
|
|
||||||
|
/** 来源页面名称 */
|
||||||
|
private String sourcePage;
|
||||||
|
|
||||||
|
/** 证据快照JSON */
|
||||||
|
private String snapshotJson;
|
||||||
|
|
||||||
|
/** 确认理由/备注 */
|
||||||
|
private String confirmReason;
|
||||||
|
|
||||||
|
/** 确认人 */
|
||||||
|
private String confirmBy;
|
||||||
|
|
||||||
|
/** 确认时间 */
|
||||||
|
private Date confirmTime;
|
||||||
|
|
||||||
|
/** 创建者 */
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private String createBy;
|
||||||
|
|
||||||
|
/** 创建时间 */
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/** 更新者 */
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private String updateBy;
|
||||||
|
|
||||||
|
/** 更新时间 */
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目证据返回对象
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiEvidenceVO {
|
||||||
|
|
||||||
|
/** 证据ID */
|
||||||
|
private Long evidenceId;
|
||||||
|
|
||||||
|
/** 项目ID */
|
||||||
|
private Long projectId;
|
||||||
|
|
||||||
|
/** 证据类型:FLOW/MODEL/ASSET */
|
||||||
|
private String evidenceType;
|
||||||
|
|
||||||
|
/** 关联人员姓名 */
|
||||||
|
private String relatedPersonName;
|
||||||
|
|
||||||
|
/** 关联人员标识 */
|
||||||
|
private String relatedPersonId;
|
||||||
|
|
||||||
|
/** 证据标题 */
|
||||||
|
private String evidenceTitle;
|
||||||
|
|
||||||
|
/** 证据摘要 */
|
||||||
|
private String evidenceSummary;
|
||||||
|
|
||||||
|
/** 来源类型 */
|
||||||
|
private String sourceType;
|
||||||
|
|
||||||
|
/** 来源记录ID */
|
||||||
|
private String sourceRecordId;
|
||||||
|
|
||||||
|
/** 来源页面名称 */
|
||||||
|
private String sourcePage;
|
||||||
|
|
||||||
|
/** 证据快照JSON */
|
||||||
|
private String snapshotJson;
|
||||||
|
|
||||||
|
/** 确认理由/备注 */
|
||||||
|
private String confirmReason;
|
||||||
|
|
||||||
|
/** 确认人 */
|
||||||
|
private String confirmBy;
|
||||||
|
|
||||||
|
/** 确认时间 */
|
||||||
|
private Date confirmTime;
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class CcdiProjectPersonAnalysisObjectRecordVO {
|
public class CcdiProjectPersonAnalysisObjectRecordVO {
|
||||||
|
|
||||||
|
private String modelCode;
|
||||||
|
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
private String subtitle;
|
private String subtitle;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ruoyi.ccdi.project.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.ruoyi.ccdi.project.domain.entity.CcdiEvidence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目证据Mapper接口
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
public interface CcdiEvidenceMapper extends BaseMapper<CcdiEvidence> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.ruoyi.ccdi.project.service;
|
||||||
|
|
||||||
|
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceQueryDTO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceSaveDTO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiEvidenceVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目证据Service接口
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
public interface ICcdiEvidenceService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存证据
|
||||||
|
*
|
||||||
|
* @param dto 保存入参
|
||||||
|
* @param operator 操作人
|
||||||
|
* @return 证据
|
||||||
|
*/
|
||||||
|
CcdiEvidenceVO saveEvidence(CcdiEvidenceSaveDTO dto, String operator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询项目证据列表
|
||||||
|
*
|
||||||
|
* @param queryDTO 查询入参
|
||||||
|
* @return 证据列表
|
||||||
|
*/
|
||||||
|
List<CcdiEvidenceVO> listEvidence(CcdiEvidenceQueryDTO queryDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询证据详情
|
||||||
|
*
|
||||||
|
* @param evidenceId 证据ID
|
||||||
|
* @return 证据
|
||||||
|
*/
|
||||||
|
CcdiEvidenceVO getEvidence(Long evidenceId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.ruoyi.ccdi.project.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceQueryDTO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceSaveDTO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.entity.CcdiEvidence;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiEvidenceVO;
|
||||||
|
import com.ruoyi.ccdi.project.mapper.CcdiEvidenceMapper;
|
||||||
|
import com.ruoyi.ccdi.project.service.ICcdiEvidenceService;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目证据Service实现类
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class CcdiEvidenceServiceImpl implements ICcdiEvidenceService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiEvidenceMapper evidenceMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public CcdiEvidenceVO saveEvidence(CcdiEvidenceSaveDTO dto, String operator) {
|
||||||
|
CcdiEvidence evidence = new CcdiEvidence();
|
||||||
|
BeanUtils.copyProperties(dto, evidence);
|
||||||
|
evidence.setConfirmBy(operator);
|
||||||
|
evidence.setConfirmTime(new Date());
|
||||||
|
evidenceMapper.insert(evidence);
|
||||||
|
return toVO(evidence);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CcdiEvidenceVO> listEvidence(CcdiEvidenceQueryDTO queryDTO) {
|
||||||
|
if (queryDTO.getProjectId() == null) {
|
||||||
|
throw new ServiceException("项目ID不能为空");
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<CcdiEvidence> wrapper = new LambdaQueryWrapper<CcdiEvidence>()
|
||||||
|
.eq(CcdiEvidence::getProjectId, queryDTO.getProjectId())
|
||||||
|
.orderByDesc(CcdiEvidence::getConfirmTime)
|
||||||
|
.orderByDesc(CcdiEvidence::getEvidenceId);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(queryDTO.getEvidenceType())) {
|
||||||
|
wrapper.eq(CcdiEvidence::getEvidenceType, queryDTO.getEvidenceType());
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(queryDTO.getKeyword())) {
|
||||||
|
String keyword = queryDTO.getKeyword().trim();
|
||||||
|
wrapper.and(item -> item
|
||||||
|
.like(CcdiEvidence::getRelatedPersonName, keyword)
|
||||||
|
.or()
|
||||||
|
.like(CcdiEvidence::getRelatedPersonId, keyword)
|
||||||
|
.or()
|
||||||
|
.like(CcdiEvidence::getEvidenceTitle, keyword)
|
||||||
|
.or()
|
||||||
|
.like(CcdiEvidence::getEvidenceSummary, keyword)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return evidenceMapper.selectList(wrapper).stream().map(this::toVO).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CcdiEvidenceVO getEvidence(Long evidenceId) {
|
||||||
|
CcdiEvidence evidence = evidenceMapper.selectById(evidenceId);
|
||||||
|
if (evidence == null) {
|
||||||
|
throw new ServiceException("证据不存在");
|
||||||
|
}
|
||||||
|
return toVO(evidence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiEvidenceVO toVO(CcdiEvidence evidence) {
|
||||||
|
CcdiEvidenceVO vo = new CcdiEvidenceVO();
|
||||||
|
BeanUtils.copyProperties(evidence, vo);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -741,6 +741,7 @@
|
|||||||
|
|
||||||
<select id="selectPersonAnalysisObjectRows" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO">
|
<select id="selectPersonAnalysisObjectRows" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO">
|
||||||
select
|
select
|
||||||
|
max(tr.model_code) as modelCode,
|
||||||
coalesce(max(staff.name), max(relation.relation_name), max(tr.object_key), max(tr.object_type)) as title,
|
coalesce(max(staff.name), max(relation.relation_name), max(tr.object_key), max(tr.object_type)) as title,
|
||||||
max(case
|
max(case
|
||||||
when tr.object_type = 'STAFF_ID_CARD' then '员工对象'
|
when tr.object_type = 'STAFF_ID_CARD' then '员工对象'
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 证据库最小改造验证清单
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
|
||||||
|
- 流水证据:流水详情中小号「加入证据库」按钮、确认弹窗、保存入库。
|
||||||
|
- 模型证据:模型详情异常对象卡片中小号「加入证据库」按钮、确认弹窗、保存入库。
|
||||||
|
- 资产证据:资产详情中小号「加入证据库」按钮、确认弹窗、保存入库。
|
||||||
|
- 证据线索:项目详情右上角小号「证据线索」入口、右侧抽屉列表、搜索框基础展示。
|
||||||
|
|
||||||
|
## 版式约束
|
||||||
|
|
||||||
|
- 不新增独立证据库页面。
|
||||||
|
- 不改变原详情页主体字段、表格列、字号体系和业务阅读顺序。
|
||||||
|
- 「加入证据库」只能作为低频辅助按钮出现,使用 mini 尺寸、弱边框、弱背景。
|
||||||
|
- 「证据线索」只能作为轻入口和右侧抽屉,不遮挡或重排项目详情主体内容。
|
||||||
|
|
||||||
|
## 功能验证
|
||||||
|
|
||||||
|
- 项目详情页能正常打开,顶部「证据线索」按钮可打开抽屉。
|
||||||
|
- 抽屉无证据时展示空状态,有证据时展示编号、类型、关联人员、摘要、来源、确认人、备注。
|
||||||
|
- 模型详情点击「加入证据库」后,弹窗自动带出证据类型、关联人员、证据摘要。
|
||||||
|
- 流水详情点击「加入证据库」后,弹窗自动带出流水证据摘要,`source_record_id` 使用 `md5(本方账号+本方名称+对方账号+对方名称+交易时间+金额+摘要)`。
|
||||||
|
- 资产详情点击「加入证据库」后,弹窗自动带出资产证据摘要。
|
||||||
|
- 模型证据 `source_record_id` 使用 `md5(人员身份证+模型编码)`,缺少人员身份证或模型编码时不允许入库。
|
||||||
|
- 资产证据 `source_record_id` 使用 `md5(人员身份证+资产字段)`,当前资产负债聚合口径的资产字段为家庭总收入、家庭总负债、家庭总资产、风险等级编码。
|
||||||
|
- 确认理由为空时不能提交。
|
||||||
|
- 填写确认理由后可以提交,提交成功后自动打开或刷新证据线索抽屉。
|
||||||
|
- 保存后的证据落库到 `ccdi_evidence`。
|
||||||
|
|
||||||
|
## 技术验证
|
||||||
|
|
||||||
|
- 后端 `ccdi-project` 编译通过。
|
||||||
|
- 前端 `npm run build:prod` 通过。
|
||||||
|
- 数据库表 `ccdi_evidence` 存在。
|
||||||
|
- 流水证据 `source_record_id` 不依赖 `statementId/bankStatementId`,应为 32 位 MD5 指纹。
|
||||||
|
- 模型证据、资产证据的 `source_record_id` 均不拼接项目 ID,项目归属仅存 `project_id` 字段。
|
||||||
|
- 页面控制台不出现由本次改造引入的明显错误。
|
||||||
|
- 不提交或误动无关文件。
|
||||||
|
|
||||||
|
## 本期不做
|
||||||
|
|
||||||
|
- 证据卡片「查看详情」真实跳转原记录。
|
||||||
|
- 跨项目引用/复用 UI。
|
||||||
|
- 重复证据拦截。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 证据库最小改造验证记录
|
||||||
|
|
||||||
|
## 验证时间
|
||||||
|
|
||||||
|
2026-04-21
|
||||||
|
|
||||||
|
## 验证环境
|
||||||
|
|
||||||
|
- 前端:`http://localhost:62319`
|
||||||
|
- 后端:`http://localhost:62318`
|
||||||
|
- 项目:`test`
|
||||||
|
- 项目 ID:`90337`
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
| 验证项 | 结果 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 后端编译 | 通过 | `mvn -pl ccdi-project -am compile -DskipTests` 成功 |
|
||||||
|
| 前端构建 | 通过 | `npm run build:prod` 成功,仅存在原有包体积 warning |
|
||||||
|
| 数据库表 | 通过 | `ccdi_evidence` 已存在 |
|
||||||
|
| 模型证据入库 | 通过 | 模型详情小号「加入证据库」可打开弹窗并保存,生成 `EV-001` |
|
||||||
|
| 流水证据入库 | 通过 | 流水详情小号「加入证据库」可打开弹窗并保存,当前代码已改为使用 32 位 MD5 指纹作为 `source_record_id` |
|
||||||
|
| 资产证据入库 | 通过 | 资产详情小号「加入证据库」可打开弹窗并保存,已验证旧规则测试数据 `EV-003` 与新指纹规则测试数据 `EV-004` |
|
||||||
|
| 证据线索抽屉 | 通过 | 抽屉展示三类证据,包含编号、类型、关联人员、摘要、来源、确认人、备注 |
|
||||||
|
| 前端控制台 | 通过 | 验证后未发现 error/warn |
|
||||||
|
| 模型/资产来源指纹更新 | 通过 | 已重启后端并通过 MCP 页面验证:模型证据、资产证据均可打开确认弹窗,本次未确认入库,避免新增测试数据 |
|
||||||
|
| 证据抽屉跳转入口 | 通过 | 本期不做原记录跳转,已移除抽屉卡片中的「查看流水详情」「查看模型详情」「查看资产详情」按钮 |
|
||||||
|
|
||||||
|
## 落库核对
|
||||||
|
|
||||||
|
项目 `90337` 当前证据数:
|
||||||
|
|
||||||
|
| 类型 | 数量 |
|
||||||
|
| --- | ---: |
|
||||||
|
| FLOW | 1 |
|
||||||
|
| MODEL | 1 |
|
||||||
|
| ASSET | 2 |
|
||||||
|
| 合计 | 4 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 本次验证产生了测试证据数据,如正式交付前需要干净环境,可按项目 ID 清理。
|
||||||
|
- 历史已保存的测试证据可能保留旧来源标识,新保存的流水、模型、资产证据会按当前规则生成 MD5 指纹。
|
||||||
|
- 当前代码已将模型证据来源标识改为 `md5(人员身份证+模型编码)`,资产证据来源标识改为 `md5(人员身份证+资产字段)`,均不拼接项目 ID。
|
||||||
|
- 为让模型详情前端拿到模型编码,后端仅补充返回 `modelCode` 字段,不涉及表结构和接口路径变更。
|
||||||
|
|
||||||
|
## 后续边界
|
||||||
|
|
||||||
|
- 证据卡片「查看详情」本期不做真实跳转,当前抽屉不展示跳转按钮;后续如要定位原记录,可基于 `source_type`、`source_record_id`、`snapshot_json` 增加跳转逻辑。
|
||||||
|
- 跨项目引用/复用 UI 本期不做;当前 `source_record_id` 已按不拼接项目 ID 的规则生成,后续具备按同一来源指纹做跨项目比对的基础。
|
||||||
|
- 重复证据拦截本期不做;当前允许同一项目内重复确认,后续可按 `project_id + evidence_type + source_type + source_record_id` 增加唯一性提示或软拦截。
|
||||||
27
ruoyi-ui/src/api/ccdiEvidence.js
Normal file
27
ruoyi-ui/src/api/ccdiEvidence.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 保存证据
|
||||||
|
export function saveEvidence(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ccdi/evidence',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询项目证据列表
|
||||||
|
export function listEvidence(params) {
|
||||||
|
return request({
|
||||||
|
url: '/ccdi/evidence/list',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询证据详情
|
||||||
|
export function getEvidence(evidenceId) {
|
||||||
|
return request({
|
||||||
|
url: '/ccdi/evidence/' + evidenceId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
78
ruoyi-ui/src/utils/ccdiEvidence.js
Normal file
78
ruoyi-ui/src/utils/ccdiEvidence.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import md5 from "@/utils/md5";
|
||||||
|
|
||||||
|
export const FLOW_EVIDENCE_FINGERPRINT_RULE =
|
||||||
|
"md5(leAccountNo+leAccountName+customerAccountNo+customerAccountName+trxDate+displayAmount+userMemo)";
|
||||||
|
|
||||||
|
export const MODEL_EVIDENCE_FINGERPRINT_RULE = "md5(personIdCard+modelCode)";
|
||||||
|
|
||||||
|
export const ASSET_EVIDENCE_FINGERPRINT_RULE =
|
||||||
|
"md5(staffIdCard+totalIncome+totalDebt+totalAsset+riskLevelCode)";
|
||||||
|
|
||||||
|
function normalizeFingerprintValue(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCounterpartyName(detail) {
|
||||||
|
return detail.customerAccountName || detail.customerName || detail.counterpartyName || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFlowEvidenceFingerprintSource(detail = {}) {
|
||||||
|
return [
|
||||||
|
detail.leAccountNo,
|
||||||
|
detail.leAccountName,
|
||||||
|
detail.customerAccountNo,
|
||||||
|
resolveCounterpartyName(detail),
|
||||||
|
detail.trxDate,
|
||||||
|
detail.displayAmount,
|
||||||
|
detail.userMemo,
|
||||||
|
]
|
||||||
|
.map(normalizeFingerprintValue)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFlowEvidenceFingerprint(detail = {}) {
|
||||||
|
const source = buildFlowEvidenceFingerprintSource(detail);
|
||||||
|
return source ? md5(source) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFlowEvidenceSnapshot(detail = {}) {
|
||||||
|
const evidenceFingerprint = buildFlowEvidenceFingerprint(detail);
|
||||||
|
return {
|
||||||
|
...detail,
|
||||||
|
evidenceFingerprint,
|
||||||
|
evidenceFingerprintRule: FLOW_EVIDENCE_FINGERPRINT_RULE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildModelEvidenceFingerprint(personIdCard, modelCode) {
|
||||||
|
const idCard = normalizeFingerprintValue(personIdCard);
|
||||||
|
const code = normalizeFingerprintValue(modelCode);
|
||||||
|
return idCard && code ? md5(idCard + code) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssetEvidenceFingerprint(row = {}) {
|
||||||
|
const idCard = normalizeFingerprintValue(row.staffIdCard);
|
||||||
|
const assetSource = [
|
||||||
|
row.totalIncome,
|
||||||
|
row.totalDebt,
|
||||||
|
row.totalAsset,
|
||||||
|
row.riskLevelCode,
|
||||||
|
]
|
||||||
|
.map(normalizeFingerprintValue)
|
||||||
|
.join("");
|
||||||
|
return idCard && assetSource ? md5(idCard + assetSource) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssetEvidenceSnapshot(row = {}, detail = {}, summary = {}) {
|
||||||
|
const evidenceFingerprint = buildAssetEvidenceFingerprint(row);
|
||||||
|
return {
|
||||||
|
row,
|
||||||
|
detail,
|
||||||
|
summary,
|
||||||
|
evidenceFingerprint,
|
||||||
|
evidenceFingerprintRule: ASSET_EVIDENCE_FINGERPRINT_RULE,
|
||||||
|
};
|
||||||
|
}
|
||||||
161
ruoyi-ui/src/utils/md5.js
Normal file
161
ruoyi-ui/src/utils/md5.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
function safeAdd(x, y) {
|
||||||
|
const lsw = (x & 0xffff) + (y & 0xffff);
|
||||||
|
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||||
|
return (msw << 16) | (lsw & 0xffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateLeft(num, cnt) {
|
||||||
|
return (num << cnt) | (num >>> (32 - cnt));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmn(q, a, b, x, s, t) {
|
||||||
|
return safeAdd(rotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ff(a, b, c, d, x, s, t) {
|
||||||
|
return cmn((b & c) | (~b & d), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gg(a, b, c, d, x, s, t) {
|
||||||
|
return cmn((b & d) | (c & ~d), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hh(a, b, c, d, x, s, t) {
|
||||||
|
return cmn(b ^ c ^ d, a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ii(a, b, c, d, x, s, t) {
|
||||||
|
return cmn(c ^ (b | ~d), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wordsToRaw(input) {
|
||||||
|
let output = "";
|
||||||
|
for (let i = 0; i < input.length * 32; i += 8) {
|
||||||
|
output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rawToWords(input) {
|
||||||
|
const output = [];
|
||||||
|
output[(input.length >> 2) - 1] = undefined;
|
||||||
|
for (let i = 0; i < output.length; i++) {
|
||||||
|
output[i] = 0;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < input.length * 8; i += 8) {
|
||||||
|
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculate(words, len) {
|
||||||
|
words[len >> 5] |= 0x80 << (len % 32);
|
||||||
|
words[(((len + 64) >>> 9) << 4) + 14] = len;
|
||||||
|
|
||||||
|
let a = 1732584193;
|
||||||
|
let b = -271733879;
|
||||||
|
let c = -1732584194;
|
||||||
|
let d = 271733878;
|
||||||
|
|
||||||
|
for (let i = 0; i < words.length; i += 16) {
|
||||||
|
const olda = a;
|
||||||
|
const oldb = b;
|
||||||
|
const oldc = c;
|
||||||
|
const oldd = d;
|
||||||
|
|
||||||
|
a = ff(a, b, c, d, words[i], 7, -680876936);
|
||||||
|
d = ff(d, a, b, c, words[i + 1], 12, -389564586);
|
||||||
|
c = ff(c, d, a, b, words[i + 2], 17, 606105819);
|
||||||
|
b = ff(b, c, d, a, words[i + 3], 22, -1044525330);
|
||||||
|
a = ff(a, b, c, d, words[i + 4], 7, -176418897);
|
||||||
|
d = ff(d, a, b, c, words[i + 5], 12, 1200080426);
|
||||||
|
c = ff(c, d, a, b, words[i + 6], 17, -1473231341);
|
||||||
|
b = ff(b, c, d, a, words[i + 7], 22, -45705983);
|
||||||
|
a = ff(a, b, c, d, words[i + 8], 7, 1770035416);
|
||||||
|
d = ff(d, a, b, c, words[i + 9], 12, -1958414417);
|
||||||
|
c = ff(c, d, a, b, words[i + 10], 17, -42063);
|
||||||
|
b = ff(b, c, d, a, words[i + 11], 22, -1990404162);
|
||||||
|
a = ff(a, b, c, d, words[i + 12], 7, 1804603682);
|
||||||
|
d = ff(d, a, b, c, words[i + 13], 12, -40341101);
|
||||||
|
c = ff(c, d, a, b, words[i + 14], 17, -1502002290);
|
||||||
|
b = ff(b, c, d, a, words[i + 15], 22, 1236535329);
|
||||||
|
|
||||||
|
a = gg(a, b, c, d, words[i + 1], 5, -165796510);
|
||||||
|
d = gg(d, a, b, c, words[i + 6], 9, -1069501632);
|
||||||
|
c = gg(c, d, a, b, words[i + 11], 14, 643717713);
|
||||||
|
b = gg(b, c, d, a, words[i], 20, -373897302);
|
||||||
|
a = gg(a, b, c, d, words[i + 5], 5, -701558691);
|
||||||
|
d = gg(d, a, b, c, words[i + 10], 9, 38016083);
|
||||||
|
c = gg(c, d, a, b, words[i + 15], 14, -660478335);
|
||||||
|
b = gg(b, c, d, a, words[i + 4], 20, -405537848);
|
||||||
|
a = gg(a, b, c, d, words[i + 9], 5, 568446438);
|
||||||
|
d = gg(d, a, b, c, words[i + 14], 9, -1019803690);
|
||||||
|
c = gg(c, d, a, b, words[i + 3], 14, -187363961);
|
||||||
|
b = gg(b, c, d, a, words[i + 8], 20, 1163531501);
|
||||||
|
a = gg(a, b, c, d, words[i + 13], 5, -1444681467);
|
||||||
|
d = gg(d, a, b, c, words[i + 2], 9, -51403784);
|
||||||
|
c = gg(c, d, a, b, words[i + 7], 14, 1735328473);
|
||||||
|
b = gg(b, c, d, a, words[i + 12], 20, -1926607734);
|
||||||
|
|
||||||
|
a = hh(a, b, c, d, words[i + 5], 4, -378558);
|
||||||
|
d = hh(d, a, b, c, words[i + 8], 11, -2022574463);
|
||||||
|
c = hh(c, d, a, b, words[i + 11], 16, 1839030562);
|
||||||
|
b = hh(b, c, d, a, words[i + 14], 23, -35309556);
|
||||||
|
a = hh(a, b, c, d, words[i + 1], 4, -1530992060);
|
||||||
|
d = hh(d, a, b, c, words[i + 4], 11, 1272893353);
|
||||||
|
c = hh(c, d, a, b, words[i + 7], 16, -155497632);
|
||||||
|
b = hh(b, c, d, a, words[i + 10], 23, -1094730640);
|
||||||
|
a = hh(a, b, c, d, words[i + 13], 4, 681279174);
|
||||||
|
d = hh(d, a, b, c, words[i], 11, -358537222);
|
||||||
|
c = hh(c, d, a, b, words[i + 3], 16, -722521979);
|
||||||
|
b = hh(b, c, d, a, words[i + 6], 23, 76029189);
|
||||||
|
a = hh(a, b, c, d, words[i + 9], 4, -640364487);
|
||||||
|
d = hh(d, a, b, c, words[i + 12], 11, -421815835);
|
||||||
|
c = hh(c, d, a, b, words[i + 15], 16, 530742520);
|
||||||
|
b = hh(b, c, d, a, words[i + 2], 23, -995338651);
|
||||||
|
|
||||||
|
a = ii(a, b, c, d, words[i], 6, -198630844);
|
||||||
|
d = ii(d, a, b, c, words[i + 7], 10, 1126891415);
|
||||||
|
c = ii(c, d, a, b, words[i + 14], 15, -1416354905);
|
||||||
|
b = ii(b, c, d, a, words[i + 5], 21, -57434055);
|
||||||
|
a = ii(a, b, c, d, words[i + 12], 6, 1700485571);
|
||||||
|
d = ii(d, a, b, c, words[i + 3], 10, -1894986606);
|
||||||
|
c = ii(c, d, a, b, words[i + 10], 15, -1051523);
|
||||||
|
b = ii(b, c, d, a, words[i + 1], 21, -2054922799);
|
||||||
|
a = ii(a, b, c, d, words[i + 8], 6, 1873313359);
|
||||||
|
d = ii(d, a, b, c, words[i + 15], 10, -30611744);
|
||||||
|
c = ii(c, d, a, b, words[i + 6], 15, -1560198380);
|
||||||
|
b = ii(b, c, d, a, words[i + 13], 21, 1309151649);
|
||||||
|
a = ii(a, b, c, d, words[i + 4], 6, -145523070);
|
||||||
|
d = ii(d, a, b, c, words[i + 11], 10, -1120210379);
|
||||||
|
c = ii(c, d, a, b, words[i + 2], 15, 718787259);
|
||||||
|
b = ii(b, c, d, a, words[i + 9], 21, -343485551);
|
||||||
|
|
||||||
|
a = safeAdd(a, olda);
|
||||||
|
b = safeAdd(b, oldb);
|
||||||
|
c = safeAdd(c, oldc);
|
||||||
|
d = safeAdd(d, oldd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [a, b, c, d];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rawToHex(input) {
|
||||||
|
const hex = "0123456789abcdef";
|
||||||
|
let output = "";
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const x = input.charCodeAt(i);
|
||||||
|
output += hex.charAt((x >>> 4) & 0x0f) + hex.charAt(x & 0x0f);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUtf8Raw(input) {
|
||||||
|
return unescape(encodeURIComponent(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function md5(input) {
|
||||||
|
const value = input === null || input === undefined ? "" : String(input);
|
||||||
|
const raw = toUtf8Raw(value);
|
||||||
|
return rawToHex(wordsToRaw(calculate(rawToWords(raw), raw.length * 8)));
|
||||||
|
}
|
||||||
@@ -283,10 +283,23 @@
|
|||||||
:visible.sync="detailVisible"
|
:visible.sync="detailVisible"
|
||||||
append-to-body
|
append-to-body
|
||||||
custom-class="detail-dialog"
|
custom-class="detail-dialog"
|
||||||
title="流水详情"
|
|
||||||
width="980px"
|
width="980px"
|
||||||
@close="closeDetailDialog"
|
@close="closeDetailDialog"
|
||||||
>
|
>
|
||||||
|
<template slot="title">
|
||||||
|
<div class="detail-dialog-title">
|
||||||
|
<span>流水详情</span>
|
||||||
|
<el-button
|
||||||
|
class="evidence-corner-btn"
|
||||||
|
size="mini"
|
||||||
|
plain
|
||||||
|
:disabled="detailLoading || !buildFlowEvidenceFingerprint(detailData)"
|
||||||
|
@click="handleAddEvidence"
|
||||||
|
>
|
||||||
|
加入证据库
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div v-loading="detailLoading" class="detail-dialog-body">
|
<div v-loading="detailLoading" class="detail-dialog-body">
|
||||||
<div class="detail-overview-grid">
|
<div class="detail-overview-grid">
|
||||||
<div class="detail-field">
|
<div class="detail-field">
|
||||||
@@ -394,6 +407,7 @@ import {
|
|||||||
getBankStatementOptions,
|
getBankStatementOptions,
|
||||||
getBankStatementDetail,
|
getBankStatementDetail,
|
||||||
} from "@/api/ccdiProjectBankStatement";
|
} from "@/api/ccdiProjectBankStatement";
|
||||||
|
import { buildFlowEvidenceFingerprint, buildFlowEvidenceSnapshot } from "@/utils/ccdiEvidence";
|
||||||
|
|
||||||
const TAB_MAP = {
|
const TAB_MAP = {
|
||||||
all: "all",
|
all: "all",
|
||||||
@@ -518,6 +532,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
buildFlowEvidenceFingerprint,
|
||||||
async getList() {
|
async getList() {
|
||||||
this.syncProjectId();
|
this.syncProjectId();
|
||||||
if (!this.queryParams.projectId) {
|
if (!this.queryParams.projectId) {
|
||||||
@@ -638,6 +653,29 @@ export default {
|
|||||||
this.detailLoading = false;
|
this.detailLoading = false;
|
||||||
this.detailData = createEmptyDetailData();
|
this.detailData = createEmptyDetailData();
|
||||||
},
|
},
|
||||||
|
handleAddEvidence() {
|
||||||
|
const detail = this.detailData || {};
|
||||||
|
const sourceRecordId = buildFlowEvidenceFingerprint(detail);
|
||||||
|
const amountText = this.formatSignedAmount(detail.displayAmount);
|
||||||
|
const counterparty = this.formatCounterpartyName(detail);
|
||||||
|
const hitTagText = Array.isArray(detail.hitTags) && detail.hitTags.length
|
||||||
|
? `,命中${detail.hitTags.map((tag) => tag.ruleName).filter(Boolean).join("、")}标签`
|
||||||
|
: "";
|
||||||
|
this.$emit("evidence-confirm", {
|
||||||
|
evidenceType: "FLOW",
|
||||||
|
relatedPersonName: this.resolveFlowRelatedPerson(detail),
|
||||||
|
relatedPersonId: detail.cretNo || "",
|
||||||
|
evidenceTitle: `${this.resolveFlowRelatedPerson(detail)} / ${this.formatField(detail.leAccountNo)}`,
|
||||||
|
evidenceSummary: `${this.formatField(detail.trxDate)},${this.resolveFlowRelatedPerson(detail)}账户与${counterparty}发生交易,金额${amountText}${hitTagText}。`,
|
||||||
|
sourceType: "BANK_STATEMENT",
|
||||||
|
sourceRecordId,
|
||||||
|
sourcePage: "流水详情",
|
||||||
|
snapshotJson: JSON.stringify(buildFlowEvidenceSnapshot(detail)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolveFlowRelatedPerson(detail) {
|
||||||
|
return this.formatField(detail.leAccountName) === "-" ? "关联人员" : this.formatField(detail.leAccountName);
|
||||||
|
},
|
||||||
handleExport() {
|
handleExport() {
|
||||||
if (this.total === 0) {
|
if (this.total === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -751,6 +789,26 @@ export default {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-dialog-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-corner-btn {
|
||||||
|
padding: 4px 9px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b7fb8;
|
||||||
|
border-color: #d6e4f7;
|
||||||
|
background: #f8fbff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #2474e8;
|
||||||
|
border-color: #9fc3ff;
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.shell-sidebar,
|
.shell-sidebar,
|
||||||
.shell-main {
|
.shell-main {
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #ebeef5;
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="dialogVisible"
|
||||||
|
append-to-body
|
||||||
|
title="确认为证据"
|
||||||
|
width="560px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<el-form label-position="top" class="evidence-confirm-form">
|
||||||
|
<el-form-item label="证据类型">
|
||||||
|
<el-input :value="typeLabel" readonly />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="关联人员">
|
||||||
|
<el-input :value="payload.relatedPersonName || '-'" readonly />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="证据摘要">
|
||||||
|
<el-input
|
||||||
|
:value="payload.evidenceSummary || '-'"
|
||||||
|
readonly
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认理由/备注" required>
|
||||||
|
<el-input
|
||||||
|
v-model.trim="confirmReason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请填写为什么将该详情确认为证据"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div slot="footer">
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||||
|
确认入库
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { saveEvidence } from "@/api/ccdiEvidence";
|
||||||
|
|
||||||
|
const TYPE_LABEL_MAP = {
|
||||||
|
FLOW: "流水证据",
|
||||||
|
MODEL: "模型证据",
|
||||||
|
ASSET: "资产证据",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "EvidenceConfirmDialog",
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
confirmReason: "",
|
||||||
|
submitting: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dialogVisible: {
|
||||||
|
get() {
|
||||||
|
return this.visible;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (!value) {
|
||||||
|
this.handleClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeLabel() {
|
||||||
|
return TYPE_LABEL_MAP[this.payload.evidenceType] || this.payload.evidenceType || "-";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(value) {
|
||||||
|
if (value) {
|
||||||
|
this.confirmReason = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
if (this.submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$emit("update:visible", false);
|
||||||
|
},
|
||||||
|
async handleSubmit() {
|
||||||
|
if (!this.confirmReason) {
|
||||||
|
this.$message.warning("请填写确认理由/备注");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.submitting = true;
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
...this.payload,
|
||||||
|
confirmReason: this.confirmReason,
|
||||||
|
};
|
||||||
|
const response = await saveEvidence(data);
|
||||||
|
this.$message.success("证据入库成功");
|
||||||
|
this.$emit("saved", response.data);
|
||||||
|
this.$emit("update:visible", false);
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error("证据入库失败");
|
||||||
|
console.error("证据入库失败", error);
|
||||||
|
} finally {
|
||||||
|
this.submitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.evidence-confirm-form {
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
padding-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
:visible.sync="drawerVisible"
|
||||||
|
append-to-body
|
||||||
|
title="证据线索"
|
||||||
|
size="420px"
|
||||||
|
custom-class="evidence-drawer"
|
||||||
|
@open="loadEvidence"
|
||||||
|
>
|
||||||
|
<div class="evidence-drawer-body">
|
||||||
|
<el-input
|
||||||
|
v-model.trim="keyword"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
prefix-icon="el-icon-search"
|
||||||
|
placeholder="搜索姓名、账号、证据编号"
|
||||||
|
@keyup.enter.native="loadEvidence"
|
||||||
|
@clear="loadEvidence"
|
||||||
|
>
|
||||||
|
<el-button slot="append" @click="loadEvidence">查询</el-button>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<div class="evidence-list" v-loading="loading">
|
||||||
|
<el-empty
|
||||||
|
v-if="!loading && evidenceList.length === 0"
|
||||||
|
:image-size="80"
|
||||||
|
description="暂无证据线索"
|
||||||
|
/>
|
||||||
|
<article
|
||||||
|
v-for="item in evidenceList"
|
||||||
|
:key="item.evidenceId"
|
||||||
|
class="evidence-card"
|
||||||
|
>
|
||||||
|
<div class="evidence-card__header">
|
||||||
|
<span class="evidence-code">EV-{{ formatEvidenceId(item.evidenceId) }} {{ formatType(item.evidenceType) }}</span>
|
||||||
|
<el-tag size="mini" type="danger" effect="plain">确认可疑</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="evidence-title">{{ item.evidenceTitle }}</div>
|
||||||
|
<div class="evidence-summary">{{ item.evidenceSummary }}</div>
|
||||||
|
<div class="evidence-meta">
|
||||||
|
来源:{{ item.sourcePage || formatSource(item.sourceType) }};确认人:{{ item.confirmBy || "-" }}
|
||||||
|
</div>
|
||||||
|
<div v-if="item.confirmReason" class="evidence-meta">
|
||||||
|
备注:{{ item.confirmReason }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listEvidence } from "@/api/ccdiEvidence";
|
||||||
|
|
||||||
|
const TYPE_LABEL_MAP = {
|
||||||
|
FLOW: "流水证据",
|
||||||
|
MODEL: "模型证据",
|
||||||
|
ASSET: "资产证据",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SOURCE_LABEL_MAP = {
|
||||||
|
BANK_STATEMENT: "流水详情",
|
||||||
|
MODEL_DETAIL: "模型详情",
|
||||||
|
ASSET_DETAIL: "资产详情",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "EvidenceDrawer",
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
projectId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keyword: "",
|
||||||
|
loading: false,
|
||||||
|
evidenceList: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
drawerVisible: {
|
||||||
|
get() {
|
||||||
|
return this.visible;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:visible", value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadEvidence() {
|
||||||
|
if (!this.projectId) {
|
||||||
|
this.evidenceList = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await listEvidence({
|
||||||
|
projectId: this.projectId,
|
||||||
|
keyword: this.keyword,
|
||||||
|
});
|
||||||
|
this.evidenceList = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
this.evidenceList = [];
|
||||||
|
this.$message.error("加载证据线索失败");
|
||||||
|
console.error("加载证据线索失败", error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatEvidenceId(value) {
|
||||||
|
return String(value || "").padStart(3, "0");
|
||||||
|
},
|
||||||
|
formatType(type) {
|
||||||
|
return TYPE_LABEL_MAP[type] || type || "证据";
|
||||||
|
},
|
||||||
|
formatSource(sourceType) {
|
||||||
|
return SOURCE_LABEL_MAP[sourceType] || sourceType || "-";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.evidence-drawer-body {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-list {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-card {
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid #e5eaf2;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-code {
|
||||||
|
color: #2474e8;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-title {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-summary {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #475467;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a96a8;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,10 +21,24 @@
|
|||||||
</template>
|
</template>
|
||||||
<el-table-column type="expand" width="1">
|
<el-table-column type="expand" width="1">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<family-asset-liability-detail
|
<div class="family-detail-wrapper">
|
||||||
:detail="detailCache[scope.row.staffIdCard]"
|
<div class="family-detail-toolbar">
|
||||||
:loading="Boolean(detailLoadingMap[scope.row.staffIdCard])"
|
<span class="family-detail-title">资产详情</span>
|
||||||
/>
|
<el-button
|
||||||
|
class="evidence-corner-btn"
|
||||||
|
size="mini"
|
||||||
|
plain
|
||||||
|
:disabled="Boolean(detailLoadingMap[scope.row.staffIdCard]) || !buildAssetEvidenceFingerprint(scope.row)"
|
||||||
|
@click="handleAddEvidence(scope.row)"
|
||||||
|
>
|
||||||
|
加入证据库
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<family-asset-liability-detail
|
||||||
|
:detail="detailCache[scope.row.staffIdCard]"
|
||||||
|
:loading="Boolean(detailLoadingMap[scope.row.staffIdCard])"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column type="index" label="序号" width="60" />
|
<el-table-column type="index" label="序号" width="60" />
|
||||||
@@ -67,6 +81,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getFamilyAssetLiabilityDetail } from "@/api/ccdi/projectSpecialCheck";
|
import { getFamilyAssetLiabilityDetail } from "@/api/ccdi/projectSpecialCheck";
|
||||||
|
import { buildAssetEvidenceFingerprint, buildAssetEvidenceSnapshot } from "@/utils/ccdiEvidence";
|
||||||
import FamilyAssetLiabilityDetail from "./FamilyAssetLiabilityDetail";
|
import FamilyAssetLiabilityDetail from "./FamilyAssetLiabilityDetail";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -114,6 +129,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
buildAssetEvidenceFingerprint,
|
||||||
resolveRiskTagType(riskLevelCode) {
|
resolveRiskTagType(riskLevelCode) {
|
||||||
const riskTagTypeMap = {
|
const riskTagTypeMap = {
|
||||||
NORMAL: "success",
|
NORMAL: "success",
|
||||||
@@ -164,6 +180,30 @@ export default {
|
|||||||
this.detailCache = {};
|
this.detailCache = {};
|
||||||
this.detailLoadingMap = {};
|
this.detailLoadingMap = {};
|
||||||
},
|
},
|
||||||
|
handleAddEvidence(row) {
|
||||||
|
if (!row || !row.staffIdCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sourceRecordId = buildAssetEvidenceFingerprint(row);
|
||||||
|
if (!sourceRecordId) {
|
||||||
|
this.$message.warning("缺少人员身份证或资产字段,暂不能加入证据库");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const detail = this.detailCache[row.staffIdCard] || {};
|
||||||
|
const summary = detail.summary || {};
|
||||||
|
const evidenceSummary = `${row.staffName}家庭资产负债核查:家庭总年收入${this.formatAmount(row.totalIncome)},家庭总负债${this.formatAmount(row.totalDebt)},家庭总资产${this.formatAmount(row.totalAsset)},风险情况${row.riskLevelName || "-" }。`;
|
||||||
|
this.$emit("evidence-confirm", {
|
||||||
|
evidenceType: "ASSET",
|
||||||
|
relatedPersonName: row.staffName || "关联人员",
|
||||||
|
relatedPersonId: row.staffIdCard || "",
|
||||||
|
evidenceTitle: `${row.staffName || "关联人员"} / 家庭资产负债核查`,
|
||||||
|
evidenceSummary,
|
||||||
|
sourceType: "ASSET_DETAIL",
|
||||||
|
sourceRecordId,
|
||||||
|
sourcePage: "资产详情",
|
||||||
|
snapshotJson: JSON.stringify(buildAssetEvidenceSnapshot(row, detail, summary)),
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -204,6 +244,39 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.family-detail-wrapper {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-detail-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-detail-title {
|
||||||
|
margin-right: auto;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-corner-btn {
|
||||||
|
padding: 4px 9px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b7fb8;
|
||||||
|
border-color: #d6e4f7;
|
||||||
|
background: #f8fbff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #2474e8;
|
||||||
|
border-color: #9fc3ff;
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.family-table th) {
|
:deep(.family-table th) {
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
|||||||
@@ -33,7 +33,10 @@
|
|||||||
@selection-change="handleRiskModelSelectionChange"
|
@selection-change="handleRiskModelSelectionChange"
|
||||||
@view-project-analysis="handleRiskModelProjectAnalysis"
|
@view-project-analysis="handleRiskModelProjectAnalysis"
|
||||||
/>
|
/>
|
||||||
<risk-detail-section :section-data="currentData.riskDetails" />
|
<risk-detail-section
|
||||||
|
:section-data="currentData.riskDetails"
|
||||||
|
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<project-analysis-dialog
|
<project-analysis-dialog
|
||||||
:visible.sync="projectAnalysisDialogVisible"
|
:visible.sync="projectAnalysisDialogVisible"
|
||||||
@@ -43,6 +46,7 @@
|
|||||||
:model-summary="projectAnalysisModelSummary"
|
:model-summary="projectAnalysisModelSummary"
|
||||||
:project-name="projectInfo.projectName"
|
:project-name="projectInfo.projectName"
|
||||||
@close="handleProjectAnalysisDialogClose"
|
@close="handleProjectAnalysisDialogClose"
|
||||||
|
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -75,15 +75,28 @@
|
|||||||
<div v-else-if='group.groupType === "OBJECT"' class="object-card-grid">
|
<div v-else-if='group.groupType === "OBJECT"' class="object-card-grid">
|
||||||
<article
|
<article
|
||||||
v-for="(item, index) in group.records || []"
|
v-for="(item, index) in group.records || []"
|
||||||
:key="`${item.title || index}-object`"
|
:key="`${resolveGroupKey(group, groupIndex)}-${item.title || 'object'}-${index}`"
|
||||||
class="object-card"
|
class="object-card"
|
||||||
>
|
>
|
||||||
<div class="object-card__title">{{ item.title || "-" }}</div>
|
<div class="object-card__header">
|
||||||
<div class="object-card__subtitle">{{ item.subtitle || "-" }}</div>
|
<div>
|
||||||
|
<div class="object-card__title">{{ item.title || "-" }}</div>
|
||||||
|
<div class="object-card__subtitle">{{ item.subtitle || "-" }}</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
class="evidence-corner-btn"
|
||||||
|
size="mini"
|
||||||
|
plain
|
||||||
|
:disabled="!buildModelEvidenceFingerprint(resolvePersonIdCard(), item.modelCode)"
|
||||||
|
@click="handleAddModelEvidence(item, group)"
|
||||||
|
>
|
||||||
|
加入证据库
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
<div v-if="item.riskTags && item.riskTags.length" class="tag-list">
|
<div v-if="item.riskTags && item.riskTags.length" class="tag-list">
|
||||||
<el-tag
|
<el-tag
|
||||||
v-for="(tag, tagIndex) in item.riskTags"
|
v-for="(tag, tagIndex) in item.riskTags"
|
||||||
:key="`${item.title || index}-risk-${tagIndex}`"
|
:key="`${resolveGroupKey(group, groupIndex)}-${item.title || index}-risk-${tagIndex}`"
|
||||||
size="mini"
|
size="mini"
|
||||||
effect="plain"
|
effect="plain"
|
||||||
>
|
>
|
||||||
@@ -97,7 +110,7 @@
|
|||||||
<p class="object-card__summary">{{ item.summary || "-" }}</p>
|
<p class="object-card__summary">{{ item.summary || "-" }}</p>
|
||||||
<div
|
<div
|
||||||
v-for="(field, fieldIndex) in item.extraFields || []"
|
v-for="(field, fieldIndex) in item.extraFields || []"
|
||||||
:key="`${item.title || index}-field-${fieldIndex}`"
|
:key="`${resolveGroupKey(group, groupIndex)}-${item.title || index}-field-${fieldIndex}`"
|
||||||
class="summary-row"
|
class="summary-row"
|
||||||
>
|
>
|
||||||
<span class="summary-row__label">{{ field.label }}</span>
|
<span class="summary-row__label">{{ field.label }}</span>
|
||||||
@@ -113,6 +126,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { buildModelEvidenceFingerprint, MODEL_EVIDENCE_FINGERPRINT_RULE } from "@/utils/ccdiEvidence";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ProjectAnalysisAbnormalTab",
|
name: "ProjectAnalysisAbnormalTab",
|
||||||
props: {
|
props: {
|
||||||
@@ -122,6 +137,14 @@ export default {
|
|||||||
groups: [],
|
groups: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
person: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
projectId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -150,6 +173,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
buildModelEvidenceFingerprint,
|
||||||
resolveGroupKey(group, index = 0) {
|
resolveGroupKey(group, index = 0) {
|
||||||
return group.groupCode || group.groupName || `BANK_STATEMENT_${index}`;
|
return group.groupCode || group.groupName || `BANK_STATEMENT_${index}`;
|
||||||
},
|
},
|
||||||
@@ -170,6 +194,48 @@ export default {
|
|||||||
const groupKey = this.resolveGroupKey(group);
|
const groupKey = this.resolveGroupKey(group);
|
||||||
this.$set(this.statementPageMap, groupKey, page);
|
this.$set(this.statementPageMap, groupKey, page);
|
||||||
},
|
},
|
||||||
|
handleAddModelEvidence(item, group) {
|
||||||
|
const safeItem = item || {};
|
||||||
|
const safeGroup = group || {};
|
||||||
|
const relatedPersonName = this.resolveRelatedPersonName(safeItem);
|
||||||
|
const personIdCard = this.resolvePersonIdCard();
|
||||||
|
const sourceRecordId = buildModelEvidenceFingerprint(personIdCard, safeItem.modelCode);
|
||||||
|
if (!sourceRecordId) {
|
||||||
|
this.$message.warning("缺少人员身份证或模型编码,暂不能加入证据库");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const riskTags = Array.isArray(safeItem.riskTags) ? safeItem.riskTags.join("、") : "";
|
||||||
|
const reason = safeItem.reasonDetail || safeItem.summary || "-";
|
||||||
|
const payload = {
|
||||||
|
evidenceType: "MODEL",
|
||||||
|
relatedPersonName,
|
||||||
|
relatedPersonId: personIdCard,
|
||||||
|
evidenceTitle: `${relatedPersonName} / ${safeItem.title || safeGroup.groupName || "模型异常"}`,
|
||||||
|
evidenceSummary: `${safeItem.title || safeGroup.groupName || "模型异常"}:${reason}`,
|
||||||
|
sourceType: "MODEL_DETAIL",
|
||||||
|
sourceRecordId,
|
||||||
|
sourcePage: "模型详情",
|
||||||
|
snapshotJson: JSON.stringify({
|
||||||
|
group: safeGroup,
|
||||||
|
item: safeItem,
|
||||||
|
person: this.person,
|
||||||
|
riskTags,
|
||||||
|
evidenceFingerprint: sourceRecordId,
|
||||||
|
evidenceFingerprintRule: MODEL_EVIDENCE_FINGERPRINT_RULE,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
this.$emit("evidence-confirm", payload);
|
||||||
|
this.$root.$emit("ccdi-evidence-confirm", payload);
|
||||||
|
},
|
||||||
|
resolvePersonIdCard() {
|
||||||
|
return (this.person && (this.person.idNo || this.person.staffIdCard)) || "";
|
||||||
|
},
|
||||||
|
resolveRelatedPersonName(item) {
|
||||||
|
if (this.person && (this.person.name || this.person.staffName)) {
|
||||||
|
return this.person.name || this.person.staffName;
|
||||||
|
}
|
||||||
|
return item.title || "关联人员";
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -252,12 +318,35 @@ export default {
|
|||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.object-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.object-card__title {
|
.object-card__title {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.evidence-corner-btn {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 4px 9px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b7fb8;
|
||||||
|
border-color: #d6e4f7;
|
||||||
|
background: #f8fbff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #2474e8;
|
||||||
|
border-color: #9fc3ff;
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.object-card__subtitle {
|
.object-card__subtitle {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -46,7 +46,12 @@
|
|||||||
</el-alert>
|
</el-alert>
|
||||||
<el-tabs v-model="activeTab" class="project-analysis-tabs" stretch>
|
<el-tabs v-model="activeTab" class="project-analysis-tabs" stretch>
|
||||||
<el-tab-pane label="异常明细" name="abnormalDetail">
|
<el-tab-pane label="异常明细" name="abnormalDetail">
|
||||||
<project-analysis-abnormal-tab :detail-data="dialogData.abnormalDetail" />
|
<project-analysis-abnormal-tab
|
||||||
|
:detail-data="dialogData.abnormalDetail"
|
||||||
|
:person="person"
|
||||||
|
:project-id="projectId"
|
||||||
|
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||||
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="资产分析" name="assetAnalysis">
|
<el-tab-pane label="资产分析" name="assetAnalysis">
|
||||||
<project-analysis-placeholder-tab :tab-data="getTabData('assetAnalysis')" />
|
<project-analysis-placeholder-tab :tab-data="getTabData('assetAnalysis')" />
|
||||||
|
|||||||
@@ -207,10 +207,23 @@
|
|||||||
:visible.sync="detailVisible"
|
:visible.sync="detailVisible"
|
||||||
append-to-body
|
append-to-body
|
||||||
custom-class="detail-dialog"
|
custom-class="detail-dialog"
|
||||||
title="流水详情"
|
|
||||||
width="980px"
|
width="980px"
|
||||||
@close="closeDetailDialog"
|
@close="closeDetailDialog"
|
||||||
>
|
>
|
||||||
|
<template slot="title">
|
||||||
|
<div class="detail-dialog-title">
|
||||||
|
<span>流水详情</span>
|
||||||
|
<el-button
|
||||||
|
class="evidence-corner-btn"
|
||||||
|
size="mini"
|
||||||
|
plain
|
||||||
|
:disabled="detailLoading || !buildFlowEvidenceFingerprint(detailData)"
|
||||||
|
@click="handleAddEvidence"
|
||||||
|
>
|
||||||
|
加入证据库
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div v-loading="detailLoading" class="detail-dialog-body">
|
<div v-loading="detailLoading" class="detail-dialog-body">
|
||||||
<div class="detail-overview-grid">
|
<div class="detail-overview-grid">
|
||||||
<div class="detail-field">
|
<div class="detail-field">
|
||||||
@@ -318,6 +331,7 @@ import {
|
|||||||
getOverviewSuspiciousTransactions,
|
getOverviewSuspiciousTransactions,
|
||||||
} from "@/api/ccdi/projectOverview";
|
} from "@/api/ccdi/projectOverview";
|
||||||
import { getBankStatementDetail } from "@/api/ccdiProjectBankStatement";
|
import { getBankStatementDetail } from "@/api/ccdiProjectBankStatement";
|
||||||
|
import { buildFlowEvidenceFingerprint, buildFlowEvidenceSnapshot } from "@/utils/ccdiEvidence";
|
||||||
|
|
||||||
const SUSPICIOUS_TYPE_OPTIONS = [
|
const SUSPICIOUS_TYPE_OPTIONS = [
|
||||||
{ value: "ALL", label: "全部可疑人员类型" },
|
{ value: "ALL", label: "全部可疑人员类型" },
|
||||||
@@ -428,6 +442,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
buildFlowEvidenceFingerprint,
|
||||||
async handleSuspiciousTypeChange(command) {
|
async handleSuspiciousTypeChange(command) {
|
||||||
this.currentSuspiciousType = command;
|
this.currentSuspiciousType = command;
|
||||||
this.suspiciousPageNum = 1;
|
this.suspiciousPageNum = 1;
|
||||||
@@ -586,6 +601,31 @@ export default {
|
|||||||
this.detailLoading = false;
|
this.detailLoading = false;
|
||||||
this.detailData = createEmptyDetailData();
|
this.detailData = createEmptyDetailData();
|
||||||
},
|
},
|
||||||
|
handleAddEvidence() {
|
||||||
|
const detail = this.detailData || {};
|
||||||
|
const sourceRecordId = buildFlowEvidenceFingerprint(detail);
|
||||||
|
const amountText = this.formatSignedAmount(detail.displayAmount);
|
||||||
|
const counterparty = this.formatCounterpartyName(detail);
|
||||||
|
const relatedPersonName = this.resolveFlowRelatedPerson(detail);
|
||||||
|
const hitTagText = Array.isArray(detail.hitTags) && detail.hitTags.length
|
||||||
|
? `,命中${detail.hitTags.map((tag) => tag.ruleName).filter(Boolean).join("、")}标签`
|
||||||
|
: "";
|
||||||
|
this.$emit("evidence-confirm", {
|
||||||
|
evidenceType: "FLOW",
|
||||||
|
relatedPersonName,
|
||||||
|
relatedPersonId: detail.cretNo || "",
|
||||||
|
evidenceTitle: `${relatedPersonName} / ${this.formatField(detail.leAccountNo)}`,
|
||||||
|
evidenceSummary: `${this.formatField(detail.trxDate)},${relatedPersonName}账户与${counterparty}发生交易,金额${amountText}${hitTagText}。`,
|
||||||
|
sourceType: "BANK_STATEMENT",
|
||||||
|
sourceRecordId,
|
||||||
|
sourcePage: "流水详情",
|
||||||
|
snapshotJson: JSON.stringify(buildFlowEvidenceSnapshot(detail)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolveFlowRelatedPerson(detail) {
|
||||||
|
const value = this.formatField(detail.leAccountName);
|
||||||
|
return value === "-" ? "关联人员" : value;
|
||||||
|
},
|
||||||
handleRiskDetailExport() {
|
handleRiskDetailExport() {
|
||||||
if (!this.projectId) {
|
if (!this.projectId) {
|
||||||
return;
|
return;
|
||||||
@@ -960,6 +1000,26 @@ export default {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-dialog-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-corner-btn {
|
||||||
|
padding: 4px 9px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b7fb8;
|
||||||
|
border-color: #d6e4f7;
|
||||||
|
background: #f8fbff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #2474e8;
|
||||||
|
border-color: #9fc3ff;
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.detail-dialog) {
|
:deep(.detail-dialog) {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
:title="sectionTitle"
|
:title="sectionTitle"
|
||||||
:subtitle="sectionSubtitle"
|
:subtitle="sectionSubtitle"
|
||||||
|
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section class="graph-placeholder-card">
|
<section class="graph-placeholder-card">
|
||||||
|
|||||||
@@ -33,6 +33,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
<el-button
|
||||||
|
class="evidence-entry-btn"
|
||||||
|
size="mini"
|
||||||
|
plain
|
||||||
|
icon="el-icon-collection-tag"
|
||||||
|
@click="evidenceDrawerVisible = true"
|
||||||
|
>
|
||||||
|
证据线索
|
||||||
|
</el-button>
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeTab"
|
:default-active="activeTab"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
@@ -59,6 +68,18 @@
|
|||||||
@name-selected="handleNameSelected"
|
@name-selected="handleNameSelected"
|
||||||
@generate-report="handleGenerateReport"
|
@generate-report="handleGenerateReport"
|
||||||
@fetch-bank-info="handleFetchBankInfo"
|
@fetch-bank-info="handleFetchBankInfo"
|
||||||
|
@evidence-confirm="handleEvidenceConfirm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<evidence-confirm-dialog
|
||||||
|
:visible.sync="evidenceConfirmVisible"
|
||||||
|
:payload="evidencePayload"
|
||||||
|
@saved="handleEvidenceSaved"
|
||||||
|
/>
|
||||||
|
<evidence-drawer
|
||||||
|
ref="evidenceDrawer"
|
||||||
|
:visible.sync="evidenceDrawerVisible"
|
||||||
|
:project-id="projectId"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -69,6 +90,8 @@ import ParamConfig from "./components/detail/ParamConfig";
|
|||||||
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
|
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
|
||||||
import SpecialCheck from "./components/detail/SpecialCheck";
|
import SpecialCheck from "./components/detail/SpecialCheck";
|
||||||
import DetailQuery from "./components/detail/DetailQuery";
|
import DetailQuery from "./components/detail/DetailQuery";
|
||||||
|
import EvidenceConfirmDialog from "./components/detail/EvidenceConfirmDialog";
|
||||||
|
import EvidenceDrawer from "./components/detail/EvidenceDrawer";
|
||||||
import { getProject } from "@/api/ccdiProject";
|
import { getProject } from "@/api/ccdiProject";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -79,6 +102,8 @@ export default {
|
|||||||
PreliminaryCheck,
|
PreliminaryCheck,
|
||||||
SpecialCheck,
|
SpecialCheck,
|
||||||
DetailQuery,
|
DetailQuery,
|
||||||
|
EvidenceConfirmDialog,
|
||||||
|
EvidenceDrawer,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -102,6 +127,9 @@ export default {
|
|||||||
warningThreshold: 60,
|
warningThreshold: 60,
|
||||||
projectStatus: "0",
|
projectStatus: "0",
|
||||||
},
|
},
|
||||||
|
evidenceConfirmVisible: false,
|
||||||
|
evidenceDrawerVisible: false,
|
||||||
|
evidencePayload: {},
|
||||||
projectStatusPollingTimer: null,
|
projectStatusPollingTimer: null,
|
||||||
projectStatusPollingInterval: 1000,
|
projectStatusPollingInterval: 1000,
|
||||||
projectStatusPollingLoading: false,
|
projectStatusPollingLoading: false,
|
||||||
@@ -139,8 +167,10 @@ export default {
|
|||||||
// 初始化页面数据
|
// 初始化页面数据
|
||||||
this.initActiveTabFromRoute();
|
this.initActiveTabFromRoute();
|
||||||
this.initPageData();
|
this.initPageData();
|
||||||
|
this.$root.$on("ccdi-evidence-confirm", this.handleEvidenceConfirm);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
this.$root.$off("ccdi-evidence-confirm", this.handleEvidenceConfirm);
|
||||||
this.stopProjectStatusPolling();
|
this.stopProjectStatusPolling();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -400,6 +430,21 @@ export default {
|
|||||||
handleRefreshProject() {
|
handleRefreshProject() {
|
||||||
this.initPageData();
|
this.initPageData();
|
||||||
},
|
},
|
||||||
|
handleEvidenceConfirm(payload) {
|
||||||
|
this.evidencePayload = {
|
||||||
|
projectId: this.projectId,
|
||||||
|
...(payload || {}),
|
||||||
|
};
|
||||||
|
this.evidenceConfirmVisible = true;
|
||||||
|
},
|
||||||
|
handleEvidenceSaved() {
|
||||||
|
this.evidenceDrawerVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.evidenceDrawer) {
|
||||||
|
this.$refs.evidenceDrawer.loadEvidence();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
/** 导出报告 */
|
/** 导出报告 */
|
||||||
handleExport() {
|
handleExport() {
|
||||||
console.log("导出报告");
|
console.log("导出报告");
|
||||||
@@ -496,6 +541,21 @@ export default {
|
|||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.evidence-entry-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b7fb8;
|
||||||
|
border-color: #d6e4f7;
|
||||||
|
background: #f8fbff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--ccdi-primary);
|
||||||
|
border-color: #9fc3ff;
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.nav-menu {
|
.nav-menu {
|
||||||
// 移除默认背景色和边框
|
// 移除默认背景色和边框
|
||||||
|
|||||||
26
sql/migration/2026-04-21-create-ccdi-evidence.sql
Normal file
26
sql/migration/2026-04-21-create-ccdi-evidence.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- 项目证据表
|
||||||
|
CREATE TABLE IF NOT EXISTS ccdi_evidence (
|
||||||
|
evidence_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '证据ID',
|
||||||
|
project_id BIGINT NOT NULL COMMENT '项目ID',
|
||||||
|
evidence_type VARCHAR(32) NOT NULL COMMENT '证据类型:FLOW/MODEL/ASSET',
|
||||||
|
related_person_name VARCHAR(100) NOT NULL COMMENT '关联人员姓名',
|
||||||
|
related_person_id VARCHAR(64) DEFAULT NULL COMMENT '关联人员标识,优先存身份证号或员工号',
|
||||||
|
evidence_title VARCHAR(255) NOT NULL COMMENT '证据标题',
|
||||||
|
evidence_summary VARCHAR(1000) NOT NULL COMMENT '证据摘要',
|
||||||
|
source_type VARCHAR(64) NOT NULL COMMENT '来源类型:BANK_STATEMENT/MODEL_DETAIL/ASSET_DETAIL',
|
||||||
|
source_record_id VARCHAR(128) DEFAULT NULL COMMENT '来源记录ID',
|
||||||
|
source_page VARCHAR(100) DEFAULT NULL COMMENT '来源页面名称',
|
||||||
|
snapshot_json LONGTEXT DEFAULT NULL COMMENT '证据快照JSON',
|
||||||
|
confirm_reason VARCHAR(1000) NOT NULL COMMENT '确认理由/备注',
|
||||||
|
confirm_by VARCHAR(64) DEFAULT NULL COMMENT '确认人',
|
||||||
|
confirm_time DATETIME DEFAULT NULL COMMENT '确认时间',
|
||||||
|
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (evidence_id),
|
||||||
|
KEY idx_ccdi_evidence_project (project_id),
|
||||||
|
KEY idx_ccdi_evidence_type (evidence_type),
|
||||||
|
KEY idx_ccdi_evidence_person (related_person_id),
|
||||||
|
KEY idx_ccdi_evidence_source (source_type, source_record_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目证据表';
|
||||||
Reference in New Issue
Block a user