新增未提交账户风险明细

This commit is contained in:
wjj
2026-07-20 14:26:29 +08:00
parent 17628486ae
commit 7fec8e21be
16 changed files with 513 additions and 35 deletions

View File

@@ -26,6 +26,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountFlowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
@@ -241,6 +242,18 @@ public class CcdiProjectOverviewController extends BaseController {
return AjaxResult.success(result);
}
/**
* 查询未提交账户流水
*/
@GetMapping("/unsubmitted-accounts/flows")
@Operation(summary = "查询未提交账户流水")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getUnsubmittedAccountFlows(CcdiProjectUnsubmittedAccountQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectUnsubmittedAccountFlowVO> rows = overviewService.getUnsubmittedAccountFlows(queryDTO);
return AjaxResult.success(rows);
}
/**
* 导出涉疑交易明细
*/
@@ -323,9 +336,12 @@ public class CcdiProjectOverviewController extends BaseController {
@PostMapping("/unsubmitted-accounts/export")
@Operation(summary = "导出未提交账户流水")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportUnsubmittedAccountFlows(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
List<CcdiProjectUnsubmittedAccountFlowExcel> rows = overviewService.exportUnsubmittedAccountFlows(projectId);
public void exportUnsubmittedAccountFlows(
HttpServletResponse response,
CcdiProjectUnsubmittedAccountQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectUnsubmittedAccountFlowExcel> rows = overviewService.exportUnsubmittedAccountFlows(queryDTO);
ExcelUtil<CcdiProjectUnsubmittedAccountFlowExcel> util =
new ExcelUtil<>(CcdiProjectUnsubmittedAccountFlowExcel.class);
util.exportExcel(response, rows, "未提交账户流水");

View File

@@ -12,4 +12,14 @@ public class CcdiProjectUnsubmittedAccountQueryDTO {
* 项目ID
*/
private Long projectId;
/**
* 主体名称
*/
private String subjectName;
/**
* 未提交账号
*/
private String accountNo;
}

View File

@@ -35,9 +35,9 @@ public class CcdiProjectSuspiciousTransactionExcel {
@Excel(name = "交易类型")
private String cashType;
@Excel(name = "异常标签")
private String hitTags;
@Excel(name = "交易金额")
private BigDecimal displayAmount;
@Excel(name = "异常标签")
private String hitTags;
}

View File

@@ -9,6 +9,8 @@ import lombok.Data;
@Data
public class CcdiProjectUnsubmittedAccountFlowVO {
private Long bankStatementId;
private String subjectName;
private String accountNo;

View File

@@ -275,10 +275,12 @@ public interface CcdiProjectOverviewMapper {
/**
* 查询未提交账户流水
*
* @param projectId 项目ID
* @param query 查询条件
* @return 未提交账户流水
*/
List<CcdiProjectUnsubmittedAccountFlowVO> selectUnsubmittedAccountFlowList(@Param("projectId") Long projectId);
List<CcdiProjectUnsubmittedAccountFlowVO> selectUnsubmittedAccountFlowList(
@Param("query") CcdiProjectUnsubmittedAccountQueryDTO query
);
/**
* 查询项目员工负面征信导出列表

View File

@@ -28,6 +28,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountFlowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountVO;
import jakarta.servlet.http.HttpServletResponse;
@@ -293,6 +294,18 @@ public interface ICcdiProjectOverviewService {
return new CcdiProjectUnsubmittedAccountVO();
}
/**
* 查询未提交账户流水
*
* @param queryDTO 查询条件
* @return 未提交账户流水
*/
default List<CcdiProjectUnsubmittedAccountFlowVO> getUnsubmittedAccountFlows(
CcdiProjectUnsubmittedAccountQueryDTO queryDTO
) {
return List.of();
}
/**
* 导出未提交账户流水
*
@@ -303,6 +316,18 @@ public interface ICcdiProjectOverviewService {
return List.of();
}
/**
* 导出未提交账户流水
*
* @param queryDTO 查询条件
* @return 导出列表
*/
default List<CcdiProjectUnsubmittedAccountFlowExcel> exportUnsubmittedAccountFlows(
CcdiProjectUnsubmittedAccountQueryDTO queryDTO
) {
return List.of();
}
/**
* 重算结果总览员工结果并同步项目风险人数
*

View File

@@ -506,10 +506,28 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
}
@Override
public List<CcdiProjectUnsubmittedAccountFlowExcel> exportUnsubmittedAccountFlows(Long projectId) {
ensureProjectExists(projectId);
public List<CcdiProjectUnsubmittedAccountFlowVO> getUnsubmittedAccountFlows(
CcdiProjectUnsubmittedAccountQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
return defaultList(overviewMapper.selectUnsubmittedAccountFlowList(projectId)).stream()
return defaultList(overviewMapper.selectUnsubmittedAccountFlowList(queryDTO));
}
@Override
public List<CcdiProjectUnsubmittedAccountFlowExcel> exportUnsubmittedAccountFlows(Long projectId) {
CcdiProjectUnsubmittedAccountQueryDTO queryDTO = new CcdiProjectUnsubmittedAccountQueryDTO();
queryDTO.setProjectId(projectId);
return exportUnsubmittedAccountFlows(queryDTO);
}
@Override
public List<CcdiProjectUnsubmittedAccountFlowExcel> exportUnsubmittedAccountFlows(
CcdiProjectUnsubmittedAccountQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
return defaultList(overviewMapper.selectUnsubmittedAccountFlowList(queryDTO)).stream()
.map(this::buildUnsubmittedAccountFlowExcelRow)
.toList();
}

View File

@@ -56,8 +56,8 @@ public class CcdiProjectRiskDetailWorkbookExporter {
"关联员工",
"摘要",
"交易类型",
"异常标签",
"交易金额"
"交易金额",
"异常标签"
};
writeHeader(header, headers);
@@ -72,8 +72,8 @@ public class CcdiProjectRiskDetailWorkbookExporter {
row.createCell(5).setCellValue(safeText(item.getRelatedStaffDisplay()));
row.createCell(6).setCellValue(safeText(item.getUserMemo()));
row.createCell(7).setCellValue(safeText(item.getCashType()));
row.createCell(8).setCellValue(safeText(item.getHitTags()));
row.createCell(9).setCellValue(safeNumber(item.getDisplayAmount()));
row.createCell(8).setCellValue(safeNumber(item.getDisplayAmount()));
row.createCell(9).setCellValue(safeText(item.getHitTags()));
}
}

View File

@@ -111,6 +111,7 @@
</resultMap>
<resultMap id="UnsubmittedAccountFlowResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountFlowVO">
<result property="bankStatementId" column="bankStatementId"/>
<result property="subjectName" column="subjectName"/>
<result property="accountNo" column="accountNo"/>
<result property="trxDate" column="trxDate"/>
@@ -1729,8 +1730,9 @@
</select>
<select id="selectUnsubmittedAccountFlowList" resultMap="UnsubmittedAccountFlowResultMap">
<bind name="unsubmittedProjectId" value="projectId"/>
<bind name="unsubmittedProjectId" value="query.projectId"/>
select
candidate.bankStatementId,
candidate.subjectName,
candidate.accountNo,
candidate.trxDate,
@@ -1749,6 +1751,16 @@
<include refid="unsubmittedAccountAggregateSql"/>
) agg on agg.subjectName = candidate.subjectName
and agg.accountNo = candidate.accountNo
<where>
<if test="query.subjectName != null and query.subjectName != ''">
and replace(replace(trim(candidate.subjectName), '', '('), '', ')')
= replace(replace(trim(#{query.subjectName}), '', '('), '', ')')
</if>
<if test="query.accountNo != null and query.accountNo != ''">
and candidate.accountNo
= replace(replace(replace(trim(#{query.accountNo}), '-', ''), char(9), ''), ' ', '')
</if>
</where>
order by candidate.subjectName asc, candidate.accountNo asc, candidate.trxDate asc, candidate.bankStatementId asc
</select>

View File

@@ -251,6 +251,11 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(accountSql.contains("unsubmittedAccountSubjectTotalSql"), accountSql);
assertTrue(flowSql.contains("unsubmittedAccountCandidateRowSql"), flowSql);
assertTrue(flowSql.contains("unsubmittedAccountAggregateSql"), flowSql);
assertTrue(flowSql.contains("query.projectId"), flowSql);
assertTrue(flowSql.contains("query.subjectName"), flowSql);
assertTrue(flowSql.contains("query.accountNo"), flowSql);
assertTrue(flowSql.contains("candidate.subjectName"), flowSql);
assertTrue(flowSql.contains("candidate.accountNo"), flowSql);
assertTrue(candidateSql.contains("bs.project_id = #{unsubmittedProjectId}"), candidateSql);
assertTrue(candidateSql.contains("bs.CUSTOMER_ACCOUNT_NAME"), candidateSql);
assertTrue(candidateSql.contains("bs.LE_ACCOUNT_NAME"), candidateSql);

View File

@@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletResponse;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.List;
@@ -91,11 +92,12 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
assertEquals("关联员工", workbook.getSheetAt(0).getRow(0).getCell(5).getStringCellValue());
assertEquals("摘要", workbook.getSheetAt(0).getRow(0).getCell(6).getStringCellValue());
assertEquals("交易类型", workbook.getSheetAt(0).getRow(0).getCell(7).getStringCellValue());
assertEquals("异常标签", workbook.getSheetAt(0).getRow(0).getCell(8).getStringCellValue());
assertEquals("交易金额", workbook.getSheetAt(0).getRow(0).getCell(9).getStringCellValue());
assertEquals("交易金额", workbook.getSheetAt(0).getRow(0).getCell(8).getStringCellValue());
assertEquals("异常标签", workbook.getSheetAt(0).getRow(0).getCell(9).getStringCellValue());
assertEquals("测试对手方", workbook.getSheetAt(0).getRow(1).getCell(3).getStringCellValue());
assertEquals("6222000000000001", workbook.getSheetAt(0).getRow(1).getCell(4).getStringCellValue());
assertEquals("异常标签", workbook.getSheetAt(0).getRow(1).getCell(8).getStringCellValue());
assertEquals(100D, workbook.getSheetAt(0).getRow(1).getCell(8).getNumericCellValue());
assertEquals("异常标签", workbook.getSheetAt(0).getRow(1).getCell(9).getStringCellValue());
assertEquals("账号", workbook.getSheetAt(2).getRow(0).getCell(0).getStringCellValue());
assertEquals("开户人", workbook.getSheetAt(2).getRow(0).getCell(1).getStringCellValue());
assertEquals("银行", workbook.getSheetAt(2).getRow(0).getCell(2).getStringCellValue());
@@ -122,4 +124,25 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
assertEquals(30000D, workbook.getSheetAt(3).getRow(1).getCell(9).getNumericCellValue());
}
}
@Test
void shouldKeepSuspiciousTransactionExcelAmountBeforeHitTags() {
Field[] fields = CcdiProjectSuspiciousTransactionExcel.class.getDeclaredFields();
int amountIndex = findFieldIndex(fields, "displayAmount");
int hitTagsIndex = findFieldIndex(fields, "hitTags");
assertTrue(amountIndex >= 0);
assertTrue(hitTagsIndex >= 0);
assertTrue(amountIndex < hitTagsIndex);
}
private int findFieldIndex(Field[] fields, String fieldName) {
for (int i = 0; i < fields.length; i++) {
if (fieldName.equals(fields[i].getName())) {
return i;
}
}
return -1;
}
}

View File

@@ -16,7 +16,7 @@
1. 候选账号来自 `ccdi_bank_statement` 当前项目流水。
2. 对手方户名等于本方主体名,对手方账号非空,且清洗后不等于本方账号。
3. 对手方账号不在本项目已提交账号集合中,已提交账号集合来自上传记录保存的 `account_nos`
3. 对手方账号不在本项目已提交账号集合中,已提交账号集合来自当前项目已上传流水的本方账号 `LE_ACCOUNT_NO`
4. 贷款排除只使用银行贷款/信贷高确定性关键词,不使用 `TRX_TYPE`
5. 只有候选账号关联流水全部命中贷款/信贷排除条件时,才排除该账号。

View File

@@ -4,17 +4,60 @@
- 新增结果总览“未提交账户”后端查询与导出能力。
- 新增前端风险明细未提交账户卡片展示。
- 新增未提交账户卡片点击后的本地“流水明细”弹窗,按 `LE_ACCOUNT_NAME + CUSTOMER_ACCOUNT_NO` 当前卡片范围查询不跳转“流水明细查询”Tab。
- 新增未提交账户流水查询接口 `GET /ccdi/project/overview/unsubmitted-accounts/flows`;原未提交账户导出接口支持可选 `subjectName/accountNo` 过滤,用于弹窗内导出当前账号流水。
- 风险明细统一导出与结果总览报告接入未提交账户流水。
- 贷款排除仅使用银行贷款/信贷高确定性关键词,未启用内部杂项账号排除。
- 本地项目 `90342test拉取行内流水` 补充未提交账户演示数据,统一流水号标记为 `UNSUBMITTED_ACCOUNT_SEED_90342_20260720%`
## 影响范围
- 后端:`ccdi-project` 结果总览 Controller、Service、Mapper、VO、Excel 与导出器。
- 前端:`ruoyi-ui` 结果总览风险明细组件与 API。
- 后端:`ccdi-project` 结果总览 Controller、Service、Mapper、DTO、VO、Excel 与导出器。
- 前端:`ruoyi-ui` 结果总览风险明细组件与 API,未提交账户卡片新增本地明细弹窗与当前账号导出
- 文档:后端计划、前端计划、实施记录。
## 验证情况
- 后端执行 `mvn -pl ccdi-project -am test "-Dtest=CcdiProjectOverviewMapperSqlTest,CcdiProjectRiskDetailWorkbookExporterTest,CcdiProjectOverviewReportPdfExporterTest,CcdiProjectOverviewServiceImplTest" "-Dsurefire.failIfNoSpecifiedTests=false"`32 个测试通过。
- 前端使用 Node 22.22.3 / npm 10.9.8 执行 `node tests\unit\risk-detail-abnormal-account-layout.test.js`静态断言通过
- 前端使用 Node 22.22.3 / npm 10.9.8 执行 `npm run build:prod`,构建通过;仍存在项目既有资源体积告警。
- 后端增量测试首次执行通过:`mvn -pl ccdi-project "-Dtest=CcdiProjectOverviewMapperSqlTest,CcdiProjectRiskDetailWorkbookExporterTest,CcdiProjectOverviewServiceImplTest" test`,共 25 个测试通过。
- 执行 `mvn -pl ruoyi-admin -am clean package -DskipTests`后端全链路依赖模块打包成功
- Mapper XML well-formed 检查通过。
- 前端使用 Node `v22.22.3`、npm `10.9.8` 执行 `node tests/unit/risk-detail-abnormal-account-layout.test.js`,静态断言通过。
- 前端使用 Node `v22.22.3`、npm `10.9.8` 启动 `npm run dev -- --host 127.0.0.1 --port 8088`,编译成功。
- 本地补数验证:`output/sql/unsubmitted_account_seed_90342.sql` 插入 8 条流水,其中 4 个同名未提交账户聚合展示、1 个同名贷款账户通过“归还贷款/贷款还款”口径被排除;清理脚本为 `output/sql/unsubmitted_account_cleanup_90342.sql`
- 浏览器验证:已打开 `http://127.0.0.1:8088/ccdiProject/detail/90342?tab=overview`页面显示“未提交账户”4 个、流入总额 100.90 万元、流出总额 -1.16 万元、差额 99.74 万元。
## 2026-07-20 追加:未提交账户流水弹窗
- 后端复用未提交账户候选流水 SQL 与贷款排除口径,增加按主体名称和未提交账号过滤的流水查询能力。
- 前端未提交账户卡片支持点击/回车打开本地弹窗,弹窗标题展示日期范围、主体和账号,并提供全部/流入/流出切换。
- 弹窗内“导出流水”只导出当前主体当前未提交账号流水;页面区块右上角“导出流水”仍导出当前项目全部未提交账户流水。
- 后端验证:`mvn -pl ccdi-project -am "-Dtest=CcdiProjectOverviewMapperSqlTest,CcdiProjectRiskDetailWorkbookExporterTest,CcdiProjectOverviewServiceImplTest" "-Dsurefire.failIfNoSpecifiedTests=false" test` 通过25 个测试通过。
- 后端打包:先执行 `mvn -pl ruoyi-admin -am clean package -DskipTests` 时因旧后端进程锁定 `ruoyi-admin.jar` 导致 clean 失败;确认 62318 旧进程结束后执行 `mvn -pl ruoyi-admin -am package -DskipTests` 成功,并重启真实 Java 后端PID 30896端口 62318 已监听。
- 前端验证:`.nvmrc` 指向 Node 14.21.3,但该环境缺少 npm按项目约定使用 Node `v22.22.3`、npm `10.9.8` 执行 `node tests/unit/risk-detail-abnormal-account-layout.test.js` 通过,并启动 `npm run dev -- --host 127.0.0.1 --port 8088` 编译成功。
- 浏览器验证:打开 `http://127.0.0.1:8088/ccdiProject/detail/90342?tab=overview`,未提交账户显示 4 个;点击首个账号卡片后弹出本地“流水明细”弹窗,接口 `GET /dev-api/ccdi/project/overview/unsubmitted-accounts/flows?projectId=90342&subjectName=测试主体&accountNo=33001061559364` 返回 200弹窗显示全部 2、流入 1、流出 1控制台无错误。
## 2026-07-20 追加:多主体展示优化
- 前端未提交账户统计栏新增“涉及主体数”,从 `subjects.length` 计算,不改后端接口与 SQL 口径。
- 主体分组标题继续只展示主体名称,不增加“主体:”前缀;多主体时按主体名称分组列出各自账号卡片。
- 未提交账户区块右侧导出按钮调整为与本页其他明细导出一致的文字按钮“导出”;弹窗内当前账号导出仍保留“导出流水”。
- 前端验证Node `v22.22.3`、npm `10.9.8` 下执行 `node tests/unit/risk-detail-abnormal-account-layout.test.js` 通过。
- 浏览器验证:打开 `http://127.0.0.1:8088/ccdiProject/detail/90342?tab=overview`,未提交账户区块显示“涉及主体数 1”、主体分组标题显示“测试主体”区块右侧按钮显示“导出”控制台无错误。
## 2026-07-20 追加:涉疑交易导出位置调整
- 移除“风险明细”区块标题右侧的综合导出按钮。
- 将导出按钮移动到“涉疑交易明细”查询条件行右侧。
- 导出接口改为 `POST /ccdi/project/overview/suspicious-transactions/export`,只导出涉疑交易明细单独 Excel不再包含负面征信、异常账户和未提交账户等综合风险明细内容。
- “交易占比较高”仍使用后端 `highRatio` 字段,判定逻辑保持为未提交账号流入或流出占对应主体总流入/总流出达到 10%。
- 前端验证Node `v22.22.3`、npm `10.9.8` 下执行 `node tests/unit/risk-detail-abnormal-account-layout.test.js` 通过。
- 浏览器验证:打开 `http://127.0.0.1:8088/ccdiProject/detail/90342?tab=overview`,确认“风险明细”标题右侧无导出按钮,“涉疑交易明细”筛选行右侧显示“导出”;点击后请求 `POST /dev-api/ccdi/project/overview/suspicious-transactions/export` 返回 200控制台无错误。
## 2026-07-20 追加:文案与涉疑交易导出列顺序调整
- 未提交账户说明文案调整为“被调查主体未提交分析的同名账户”,不再在页面说明中展示“已排除银行贷款类账号”。
- 涉疑交易明细单独 Excel 导出列顺序调整为“交易金额”在“异常标签”左侧。
- 综合风险明细工作簿中的“涉疑交易明细”Sheet 同步调整列顺序,保持同类导出字段顺序一致。
- 后端验证:执行 `mvn -pl ccdi-project -am "-Dtest=CcdiProjectRiskDetailWorkbookExporterTest" "-Dsurefire.failIfNoSpecifiedTests=false" test` 通过,覆盖综合风险明细工作簿列顺序与涉疑交易单独 Excel 字段顺序。
- 前端验证Node `v22.22.3`、npm `10.9.8` 下执行 `node tests/unit/risk-detail-abnormal-account-layout.test.js` 通过。
- 浏览器验证:刷新 `http://127.0.0.1:8088/ccdiProject/detail/90342?tab=overview`,页面显示新说明文案,未出现“已排除银行贷款类账号”;涉疑交易表格列顺序为“交易金额”在“异常标签”左侧,控制台无错误。

View File

@@ -152,6 +152,18 @@ export function getOverviewUnsubmittedAccounts(projectId) {
})
}
export function getOverviewUnsubmittedAccountFlows(params) {
return request({
url: '/ccdi/project/overview/unsubmitted-accounts/flows',
method: 'get',
params: {
projectId: params.projectId,
subjectName: params.subjectName,
accountNo: params.accountNo
}
})
}
export function exportOverviewReport(projectId) {
return request({
url: '/ccdi/project/overview/report/export',

View File

@@ -6,31 +6,31 @@
<div class="section-title">风险明细</div>
<div class="section-subtitle">展示涉疑交易与异常账户关联人员信息</div>
</div>
<el-button size="mini" type="text" :disabled="!projectId" @click="handleRiskDetailExport">
导出
</el-button>
</div>
<div class="block">
<div class="block-header">
<div>
<div class="block-title">未提交账户</div>
<div class="block-subtitle">被调查主体未提交分析的同名账户已排除银行贷款类账号</div>
<div class="block-subtitle">被调查主体未提交分析的同名账户</div>
</div>
<el-button
size="mini"
type="text"
icon="el-icon-download"
:disabled="!projectId || unsubmittedAccountTotal <= 0"
@click="handleUnsubmittedAccountExport"
>
导出流水
导出
</el-button>
</div>
<div v-loading="unsubmittedAccountLoading" class="unsubmitted-panel">
<template v-if="unsubmittedAccountTotal > 0">
<div class="unsubmitted-metrics">
<div class="unsubmitted-metric">
<div class="metric-label">涉及主体数</div>
<div class="metric-value">{{ unsubmittedSubjectTotal }}</div>
</div>
<div class="unsubmitted-metric">
<div class="metric-label">未提交账户数</div>
<div class="metric-value">{{ unsubmittedAccountTotal }}</div>
@@ -65,6 +65,10 @@
v-for="account in subject.accounts"
:key="`${subject.subjectName}-${account.accountNo}`"
class="unsubmitted-account-card"
role="button"
tabindex="0"
@click="handleUnsubmittedAccountCardClick(subject, account)"
@keyup.enter="handleUnsubmittedAccountCardClick(subject, account)"
>
<div class="unsubmitted-account-head">
<div class="unsubmitted-account-no">
@@ -143,6 +147,11 @@
查询
</el-button>
</el-form-item>
<el-form-item class="suspicious-export-action">
<el-button size="mini" type="text" :disabled="!projectId" @click="handleSuspiciousTransactionExport">
导出
</el-button>
</el-form-item>
</el-form>
<el-table
@@ -329,6 +338,96 @@
</div>
</div>
<el-dialog
:visible.sync="unsubmittedFlowVisible"
append-to-body
custom-class="unsubmitted-flow-dialog"
width="88%"
top="4vh"
@close="closeUnsubmittedFlowDialog"
>
<template slot="title">
<div class="unsubmitted-flow-title">
<span>流水明细</span>
<div v-if="selectedUnsubmittedAccount" class="unsubmitted-flow-title-tags">
<span>{{ formatUnsubmittedFlowPeriod }}</span>
<span>{{ formatField(selectedUnsubmittedAccount.subjectName) }}</span>
<span>{{ formatField(selectedUnsubmittedAccount.accountNo) }}</span>
</div>
</div>
</template>
<div class="unsubmitted-flow-toolbar">
<el-radio-group v-model="unsubmittedFlowType" size="small">
<el-radio-button label="ALL">全部 · {{ unsubmittedFlowAllCount }}</el-radio-button>
<el-radio-button label="INFLOW">流入 · {{ unsubmittedFlowInCount }}</el-radio-button>
<el-radio-button label="OUTFLOW">流出 · {{ unsubmittedFlowOutCount }}</el-radio-button>
</el-radio-group>
<el-button
size="small"
type="text"
icon="el-icon-download"
:disabled="!selectedUnsubmittedAccount || unsubmittedFlowAllCount <= 0"
@click="handleUnsubmittedFlowExport"
>
导出流水
</el-button>
</div>
<el-table
v-loading="unsubmittedFlowLoading"
:data="unsubmittedFlowFilteredList"
class="result-table unsubmitted-flow-table"
height="640"
>
<template slot="empty">
<el-empty :image-size="96" description="当前账号暂无流水明细" />
</template>
<el-table-column label="交易时间" min-width="150">
<template slot-scope="scope">
<div class="multi-line-cell">
<div class="primary-text">{{ formatDatePart(scope.row.trxDate) }}</div>
<div class="secondary-text">{{ formatTimePart(scope.row.trxDate) }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="本方账户/主体" min-width="240">
<template slot-scope="scope">
<div class="multi-line-cell">
<div class="primary-text">{{ formatField(scope.row.leAccountNo) }}</div>
<div class="secondary-text">{{ formatField(scope.row.leAccountName) }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="对方名称/账户" min-width="260">
<template slot-scope="scope">
<div class="multi-line-cell">
<div class="primary-text link-text">{{ formatField(scope.row.customerAccountName) }}</div>
<div class="secondary-text">{{ formatField(scope.row.customerAccountNo) }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="摘要/交易类型" min-width="260" show-overflow-tooltip>
<template slot-scope="scope">
<div class="multi-line-cell">
<div class="primary-text">{{ formatField(scope.row.userMemo) }}</div>
<div class="secondary-text">{{ formatField(scope.row.cashType) }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="交易金额" min-width="170" align="right">
<template slot-scope="scope">
<span class="flow-amount" :class="getUnsubmittedFlowAmountClass(scope.row)">
{{ formatUnsubmittedFlowAmount(scope.row) }}
</span>
</template>
</el-table-column>
<el-table-column label="分类" min-width="130">
<template>
<span>未提交账户</span>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog
:visible.sync="detailVisible"
append-to-body
@@ -456,6 +555,7 @@ import {
getOverviewAbnormalAccountPeople,
getOverviewEmployeeCreditNegative,
getOverviewSuspiciousTransactions,
getOverviewUnsubmittedAccountFlows,
getOverviewUnsubmittedAccounts,
} from "@/api/ccdi/projectOverview";
import { getBankStatementDetail } from "@/api/ccdiProjectBankStatement";
@@ -655,6 +755,11 @@ export default {
},
unsubmittedAccountTotal: 0,
unsubmittedAccountSubjects: [],
unsubmittedFlowVisible: false,
unsubmittedFlowLoading: false,
unsubmittedFlowType: "ALL",
selectedUnsubmittedAccount: null,
unsubmittedFlowList: [],
projectId: null,
statementDetailCache: {},
suspiciousRequestSeq: 0,
@@ -683,6 +788,33 @@ export default {
allModelOptionLabel() {
return this.isExternalSuspiciousType ? "全部外部模型" : "全部模型";
},
unsubmittedFlowFilteredList() {
if (this.unsubmittedFlowType === "INFLOW") {
return this.unsubmittedFlowList.filter((row) => this.isUnsubmittedFlowIn(row));
}
if (this.unsubmittedFlowType === "OUTFLOW") {
return this.unsubmittedFlowList.filter((row) => this.isUnsubmittedFlowOut(row));
}
return this.unsubmittedFlowList;
},
unsubmittedFlowAllCount() {
return this.unsubmittedFlowList.length;
},
unsubmittedSubjectTotal() {
return this.unsubmittedAccountSubjects.length;
},
unsubmittedFlowInCount() {
return this.unsubmittedFlowList.filter((row) => this.isUnsubmittedFlowIn(row)).length;
},
unsubmittedFlowOutCount() {
return this.unsubmittedFlowList.filter((row) => this.isUnsubmittedFlowOut(row)).length;
},
formatUnsubmittedFlowPeriod() {
if (!this.selectedUnsubmittedAccount) {
return "-";
}
return `${this.formatDatePart(this.selectedUnsubmittedAccount.startDate)} ~ ${this.formatDatePart(this.selectedUnsubmittedAccount.endDate)}`;
},
},
watch: {
sectionData: {
@@ -718,6 +850,7 @@ export default {
this.suspiciousLoading = false;
this.statementDetailCache = {};
this.closeDetailDialog();
this.closeUnsubmittedFlowDialog();
}
this.loadAbnormalAccountPeople();
this.loadUnsubmittedAccounts();
@@ -928,6 +1061,42 @@ export default {
this.unsubmittedAccountLoading = false;
}
},
async handleUnsubmittedAccountCardClick(subject, account) {
if (!this.projectId || !subject || !account || !account.accountNo) {
return;
}
const selectedAccount = {
...account,
subjectName: subject.subjectName,
};
this.selectedUnsubmittedAccount = selectedAccount;
this.unsubmittedFlowType = "ALL";
this.unsubmittedFlowList = [];
this.unsubmittedFlowVisible = true;
this.unsubmittedFlowLoading = true;
try {
const response = await getOverviewUnsubmittedAccountFlows({
projectId: this.projectId,
subjectName: selectedAccount.subjectName,
accountNo: selectedAccount.accountNo,
});
this.unsubmittedFlowList = Array.isArray(response && response.data) ? response.data : [];
} catch (error) {
this.unsubmittedFlowList = [];
this.$message.error("加载未提交账户流水失败");
console.error("加载未提交账户流水失败", error);
} finally {
this.unsubmittedFlowLoading = false;
}
},
closeUnsubmittedFlowDialog() {
this.unsubmittedFlowVisible = false;
this.unsubmittedFlowLoading = false;
this.unsubmittedFlowType = "ALL";
this.selectedUnsubmittedAccount = null;
this.unsubmittedFlowList = [];
},
async fetchStatementDetail(bankStatementId, silent, scope = {}) {
if (!bankStatementId) {
return createEmptyDetailData();
@@ -1022,18 +1191,18 @@ export default {
const value = this.formatField(detail.leAccountName);
return value === "-" ? "关联人员" : value;
},
handleRiskDetailExport() {
handleSuspiciousTransactionExport() {
if (!this.projectId) {
return;
}
this.download(
"ccdi/project/overview/risk-details/export",
"ccdi/project/overview/suspicious-transactions/export",
{
projectId: this.projectId,
modelCode: this.selectedModelCode,
suspiciousType: this.currentSuspiciousType,
},
`风险明细_${this.projectId}_${new Date().getTime()}.xlsx`
`涉疑交易明细_${this.projectId}_${new Date().getTime()}.xlsx`
);
},
handleUnsubmittedAccountExport() {
@@ -1046,6 +1215,20 @@ export default {
`未提交账户流水_${this.projectId}_${new Date().getTime()}.xlsx`
);
},
handleUnsubmittedFlowExport() {
if (!this.projectId || !this.selectedUnsubmittedAccount) {
return;
}
this.download(
"ccdi/project/overview/unsubmitted-accounts/export",
{
projectId: this.projectId,
subjectName: this.selectedUnsubmittedAccount.subjectName,
accountNo: this.selectedUnsubmittedAccount.accountNo,
},
`未提交账户流水_${this.projectId}_${this.selectedUnsubmittedAccount.accountNo}_${new Date().getTime()}.xlsx`
);
},
formatRelatedStaff(row) {
if (!row || !row.relatedStaffName) {
return "-";
@@ -1113,6 +1296,42 @@ export default {
}
return `${text.slice(0, 8)} ... ${text.slice(-6)}`;
},
formatDatePart(value) {
if (value === null || value === undefined || value === "") {
return "-";
}
return String(value).split(" ")[0] || "-";
},
formatTimePart(value) {
if (value === null || value === undefined || value === "") {
return "-";
}
const parts = String(value).split(" ");
return parts.length > 1 && parts[1] ? parts[1] : "-";
},
isUnsubmittedFlowIn(row) {
return Number(row && row.amountCr) > 0;
},
isUnsubmittedFlowOut(row) {
return Number(row && row.amountDr) > 0;
},
getUnsubmittedFlowAmountValue(row) {
if (this.isUnsubmittedFlowIn(row)) {
return Number(row.amountCr) || 0;
}
if (this.isUnsubmittedFlowOut(row)) {
return -Number(row.amountDr || 0);
}
return 0;
},
formatUnsubmittedFlowAmount(row) {
const amount = this.getUnsubmittedFlowAmountValue(row);
const prefix = amount < 0 ? "-¥" : "¥";
return `${prefix}${this.formatAmount(Math.abs(amount))}`;
},
getUnsubmittedFlowAmountClass(row) {
return this.getUnsubmittedFlowAmountValue(row) >= 0 ? "amount-in" : "amount-out";
},
formatSignedAmount(value) {
if (value === null || value === undefined || value === "") {
return "-";
@@ -1283,6 +1502,10 @@ export default {
min-width: 88px;
}
.suspicious-export-action {
margin-left: auto;
}
.suspicious-filter-select {
width: 240px;
}
@@ -1297,7 +1520,7 @@ export default {
.unsubmitted-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 18px;
margin-bottom: 24px;
}
@@ -1345,6 +1568,16 @@ export default {
padding: 18px 20px;
border: 1px solid #e5e7eb;
background: #fff;
cursor: pointer;
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
.unsubmitted-account-card:hover,
.unsubmitted-account-card:focus {
border-color: #93c5fd;
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.1);
outline: none;
transform: translateY(-1px);
}
.unsubmitted-account-head {
@@ -1397,6 +1630,60 @@ export default {
overflow-wrap: anywhere;
}
.unsubmitted-flow-title {
display: flex;
align-items: center;
gap: 18px;
min-width: 0;
}
.unsubmitted-flow-title > span {
flex: 0 0 auto;
color: #111827;
font-size: 18px;
font-weight: 700;
}
.unsubmitted-flow-title-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
min-width: 0;
}
.unsubmitted-flow-title-tags span {
max-width: 260px;
padding: 4px 10px;
color: #409eff;
background: #ecf5ff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.unsubmitted-flow-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.unsubmitted-flow-table :deep(.el-table__header th) {
background: #fff;
color: #606266;
font-size: 14px;
}
.flow-amount {
font-size: 16px;
font-weight: 600;
}
.link-text {
color: #409eff;
}
.result-table {
width: 100%;
}

View File

@@ -12,12 +12,25 @@ const source = fs.readFileSync(
[
"未提交账户",
"被调查主体未提交分析的同名账户",
"涉及主体数",
"未提交账户数",
"流入总额",
"流出总额",
"流入/流出差额",
"交易占比较高",
"导出流水",
"handleUnsubmittedAccountCardClick",
"getOverviewUnsubmittedAccountFlows",
"unsubmittedFlowVisible",
"unsubmittedSubjectTotal",
"流水明细",
"本方账户/主体",
"对方名称/账户",
"摘要/交易类型",
"handleSuspiciousTransactionExport",
"ccdi/project/overview/suspicious-transactions/export",
"涉疑交易明细_",
"当前项目暂无未提交账户",
"异常账户人员信息",
"账号",
@@ -30,3 +43,13 @@ const source = fs.readFileSync(
"涉及金额",
"当前项目暂无异常账户人员信息",
].forEach((token) => assert(source.includes(token), token));
assert(
!source.includes("ccdi/project/overview/risk-details/export"),
"风险明细区块不应再调用综合风险明细导出"
);
assert(
!source.includes("已排除银行贷款类账号"),
"未提交账户说明不应展示贷款排除文案"
);