修复涉疑交易筛选与标签错乱

This commit is contained in:
wjj
2026-07-16 18:46:51 +08:00
parent c3b56bdf2a
commit e0188b5e5b
24 changed files with 584 additions and 106 deletions

View File

@@ -18,9 +18,10 @@ export function getBankStatementOptions(projectId) {
});
}
export function getBankStatementDetail(bankStatementId) {
export function getBankStatementDetail(bankStatementId, params) {
return request({
url: `/ccdi/project/bank-statement/detail/${bankStatementId}`,
method: "get",
params,
});
}

View File

@@ -25,6 +25,7 @@
class="suspicious-filter-select"
:placeholder="allModelOptionLabel"
filterable
@change="handleSuspiciousModelSelectChange"
>
<el-option :label="allModelOptionLabel" value="" />
<el-option
@@ -513,6 +514,15 @@ const normalizeDetailData = (detail) => ({
hitTags: normalizeHitTags(detail && detail.hitTags),
});
const normalizeSuspiciousRows = (rows) => (
Array.isArray(rows)
? rows.map((row) => ({
...row,
hitTags: normalizeHitTags(row && row.hitTags, row),
}))
: []
);
export default {
name: "RiskDetailSection",
props: {
@@ -546,6 +556,8 @@ export default {
abnormalAccountList: [],
projectId: null,
statementDetailCache: {},
suspiciousRequestSeq: 0,
detailRequestSeq: 0,
};
},
computed: {
@@ -574,15 +586,11 @@ export default {
watch: {
sectionData: {
immediate: true,
deep: true,
handler(value) {
this.projectId = value && value.projectId ? value.projectId : null;
this.currentSuspiciousType = normalizeSuspiciousType(value && value.suspiciousType);
this.selectedModelCode = "";
const nextProjectId = value && value.projectId ? value.projectId : null;
const projectChanged = String(nextProjectId || "") !== String(this.projectId || "");
this.projectId = nextProjectId;
this.riskModelOptions = normalizeRiskModelOptions(value && value.riskModelOptions);
this.suspiciousPageNum = 1;
this.suspiciousPageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE;
this.suspiciousTotal = Number(value && value.total) || 0;
this.employeeCreditNegativePageNum = 1;
this.employeeCreditNegativePageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE;
this.employeeCreditNegativeTotal = Number(value && value.employeeCreditNegativeTotal) || 0;
@@ -590,19 +598,33 @@ export default {
this.abnormalAccountPageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE;
this.abnormalAccountTotal = 0;
this.abnormalAccountList = [];
const rows = Array.isArray(value && value.suspiciousTransactionList)
? value.suspiciousTransactionList
: [];
this.employeeCreditNegativeList = Array.isArray(value && value.employeeCreditNegativeList)
? value.employeeCreditNegativeList
: [];
this.hydrateSuspiciousRows(rows);
if (projectChanged) {
this.suspiciousRequestSeq += 1;
this.detailRequestSeq += 1;
this.currentSuspiciousType = normalizeSuspiciousType(value && value.suspiciousType);
this.selectedModelCode = "";
this.suspiciousPageNum = 1;
this.suspiciousPageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE;
this.suspiciousTotal = Number(value && value.total) || 0;
this.suspiciousTransactionList = normalizeSuspiciousRows(
value && value.suspiciousTransactionList
);
this.suspiciousLoading = false;
this.statementDetailCache = {};
this.closeDetailDialog();
}
this.loadAbnormalAccountPeople();
},
},
},
methods: {
buildFlowEvidenceFingerprint,
handleSuspiciousModelSelectChange() {
this.resetSuspiciousFilterResults();
},
handleSuspiciousTypeSelectChange(value) {
if (value === "EXTERNAL_PERSON") {
const hasExternalModel = EXTERNAL_RISK_MODEL_OPTIONS
@@ -610,11 +632,20 @@ export default {
if (!hasExternalModel) {
this.selectedModelCode = "";
}
return;
}
if (value === "MODEL_RULE" && isExternalModelCode(this.selectedModelCode)) {
} else if (value === "MODEL_RULE" && isExternalModelCode(this.selectedModelCode)) {
this.selectedModelCode = "";
}
this.resetSuspiciousFilterResults();
},
resetSuspiciousFilterResults() {
this.suspiciousRequestSeq += 1;
this.detailRequestSeq += 1;
this.suspiciousPageNum = 1;
this.suspiciousTotal = 0;
this.suspiciousTransactionList = [];
this.suspiciousLoading = false;
this.statementDetailCache = {};
this.closeDetailDialog();
},
async handleSuspiciousQuery() {
this.suspiciousPageNum = 1;
@@ -663,6 +694,7 @@ export default {
await this.loadAbnormalAccountPeople();
},
async loadSuspiciousTransactions() {
const requestSeq = ++this.suspiciousRequestSeq;
if (!this.projectId) {
this.suspiciousTransactionList = [];
this.suspiciousTotal = 0;
@@ -670,24 +702,34 @@ export default {
return;
}
const query = {
projectId: this.projectId,
modelCode: this.selectedModelCode,
suspiciousType: this.currentSuspiciousType,
pageNum: this.suspiciousPageNum,
pageSize: this.suspiciousPageSize,
};
this.suspiciousLoading = true;
try {
const response = await getOverviewSuspiciousTransactions({
projectId: this.projectId,
modelCode: this.selectedModelCode,
suspiciousType: this.currentSuspiciousType,
pageNum: this.suspiciousPageNum,
pageSize: this.suspiciousPageSize,
});
const response = await getOverviewSuspiciousTransactions(query);
if (requestSeq !== this.suspiciousRequestSeq) {
return;
}
const data = (response && response.data) || {};
this.suspiciousTotal = Number(data.total) || 0;
await this.hydrateSuspiciousRows(Array.isArray(data.rows) ? data.rows : []);
this.suspiciousTransactionList = normalizeSuspiciousRows(data.rows);
} catch (error) {
if (requestSeq !== this.suspiciousRequestSeq) {
return;
}
this.suspiciousTransactionList = [];
this.suspiciousTotal = 0;
this.$message.error("加载涉疑交易明细失败");
console.error("加载涉疑交易明细失败", error);
this.suspiciousLoading = false;
} finally {
if (requestSeq === this.suspiciousRequestSeq) {
this.suspiciousLoading = false;
}
}
},
async loadEmployeeCreditNegative() {
@@ -744,57 +786,25 @@ export default {
this.abnormalAccountLoading = false;
}
},
async hydrateSuspiciousRows(rows) {
const safeRows = Array.isArray(rows) ? rows : [];
if (!safeRows.length) {
this.suspiciousTransactionList = [];
this.suspiciousLoading = false;
return;
}
this.suspiciousLoading = true;
try {
const enrichedRows = await Promise.all(
safeRows.map(async (row) => {
if (!row || !row.bankStatementId) {
return {
...row,
hitTags: [],
};
}
try {
const detail = await this.fetchStatementDetail(row.bankStatementId, true);
return {
...detail,
...row,
hitTags: normalizeHitTags(detail.hitTags, row),
};
} catch (error) {
return {
...row,
hitTags: [],
};
}
})
);
this.suspiciousTransactionList = enrichedRows;
} finally {
this.suspiciousLoading = false;
}
},
async fetchStatementDetail(bankStatementId, silent) {
async fetchStatementDetail(bankStatementId, silent, scope = {}) {
if (!bankStatementId) {
return createEmptyDetailData();
}
if (this.statementDetailCache[bankStatementId]) {
return this.statementDetailCache[bankStatementId];
const modelCode = scope.modelCode !== undefined ? scope.modelCode : this.selectedModelCode;
const suspiciousType = scope.suspiciousType || this.currentSuspiciousType;
const cacheKey = `${bankStatementId}|${suspiciousType || "ALL"}|${modelCode || "ALL"}`;
if (this.statementDetailCache[cacheKey]) {
return this.statementDetailCache[cacheKey];
}
try {
const response = await getBankStatementDetail(bankStatementId);
const response = await getBankStatementDetail(bankStatementId, {
modelCode,
suspiciousType,
});
const detail = normalizeDetailData(response && response.data);
this.$set(this.statementDetailCache, bankStatementId, detail);
this.$set(this.statementDetailCache, cacheKey, detail);
return detail;
} catch (error) {
if (!silent) {
@@ -808,10 +818,18 @@ export default {
return;
}
const requestSeq = ++this.detailRequestSeq;
const scope = {
modelCode: this.selectedModelCode,
suspiciousType: this.currentSuspiciousType,
};
this.detailVisible = true;
this.detailLoading = true;
try {
const detail = await this.fetchStatementDetail(row.bankStatementId, false);
const detail = await this.fetchStatementDetail(row.bankStatementId, false, scope);
if (requestSeq !== this.detailRequestSeq) {
return;
}
this.detailData = {
...detail,
...row,
@@ -820,11 +838,16 @@ export default {
: normalizeHitTags(detail.hitTags, row),
};
} catch (error) {
if (requestSeq !== this.detailRequestSeq) {
return;
}
this.detailData = createEmptyDetailData();
this.$message.error("加载流水详情失败");
console.error("加载流水详情失败", error);
} finally {
this.detailLoading = false;
if (requestSeq === this.detailRequestSeq) {
this.detailLoading = false;
}
}
},
closeDetailDialog() {
@@ -865,6 +888,8 @@ export default {
"ccdi/project/overview/risk-details/export",
{
projectId: this.projectId,
modelCode: this.selectedModelCode,
suspiciousType: this.currentSuspiciousType,
},
`风险明细_${this.projectId}_${new Date().getTime()}.xlsx`
);

View File

@@ -0,0 +1,43 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const componentSource = fs.readFileSync(
path.resolve(
__dirname,
"../../src/views/ccdiProject/components/detail/RiskDetailSection.vue"
),
"utf8"
);
const statementApiSource = fs.readFileSync(
path.resolve(__dirname, "../../src/api/ccdiProjectBankStatement.js"),
"utf8"
);
[
'@change="handleSuspiciousModelSelectChange"',
"resetSuspiciousFilterResults()",
"const requestSeq = ++this.suspiciousRequestSeq;",
"if (requestSeq !== this.suspiciousRequestSeq)",
"if (requestSeq === this.suspiciousRequestSeq)",
"this.suspiciousTransactionList = normalizeSuspiciousRows(data.rows);",
"const projectChanged =",
"if (projectChanged) {",
"modelCode: this.selectedModelCode",
"suspiciousType: this.currentSuspiciousType",
"const cacheKey = `${bankStatementId}|${suspiciousType || \"ALL\"}|${modelCode || \"ALL\"}`;",
].forEach((token) => assert(componentSource.includes(token), token));
assert(
!componentSource.includes("async hydrateSuspiciousRows(rows)"),
"涉疑列表不得再按行请求详情补标签"
);
assert(
!componentSource.includes("deep: true"),
"父页面同项目数据更新不得通过深度监听重置筛选"
);
[
"getBankStatementDetail(bankStatementId, params)",
"params,",
].forEach((token) => assert(statementApiSource.includes(token), token));

View File

@@ -15,7 +15,7 @@ const source = fs.readFileSync(
"detailVisible",
"handleViewDetail",
"this.download(",
"ccdi/project/overview/suspicious-transactions/export",
"ccdi/project/overview/risk-details/export",
"原始文件",
"命中异常标签",
"formatOriginalFileName",