新增项目证据库一期功能
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
|
||||
public class CcdiProjectPersonAnalysisObjectRecordVO {
|
||||
|
||||
private String modelCode;
|
||||
|
||||
private String title;
|
||||
|
||||
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
|
||||
max(tr.model_code) as modelCode,
|
||||
coalesce(max(staff.name), max(relation.relation_name), max(tr.object_key), max(tr.object_type)) as title,
|
||||
max(case
|
||||
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"
|
||||
append-to-body
|
||||
custom-class="detail-dialog"
|
||||
title="流水详情"
|
||||
width="980px"
|
||||
@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 class="detail-overview-grid">
|
||||
<div class="detail-field">
|
||||
@@ -394,6 +407,7 @@ import {
|
||||
getBankStatementOptions,
|
||||
getBankStatementDetail,
|
||||
} from "@/api/ccdiProjectBankStatement";
|
||||
import { buildFlowEvidenceFingerprint, buildFlowEvidenceSnapshot } from "@/utils/ccdiEvidence";
|
||||
|
||||
const TAB_MAP = {
|
||||
all: "all",
|
||||
@@ -518,6 +532,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
buildFlowEvidenceFingerprint,
|
||||
async getList() {
|
||||
this.syncProjectId();
|
||||
if (!this.queryParams.projectId) {
|
||||
@@ -638,6 +653,29 @@ export default {
|
||||
this.detailLoading = false;
|
||||
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() {
|
||||
if (this.total === 0) {
|
||||
return;
|
||||
@@ -751,6 +789,26 @@ export default {
|
||||
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-main {
|
||||
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>
|
||||
<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])"
|
||||
/>
|
||||
<div class="family-detail-wrapper">
|
||||
<div class="family-detail-toolbar">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
@@ -67,6 +81,7 @@
|
||||
|
||||
<script>
|
||||
import { getFamilyAssetLiabilityDetail } from "@/api/ccdi/projectSpecialCheck";
|
||||
import { buildAssetEvidenceFingerprint, buildAssetEvidenceSnapshot } from "@/utils/ccdiEvidence";
|
||||
import FamilyAssetLiabilityDetail from "./FamilyAssetLiabilityDetail";
|
||||
|
||||
export default {
|
||||
@@ -114,6 +129,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
buildAssetEvidenceFingerprint,
|
||||
resolveRiskTagType(riskLevelCode) {
|
||||
const riskTagTypeMap = {
|
||||
NORMAL: "success",
|
||||
@@ -164,6 +180,30 @@ export default {
|
||||
this.detailCache = {};
|
||||
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>
|
||||
@@ -204,6 +244,39 @@ export default {
|
||||
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) {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
@selection-change="handleRiskModelSelectionChange"
|
||||
@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>
|
||||
<project-analysis-dialog
|
||||
:visible.sync="projectAnalysisDialogVisible"
|
||||
@@ -43,6 +46,7 @@
|
||||
:model-summary="projectAnalysisModelSummary"
|
||||
:project-name="projectInfo.projectName"
|
||||
@close="handleProjectAnalysisDialogClose"
|
||||
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,15 +75,28 @@
|
||||
<div v-else-if='group.groupType === "OBJECT"' class="object-card-grid">
|
||||
<article
|
||||
v-for="(item, index) in group.records || []"
|
||||
:key="`${item.title || index}-object`"
|
||||
:key="`${resolveGroupKey(group, groupIndex)}-${item.title || 'object'}-${index}`"
|
||||
class="object-card"
|
||||
>
|
||||
<div class="object-card__title">{{ item.title || "-" }}</div>
|
||||
<div class="object-card__subtitle">{{ item.subtitle || "-" }}</div>
|
||||
<div class="object-card__header">
|
||||
<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">
|
||||
<el-tag
|
||||
v-for="(tag, tagIndex) in item.riskTags"
|
||||
:key="`${item.title || index}-risk-${tagIndex}`"
|
||||
:key="`${resolveGroupKey(group, groupIndex)}-${item.title || index}-risk-${tagIndex}`"
|
||||
size="mini"
|
||||
effect="plain"
|
||||
>
|
||||
@@ -97,7 +110,7 @@
|
||||
<p class="object-card__summary">{{ item.summary || "-" }}</p>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<span class="summary-row__label">{{ field.label }}</span>
|
||||
@@ -113,6 +126,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { buildModelEvidenceFingerprint, MODEL_EVIDENCE_FINGERPRINT_RULE } from "@/utils/ccdiEvidence";
|
||||
|
||||
export default {
|
||||
name: "ProjectAnalysisAbnormalTab",
|
||||
props: {
|
||||
@@ -122,6 +137,14 @@ export default {
|
||||
groups: [],
|
||||
}),
|
||||
},
|
||||
person: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
projectId: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -150,6 +173,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
buildModelEvidenceFingerprint,
|
||||
resolveGroupKey(group, index = 0) {
|
||||
return group.groupCode || group.groupName || `BANK_STATEMENT_${index}`;
|
||||
},
|
||||
@@ -170,6 +194,48 @@ export default {
|
||||
const groupKey = this.resolveGroupKey(group);
|
||||
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>
|
||||
@@ -252,12 +318,35 @@ export default {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.object-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.object-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
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 {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -46,7 +46,12 @@
|
||||
</el-alert>
|
||||
<el-tabs v-model="activeTab" class="project-analysis-tabs" stretch>
|
||||
<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 label="资产分析" name="assetAnalysis">
|
||||
<project-analysis-placeholder-tab :tab-data="getTabData('assetAnalysis')" />
|
||||
|
||||
@@ -207,10 +207,23 @@
|
||||
:visible.sync="detailVisible"
|
||||
append-to-body
|
||||
custom-class="detail-dialog"
|
||||
title="流水详情"
|
||||
width="980px"
|
||||
@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 class="detail-overview-grid">
|
||||
<div class="detail-field">
|
||||
@@ -318,6 +331,7 @@ import {
|
||||
getOverviewSuspiciousTransactions,
|
||||
} from "@/api/ccdi/projectOverview";
|
||||
import { getBankStatementDetail } from "@/api/ccdiProjectBankStatement";
|
||||
import { buildFlowEvidenceFingerprint, buildFlowEvidenceSnapshot } from "@/utils/ccdiEvidence";
|
||||
|
||||
const SUSPICIOUS_TYPE_OPTIONS = [
|
||||
{ value: "ALL", label: "全部可疑人员类型" },
|
||||
@@ -428,6 +442,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
buildFlowEvidenceFingerprint,
|
||||
async handleSuspiciousTypeChange(command) {
|
||||
this.currentSuspiciousType = command;
|
||||
this.suspiciousPageNum = 1;
|
||||
@@ -586,6 +601,31 @@ export default {
|
||||
this.detailLoading = false;
|
||||
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() {
|
||||
if (!this.projectId) {
|
||||
return;
|
||||
@@ -960,6 +1000,26 @@ export default {
|
||||
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) {
|
||||
border-radius: 8px;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
:project-id="projectId"
|
||||
:title="sectionTitle"
|
||||
:subtitle="sectionSubtitle"
|
||||
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||
/>
|
||||
|
||||
<section class="graph-placeholder-card">
|
||||
|
||||
@@ -33,6 +33,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
:default-active="activeTab"
|
||||
mode="horizontal"
|
||||
@@ -59,6 +68,18 @@
|
||||
@name-selected="handleNameSelected"
|
||||
@generate-report="handleGenerateReport"
|
||||
@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>
|
||||
</template>
|
||||
@@ -69,6 +90,8 @@ import ParamConfig from "./components/detail/ParamConfig";
|
||||
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
|
||||
import SpecialCheck from "./components/detail/SpecialCheck";
|
||||
import DetailQuery from "./components/detail/DetailQuery";
|
||||
import EvidenceConfirmDialog from "./components/detail/EvidenceConfirmDialog";
|
||||
import EvidenceDrawer from "./components/detail/EvidenceDrawer";
|
||||
import { getProject } from "@/api/ccdiProject";
|
||||
|
||||
export default {
|
||||
@@ -79,6 +102,8 @@ export default {
|
||||
PreliminaryCheck,
|
||||
SpecialCheck,
|
||||
DetailQuery,
|
||||
EvidenceConfirmDialog,
|
||||
EvidenceDrawer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -102,6 +127,9 @@ export default {
|
||||
warningThreshold: 60,
|
||||
projectStatus: "0",
|
||||
},
|
||||
evidenceConfirmVisible: false,
|
||||
evidenceDrawerVisible: false,
|
||||
evidencePayload: {},
|
||||
projectStatusPollingTimer: null,
|
||||
projectStatusPollingInterval: 1000,
|
||||
projectStatusPollingLoading: false,
|
||||
@@ -139,8 +167,10 @@ export default {
|
||||
// 初始化页面数据
|
||||
this.initActiveTabFromRoute();
|
||||
this.initPageData();
|
||||
this.$root.$on("ccdi-evidence-confirm", this.handleEvidenceConfirm);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off("ccdi-evidence-confirm", this.handleEvidenceConfirm);
|
||||
this.stopProjectStatusPolling();
|
||||
},
|
||||
methods: {
|
||||
@@ -400,6 +430,21 @@ export default {
|
||||
handleRefreshProject() {
|
||||
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() {
|
||||
console.log("导出报告");
|
||||
@@ -496,6 +541,21 @@ export default {
|
||||
.header-right {
|
||||
display: flex;
|
||||
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 {
|
||||
// 移除默认背景色和边框
|
||||
|
||||
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