Compare commits
3 Commits
c999497693
...
7a317204f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a317204f7 | |||
| 2c6c982ed3 | |||
| 486a499af7 |
@@ -49,6 +49,10 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
private static final Set<String> ACCOUNT_TYPES = Set.of("BANK", "SECURITIES", "PAYMENT", "OTHER");
|
||||
private static final Set<String> BANK_SCOPES = Set.of("INTERNAL", "EXTERNAL");
|
||||
private static final Set<String> LEVELS = Set.of("LOW", "MEDIUM", "HIGH");
|
||||
private static final String OWNER_TYPE_INTERMEDIARY = "INTERMEDIARY";
|
||||
private static final String BANK_SCOPE_EXTERNAL = "EXTERNAL";
|
||||
private static final String RISK_LEVEL_LOW = "LOW";
|
||||
private static final String RISK_LEVEL_HIGH = "HIGH";
|
||||
private final List<AccountInfoImportFailureVO> latestImportFailures = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Resource
|
||||
@@ -195,12 +199,12 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
|
||||
private void validateOwner(String ownerType, String ownerId) {
|
||||
if (StringUtils.isEmpty(ownerId)) {
|
||||
if ("EXTERNAL".equals(ownerType) || "INTERMEDIARY".equals(ownerType)) {
|
||||
if ("EXTERNAL".equals(ownerType) || OWNER_TYPE_INTERMEDIARY.equals(ownerType)) {
|
||||
throw new RuntimeException("证件号不能为空");
|
||||
}
|
||||
throw new RuntimeException("所属人不能为空");
|
||||
}
|
||||
if ("EXTERNAL".equals(ownerType) || "INTERMEDIARY".equals(ownerType)) {
|
||||
if ("EXTERNAL".equals(ownerType) || OWNER_TYPE_INTERMEDIARY.equals(ownerType)) {
|
||||
return;
|
||||
}
|
||||
if ("EMPLOYEE".equals(ownerType)) {
|
||||
@@ -232,8 +236,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
}
|
||||
|
||||
private void prepareAnalysisFields(CcdiAccountInfo accountInfo) {
|
||||
if (!"EXTERNAL".equals(accountInfo.getBankScope())) {
|
||||
if (!BANK_SCOPE_EXTERNAL.equals(accountInfo.getBankScope())) {
|
||||
clearAnalysisFields(accountInfo);
|
||||
applyIntermediaryRiskLevel(accountInfo);
|
||||
return;
|
||||
}
|
||||
if (accountInfo.getIsActualControl() == null) {
|
||||
@@ -249,8 +254,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
accountInfo.setTxnFrequencyLevel("MEDIUM");
|
||||
}
|
||||
if (StringUtils.isEmpty(accountInfo.getTxnRiskLevel())) {
|
||||
accountInfo.setTxnRiskLevel("LOW");
|
||||
accountInfo.setTxnRiskLevel(RISK_LEVEL_LOW);
|
||||
}
|
||||
applyIntermediaryRiskLevel(accountInfo);
|
||||
}
|
||||
|
||||
private void clearAnalysisFields(CcdiAccountInfo accountInfo) {
|
||||
@@ -265,6 +271,12 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
accountInfo.setTxnRiskLevel(null);
|
||||
}
|
||||
|
||||
private void applyIntermediaryRiskLevel(CcdiAccountInfo accountInfo) {
|
||||
if (OWNER_TYPE_INTERMEDIARY.equals(accountInfo.getOwnerType())) {
|
||||
accountInfo.setTxnRiskLevel(RISK_LEVEL_HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAmount(BigDecimal amount, String fieldLabel) {
|
||||
if (amount == null) {
|
||||
return;
|
||||
@@ -285,6 +297,7 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
addDTO.setTxnFrequencyLevel(toUpper(addDTO.getTxnFrequencyLevel()));
|
||||
addDTO.setTxnRiskLevel(toUpper(addDTO.getTxnRiskLevel()));
|
||||
addDTO.setOwnerId(normalizeOwnerId(addDTO.getOwnerId()));
|
||||
applyIntermediaryRiskLevel(addDTO);
|
||||
}
|
||||
|
||||
private void normalizeEditDto(CcdiAccountInfoEditDTO editDTO) {
|
||||
@@ -295,6 +308,19 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
editDTO.setTxnFrequencyLevel(toUpper(editDTO.getTxnFrequencyLevel()));
|
||||
editDTO.setTxnRiskLevel(toUpper(editDTO.getTxnRiskLevel()));
|
||||
editDTO.setOwnerId(normalizeOwnerId(editDTO.getOwnerId()));
|
||||
applyIntermediaryRiskLevel(editDTO);
|
||||
}
|
||||
|
||||
private void applyIntermediaryRiskLevel(CcdiAccountInfoAddDTO addDTO) {
|
||||
if (OWNER_TYPE_INTERMEDIARY.equals(addDTO.getOwnerType())) {
|
||||
addDTO.setTxnRiskLevel(RISK_LEVEL_HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyIntermediaryRiskLevel(CcdiAccountInfoEditDTO editDTO) {
|
||||
if (OWNER_TYPE_INTERMEDIARY.equals(editDTO.getOwnerType())) {
|
||||
editDTO.setTxnRiskLevel(RISK_LEVEL_HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeCurrency(String currency) {
|
||||
@@ -362,7 +388,8 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
|
||||
private CcdiAccountInfoAddDTO toAddDto(CcdiAccountInfoExcel excel) {
|
||||
CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO();
|
||||
dto.setOwnerType(parseOwnerType(excel.getOwnerType()));
|
||||
String ownerType = parseOwnerType(excel.getOwnerType());
|
||||
dto.setOwnerType(ownerType);
|
||||
dto.setOwnerId(normalizeOwnerId(excel.getOwnerId()));
|
||||
dto.setAccountName(trimToNull(excel.getAccountName()));
|
||||
dto.setAccountNo(trimToNull(excel.getAccountNo()));
|
||||
@@ -382,7 +409,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
dto.setCreditSingleMaxAmount(parseDecimal(excel.getCreditSingleMaxAmount()));
|
||||
dto.setDebitDailyMaxAmount(parseDecimal(excel.getDebitDailyMaxAmount()));
|
||||
dto.setCreditDailyMaxAmount(parseDecimal(excel.getCreditDailyMaxAmount()));
|
||||
dto.setTxnRiskLevel(parseLevel(excel.getTxnRiskLevel(), "风险等级"));
|
||||
dto.setTxnRiskLevel(OWNER_TYPE_INTERMEDIARY.equals(ownerType)
|
||||
? RISK_LEVEL_HIGH
|
||||
: parseLevel(excel.getTxnRiskLevel(), "风险等级"));
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -418,7 +447,7 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
|
||||
return "RELATION";
|
||||
}
|
||||
if ("中介".equals(value)) {
|
||||
return "INTERMEDIARY";
|
||||
return OWNER_TYPE_INTERMEDIARY;
|
||||
}
|
||||
if ("外部人员".equals(value)) {
|
||||
return "EXTERNAL";
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.info.collection.service;
|
||||
import com.ruoyi.info.collection.domain.CcdiAccountInfo;
|
||||
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiAccountInfoExcel;
|
||||
import com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
|
||||
@@ -16,6 +17,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.beans.BeanWrapperImpl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -109,6 +111,64 @@ class CcdiAccountInfoServiceImplTest {
|
||||
assertNull(wrapper.getPropertyValue("txnRiskLevel"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void insertInternalIntermediaryAccount_shouldForceHighRiskLevelOnAccountInfo() {
|
||||
CcdiAccountInfoAddDTO dto = buildBaseAddDto();
|
||||
dto.setOwnerType("INTERMEDIARY");
|
||||
dto.setOwnerId("91330100MA00000001");
|
||||
dto.setBankScope("INTERNAL");
|
||||
dto.setIsActualControl(1);
|
||||
dto.setAvgMonthTxnCount(8);
|
||||
dto.setAvgMonthTxnAmount(new BigDecimal("9988.66"));
|
||||
dto.setTxnFrequencyLevel("HIGH");
|
||||
dto.setDebitSingleMaxAmount(new BigDecimal("111.11"));
|
||||
dto.setCreditSingleMaxAmount(new BigDecimal("222.22"));
|
||||
dto.setDebitDailyMaxAmount(new BigDecimal("333.33"));
|
||||
dto.setCreditDailyMaxAmount(new BigDecimal("444.44"));
|
||||
dto.setTxnRiskLevel("LOW");
|
||||
|
||||
when(accountInfoMapper.selectCount(any())).thenReturn(0L);
|
||||
when(accountInfoMapper.insert(any(CcdiAccountInfo.class))).thenReturn(1);
|
||||
|
||||
service.insertAccountInfo(dto);
|
||||
|
||||
ArgumentCaptor<CcdiAccountInfo> captor = ArgumentCaptor.forClass(CcdiAccountInfo.class);
|
||||
verify(accountInfoMapper).insert(captor.capture());
|
||||
BeanWrapperImpl wrapper = new BeanWrapperImpl(captor.getValue());
|
||||
assertNull(wrapper.getPropertyValue("isActualControl"));
|
||||
assertNull(wrapper.getPropertyValue("avgMonthTxnCount"));
|
||||
assertNull(wrapper.getPropertyValue("avgMonthTxnAmount"));
|
||||
assertNull(wrapper.getPropertyValue("txnFrequencyLevel"));
|
||||
assertEquals("HIGH", wrapper.getPropertyValue("txnRiskLevel"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExternalIntermediaryAccount_shouldForceHighRiskLevelOnAccountInfo() {
|
||||
CcdiAccountInfoExcel excel = new CcdiAccountInfoExcel();
|
||||
excel.setOwnerType("中介");
|
||||
excel.setOwnerId("91330100MA00000002");
|
||||
excel.setAccountName("测试中介");
|
||||
excel.setAccountNo("6222024000000099");
|
||||
excel.setAccountType("银行账户");
|
||||
excel.setBankScope("行外");
|
||||
excel.setOpenBank("中国银行");
|
||||
excel.setBankCode("BOC");
|
||||
excel.setCurrency("CNY");
|
||||
excel.setStatus("正常");
|
||||
excel.setEffectiveDate("2026-07-10");
|
||||
excel.setTxnRiskLevel("LOW");
|
||||
|
||||
when(accountInfoMapper.selectOne(any())).thenReturn(null);
|
||||
when(accountInfoMapper.selectCount(any())).thenReturn(0L);
|
||||
when(accountInfoMapper.insert(any(CcdiAccountInfo.class))).thenReturn(1);
|
||||
|
||||
service.importAccountInfo(List.of(excel), false);
|
||||
|
||||
ArgumentCaptor<CcdiAccountInfo> captor = ArgumentCaptor.forClass(CcdiAccountInfo.class);
|
||||
verify(accountInfoMapper).insert(captor.capture());
|
||||
assertEquals("HIGH", captor.getValue().getTxnRiskLevel());
|
||||
}
|
||||
|
||||
private CcdiAccountInfoAddDTO buildBaseAddDto() {
|
||||
CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO();
|
||||
dto.setAccountNo("6222024000000001");
|
||||
|
||||
@@ -51,7 +51,7 @@ public class GetBankStatementResponse {
|
||||
private String leName;
|
||||
|
||||
/** 企业银行账号 */
|
||||
private String accountMaskNo;
|
||||
private String accountNo;
|
||||
|
||||
/** 账号日期ID */
|
||||
private Integer accountingDateId;
|
||||
@@ -102,7 +102,7 @@ public class GetBankStatementResponse {
|
||||
private String customerName;
|
||||
|
||||
/** 对手方账号 */
|
||||
private String customerAccountMaskNo;
|
||||
private String customerAccountNo;
|
||||
|
||||
/** 对手方银行 */
|
||||
private String customerBank;
|
||||
|
||||
@@ -194,8 +194,8 @@ public class CcdiBankStatement implements Serializable {
|
||||
BeanUtils.copyProperties(item, entity);
|
||||
|
||||
// 4. 手动映射字段名不一致的情况
|
||||
entity.setLeAccountNo(item.getAccountMaskNo());
|
||||
entity.setCustomerAccountNo(item.getCustomerAccountMaskNo());
|
||||
entity.setLeAccountNo(item.getAccountNo());
|
||||
entity.setCustomerAccountNo(item.getCustomerAccountNo());
|
||||
entity.setLeAccountName(item.getLeName());
|
||||
entity.setAmountDr(item.getDrAmount());
|
||||
entity.setAmountCr(item.getCrAmount());
|
||||
|
||||
@@ -388,11 +388,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
staff.id_card AS idCard,
|
||||
SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)) AS annualAmount
|
||||
from ccdi_bank_statement bs
|
||||
inner join (
|
||||
select
|
||||
max(COALESCE(
|
||||
STR_TO_DATE(LEFT(TRIM(anchor_bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'),
|
||||
STR_TO_DATE(LEFT(TRIM(anchor_bs.TRX_DATE), 10), '%Y-%m-%d')
|
||||
)) AS anchorDate
|
||||
from ccdi_bank_statement anchor_bs
|
||||
where anchor_bs.project_id = #{projectId}
|
||||
) project_anchor on project_anchor.anchorDate is not null
|
||||
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
and STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
and COALESCE(
|
||||
STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'),
|
||||
STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d')
|
||||
) >= DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)
|
||||
group by staff.id_card
|
||||
having SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)) > #{threshold}
|
||||
) t
|
||||
@@ -541,11 +553,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
max(IFNULL(bs.LE_ACCOUNT_NAME, '')) AS personName,
|
||||
ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) AS annualAmount
|
||||
from ccdi_bank_statement bs
|
||||
inner join (
|
||||
select
|
||||
max(COALESCE(
|
||||
STR_TO_DATE(LEFT(TRIM(anchor_bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'),
|
||||
STR_TO_DATE(LEFT(TRIM(anchor_bs.TRX_DATE), 10), '%Y-%m-%d')
|
||||
)) AS anchorDate
|
||||
from ccdi_bank_statement anchor_bs
|
||||
where anchor_bs.project_id = #{projectId}
|
||||
) project_anchor on project_anchor.anchorDate is not null
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
|
||||
and <include refid="financialProductExclusionPredicate"/>
|
||||
and STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
and COALESCE(
|
||||
STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'),
|
||||
STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d')
|
||||
) >= DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)
|
||||
group by bs.cret_no
|
||||
having ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) > #{threshold}
|
||||
) t
|
||||
@@ -775,12 +799,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
CONCAT(
|
||||
'摘要/对手命中赌博敏感词,摘要“', IFNULL(bs.USER_MEMO, ''),
|
||||
'”,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
|
||||
'”,支出金额 ', CAST(IFNULL(bs.AMOUNT_DR, 0) AS CHAR), ' 元'
|
||||
'”,支出金额 ', CAST(IFNULL(bs.AMOUNT_DR, 0) AS CHAR),
|
||||
' 元,达到敏感交易最低金额 200 元'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_DR, 0) > 0
|
||||
and IFNULL(bs.AMOUNT_DR, 0) >= 200
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
@@ -796,12 +821,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
|
||||
'”摘要/对手方命中疑似赌博关键词,摘要“', IFNULL(bs.USER_MEMO, ''),
|
||||
'”,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
|
||||
'”,交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR), ' 元'
|
||||
'”,交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
|
||||
' 元,达到敏感交易最低金额 200 元'
|
||||
) AS reasonDetail
|
||||
from ccdi_bank_statement bs
|
||||
where bs.project_id = #{projectId}
|
||||
and <include refid="externalPersonPredicateSql"/>
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
|
||||
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) >= 200
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
|
||||
@@ -1152,7 +1178,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
CONCAT(
|
||||
'物业缴费金额 ', CAST(trade.amountDr AS CHAR),
|
||||
' 元,对手方“', IFNULL(trade.customerAccountName, ''),
|
||||
'”,证件号 ', trade.personId, ' 名下无房产登记'
|
||||
'”,证件号 ', trade.personId, ' 名下无匹配房产登记'
|
||||
) AS reasonDetail
|
||||
from (
|
||||
select
|
||||
@@ -1161,14 +1187,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
IFNULL(bs.AMOUNT_DR, 0) AS amountDr,
|
||||
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName
|
||||
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
|
||||
bs.USER_MEMO AS userMemo
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and IFNULL(bs.AMOUNT_DR, 0) > 0
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '物业|物业费|管理费|物业服务|综合服务'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '物业|小区|花园|苑|中心|大厦|业委会|业主委员会|置业|房地产|服务中心|管理处|社区'
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金'
|
||||
)
|
||||
|
||||
union all
|
||||
@@ -1179,26 +1206,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
bs.group_id AS groupId,
|
||||
bs.batch_id AS logId,
|
||||
IFNULL(bs.AMOUNT_DR, 0) AS amountDr,
|
||||
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName
|
||||
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
|
||||
bs.USER_MEMO AS userMemo
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_staff_fmy_relation relation on relation.relation_cert_no = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and relation.status = 1
|
||||
and IFNULL(bs.AMOUNT_DR, 0) > 0
|
||||
and (
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '物业|物业费|管理费|物业服务|综合服务'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '物业|小区|花园|苑|中心|大厦|业委会|业主委员会|置业|房地产|服务中心|管理处|社区'
|
||||
IFNULL(bs.USER_MEMO, '') REGEXP '物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金'
|
||||
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金'
|
||||
)
|
||||
) trade
|
||||
left join (
|
||||
select distinct
|
||||
asset.person_id AS personId
|
||||
asset.person_id AS personId,
|
||||
asset.asset_name AS assetName
|
||||
from ccdi_asset_info asset
|
||||
where asset.asset_main_type = '房产'
|
||||
and asset.asset_sub_type = '住宅'
|
||||
and asset.asset_status = '正常'
|
||||
and trim(IFNULL(asset.asset_name, '')) != ''
|
||||
) asset
|
||||
on asset.personId = trade.personId
|
||||
and (
|
||||
(
|
||||
trim(IFNULL(trade.customerAccountName, '')) != ''
|
||||
and IFNULL(trade.customerAccountName, '') LIKE concat('%', asset.assetName, '%')
|
||||
)
|
||||
or (
|
||||
trim(IFNULL(trade.userMemo, '')) != ''
|
||||
and IFNULL(trade.userMemo, '') LIKE concat('%', asset.assetName, '%')
|
||||
)
|
||||
)
|
||||
where asset.personId is null
|
||||
</select>
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ class CcdiBankStatementTest {
|
||||
item.setLeId(100);
|
||||
item.setAccountId(200L);
|
||||
item.setLeName("测试企业");
|
||||
item.setAccountMaskNo("6222****1234");
|
||||
item.setAccountNo("6222023300001234");
|
||||
item.setDrAmount(new BigDecimal("1000.00"));
|
||||
item.setCrAmount(new BigDecimal("500.00"));
|
||||
item.setBalanceAmount(new BigDecimal("5000.00"));
|
||||
item.setTrxDate("2026-03-04");
|
||||
item.setCustomerAccountMaskNo("6228****5678");
|
||||
item.setCustomerAccountNo("6228483300005678");
|
||||
|
||||
// 执行转换
|
||||
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
|
||||
@@ -41,8 +41,8 @@ class CcdiBankStatementTest {
|
||||
assertEquals("测试企业", entity.getLeAccountName(), "企业名称应该匹配");
|
||||
|
||||
// 验证手动映射的字段
|
||||
assertEquals("6222****1234", entity.getLeAccountNo(), "企业账号应该从 accountMaskNo 映射");
|
||||
assertEquals("6228****5678", entity.getCustomerAccountNo(), "对手方账号应该从 customerAccountMaskNo 映射");
|
||||
assertEquals("6222023300001234", entity.getLeAccountNo(), "企业账号应该从 accountNo 映射");
|
||||
assertEquals("6228483300005678", entity.getCustomerAccountNo(), "对手方账号应该从 customerAccountNo 映射");
|
||||
|
||||
// 验证金额字段
|
||||
assertEquals(new BigDecimal("1000.00"), entity.getAmountDr(), "付款金额应该匹配");
|
||||
|
||||
@@ -152,6 +152,49 @@ class CcdiBankTagAnalysisMapperXmlTest {
|
||||
assertTrue(!selectSql.contains("social_credit_code = bs"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamblingSensitiveKeywordRules_shouldIgnoreSmallAmountTransactions() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String staffSelectSql = extractSelectSql(xml, "selectGamblingSensitiveKeywordStatements");
|
||||
String externalSelectSql = extractSelectSql(xml, "selectExternalGamblingMemoStatements");
|
||||
|
||||
assertTrue(staffSelectSql.contains("IFNULL(bs.AMOUNT_DR, 0) >= 200"));
|
||||
assertTrue(staffSelectSql.contains("达到敏感交易最低金额 200 元"));
|
||||
assertTrue(externalSelectSql.contains(
|
||||
"GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) >= 200"
|
||||
));
|
||||
assertTrue(externalSelectSql.contains("达到敏感交易最低金额 200 元"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void annualTurnoverRules_shouldUseProjectStatementDateAsOneYearAnchor() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String staffSelectSql = extractSelectSql(xml, "selectAnnualTurnoverObjects");
|
||||
String externalSelectSql = extractSelectSql(xml, "selectExternalAnnualTurnoverObjects");
|
||||
|
||||
assertTrue(staffSelectSql.contains("project_anchor.anchorDate"));
|
||||
assertTrue(staffSelectSql.contains("DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)"));
|
||||
assertFalse(staffSelectSql.contains("CURDATE()"));
|
||||
assertTrue(externalSelectSql.contains("project_anchor.anchorDate"));
|
||||
assertTrue(externalSelectSql.contains("DATE_SUB(project_anchor.anchorDate, INTERVAL 12 MONTH)"));
|
||||
assertFalse(externalSelectSql.contains("CURDATE()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertyFeeMismatchRule_shouldUseReasonablePropertyKeywordsAndAssetNameMatching() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String selectSql = extractSelectSql(xml, "selectPropertyFeeRegistrationMismatchStatements");
|
||||
|
||||
assertTrue(selectSql.contains("物业|物管|业委会|业主委员会|维修基金|住宅专项维修资金|房屋维修资金"));
|
||||
assertFalse(selectSql.contains("中心"));
|
||||
assertFalse(selectSql.contains("社区"));
|
||||
assertFalse(selectSql.contains("大厦"));
|
||||
assertTrue(selectSql.contains("asset.asset_name AS assetName"));
|
||||
assertTrue(selectSql.contains("trade.customerAccountName"));
|
||||
assertTrue(selectSql.contains("trade.userMemo"));
|
||||
assertTrue(selectSql.contains("名下无匹配房产登记"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withdrawCntObjectRule_shouldUseRealSqlAndKeepObjectHitFields() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
|
||||
@@ -957,9 +957,9 @@ class CcdiFileUploadServiceImplTest {
|
||||
return response;
|
||||
}
|
||||
|
||||
private GetBankStatementResponse.BankStatementItem buildBankStatementItem(String accountMaskNo) {
|
||||
private GetBankStatementResponse.BankStatementItem buildBankStatementItem(String accountNo) {
|
||||
GetBankStatementResponse.BankStatementItem item = new GetBankStatementResponse.BankStatementItem();
|
||||
item.setAccountMaskNo(accountMaskNo);
|
||||
item.setAccountNo(accountNo);
|
||||
item.setAccountingDateId(20260310);
|
||||
item.setDrAmount(new BigDecimal("100.00"));
|
||||
item.setCrAmount(new BigDecimal("0.00"));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 账户库中介风险等级后端实施计划
|
||||
|
||||
## 1. 目标
|
||||
|
||||
账户库中所属人类型为 `INTERMEDIARY` 的账户,风险等级统一为 `HIGH`。该规则覆盖页面新增、编辑保存、Excel 导入新增和 Excel 导入更新。
|
||||
|
||||
## 2. 实施范围
|
||||
|
||||
- `ccdi-info-collection` 账户库保存与导入服务
|
||||
- 账户库服务单元测试
|
||||
|
||||
## 3. 实施步骤
|
||||
|
||||
1. 在 `CcdiAccountInfoServiceImpl` 增加中介风险等级统一处理方法。
|
||||
2. 在新增、编辑 DTO 归一化后,将 `ownerType = INTERMEDIARY` 的 `txnRiskLevel` 强制置为 `HIGH`。
|
||||
3. 在分析字段准备阶段保留该规则,使行内中介账户即使清空其他分析字段,也保留风险等级 `HIGH`。
|
||||
4. 在 Excel 导入 DTO 转换阶段,对中介行跳过模板风险等级值,直接置为 `HIGH`。
|
||||
5. 补充单元测试覆盖行内中介保存和行外中介导入。
|
||||
|
||||
## 4. 验证要点
|
||||
|
||||
- 行内中介账户保存后 `trans_risk_level = HIGH`。
|
||||
- 行外中介账户导入时,即使模板填写 `LOW`,保存结果仍为 `HIGH`。
|
||||
- 普通行内员工账户仍清空分析字段,不写风险等级。
|
||||
- 已有历史数据不会自动更新,只有重新编辑保存或导入更新后才应用该规则。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 账户库中介风险等级前端实施计划
|
||||
|
||||
## 1. 目标
|
||||
|
||||
账户库新增或编辑中介账户时,页面表单与后端强制规则保持一致,风险等级显示为 `HIGH`。
|
||||
|
||||
## 2. 实施范围
|
||||
|
||||
- `ruoyi-ui/src/views/ccdiAccountInfo/index.vue`
|
||||
|
||||
## 3. 实施步骤
|
||||
|
||||
1. 增加表单侧中介风险等级处理方法。
|
||||
2. 切换所属人类型为 `INTERMEDIARY` 时,将 `txnRiskLevel` 设置为 `HIGH`。
|
||||
3. 打开非详情模式的编辑弹窗时,若所属人类型为中介,同步显示 `HIGH`。
|
||||
4. 提交表单前再次应用该规则,避免页面状态被手动或旧数据改回其他等级。
|
||||
5. 所属人类型为中介时,风险等级下拉置为只读,避免用户选择其他等级后保存结果被后端强制改回 `HIGH`。
|
||||
|
||||
## 4. 验证要点
|
||||
|
||||
- 新增账户时选择“中介”,风险等级显示 `HIGH`。
|
||||
- 编辑中介账户时,风险等级显示 `HIGH`。
|
||||
- 中介账户表单中风险等级不可编辑。
|
||||
- 提交 payload 中中介账户的 `txnRiskLevel` 为 `HIGH`。
|
||||
- 非中介账户不受前端默认值变更影响。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 流水账号字段映射实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 在流水分析响应 DTO `GetBankStatementResponse.BankStatementItem` 中补充 `accountNo`、`customerAccountNo` 字段,并移除 `accountMaskNo`、`customerAccountMaskNo` 字段。
|
||||
- 将银行流水实体转换逻辑从 `accountMaskNo`、`customerAccountMaskNo` 改为直接读取 `accountNo`、`customerAccountNo`。
|
||||
- 同步调整相关单元测试数据和断言,避免继续按掩码账号字段验证。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响流水分析接口返回流水明细后写入 `ccdi_bank_statement.LE_ACCOUNT_NO`、`CUSTOMER_ACCOUNT_NO` 的字段来源。
|
||||
- 不改变数据库结构。
|
||||
- 不改变前端页面。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 已执行 `mvn -pl ccdi-project -am test "-Dtest=CcdiBankStatementTest,CcdiFileUploadServiceImplTest" "-Dsurefire.failIfNoSpecifiedTests=false"`。
|
||||
- 测试结果:39 个测试通过,0 失败,0 错误。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 账户库中介风险等级实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 后端 `CcdiAccountInfoServiceImpl` 增加中介风险等级统一规则:`ownerType = INTERMEDIARY` 时强制 `txnRiskLevel = HIGH`。
|
||||
- 行内中介账户清空其他分析字段后,保留风险等级为 `HIGH`。
|
||||
- Excel 导入中介账户时忽略模板中的风险等级值,统一写入 `HIGH`。
|
||||
- 前端账户库表单在选择或编辑中介账户时同步显示风险等级 `HIGH`,并禁用风险等级下拉;提交前再次应用该规则。
|
||||
- 补充账户库服务单元测试,覆盖行内中介保存和行外中介导入。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响账户库页面新增、编辑保存、Excel 导入新增、Excel 导入更新。
|
||||
- 不自动修改已有历史数据;历史中介账户只有重新编辑保存或导入更新后才会统一为 `HIGH`。
|
||||
- 不改变数据库表结构。
|
||||
- 不改变非中介账户风险等级逻辑。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 已执行 `mvn -pl ccdi-info-collection -am test '-Dtest=CcdiAccountInfoServiceImplTest' '-Dsurefire.failIfNoSpecifiedTests=false'`。
|
||||
- 测试结果:4 个测试通过,0 失败,0 错误。
|
||||
- 已执行 `npm run build:prod`。
|
||||
- 构建结果:通过;存在既有资源体积警告,其中包含工作区已有的 `ruoyi-ui/public/0uS5znL.docx` 被打包提醒。
|
||||
- 已使用浏览器打开真实业务页面 `http://localhost/ccdiAccountInfo` 验证新增弹窗:初始员工风险等级为 `LOW`,切换所属人类型为“中介”后风险等级变为 `HIGH` 且下拉不可编辑;未提交保存,未写入测试数据。
|
||||
- 已复测中介风险等级交互:风险等级控件在中介状态下为禁用状态,避免用户选择其他等级后被后端覆盖。
|
||||
- 页面验证过程中观察到现有中介历史记录风险等级仍为空,符合本次不自动迁移历史数据的口径。
|
||||
- 前端验证使用 Node v22.22.3、npm 10.9.8。原因:`.nvmrc` 指向的 Node 14.21.3 在当前 PowerShell 环境切换后 `node`、`npm` 不可用,按仓库规则改用已验证可用版本。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 涉疑交易明细筛选联动实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将涉疑交易明细第二个筛选框文案由“可疑规则”统一为“预警类型”。
|
||||
- 将默认选项文案由“全部可疑规则”调整为“全部预警类型”。
|
||||
- 在“全部预警类型”下,“命中模型”下拉同时展示员工模型与外部人员模型,并按模型编码去重。
|
||||
- 选择外部人员模型时不再强制切换“预警类型”,支持 `全部预警类型 + 外部人员可疑关系` 直接查询。
|
||||
- 保留明确类型约束:当预警类型切到“外部人员预警”时,仅保留外部人员模型;当预警类型切到“员工规则命中”时,清空已选择的外部人员模型。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 前端组件:`ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`
|
||||
- 前端静态单测:
|
||||
- `ruoyi-ui/tests/unit/risk-detail-suspicious-transaction-layout.test.js`
|
||||
- `ruoyi-ui/tests/unit/risk-detail-suspicious-filter-linkage.test.js`
|
||||
|
||||
本次未修改后端接口、SQL 或查询参数结构。查询仍沿用 `suspiciousType` 与 `modelCode` 两个参数组合。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- Node 环境:`.nvmrc` 为 14.21.3;当前 PowerShell 下 `nvm use` 未自动读取版本,且本机已知 Node 14 安装缺少 npm,因此按项目约定使用已验证可用的 Node 22.22.3 / npm 10.9.8。
|
||||
- 已执行静态单测:
|
||||
- `node tests/unit/risk-detail-suspicious-transaction-layout.test.js`
|
||||
- `node tests/unit/risk-detail-suspicious-filter-linkage.test.js`
|
||||
- `node tests/unit/project-overview-api.test.js`
|
||||
- 已启动前端开发服务并编译成功:`npm run dev`,端口 `8088`。
|
||||
- 已使用系统 Chrome 通过 Playwright CLI 打开实际应用入口并截图:`output/playwright/risk-detail-filter-home-independent.png`。
|
||||
- 浏览器验证限制:当前后端 `62318` 未监听,实际应用入口停留在登录页并提示“系统接口500异常”,因此未进入涉疑交易明细业务页做端到端交互验证。
|
||||
- 测试后已关闭本次启动的前端开发进程,`8088` 未继续监听。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 流水打标规则口径调整后端实施记录
|
||||
|
||||
## 修改背景
|
||||
|
||||
对现有流水打标规则进行审查后,确认需要优先处理两个后端口径问题:
|
||||
|
||||
1. 年流水交易额使用系统当前日期 `CURDATE()` 作为近一年锚点,历史流水项目重复跑模型时结果会随运行日期漂移。
|
||||
2. 物业缴费与房产登记不匹配规则仅判断是否存在任意房产登记,且关键词口径要兼顾召回和误报控制。
|
||||
|
||||
外部人员夜间交易属于早期残留规则,不属于当前最终外部人员四类口径,本次不调整。
|
||||
|
||||
## 修改内容
|
||||
|
||||
1. 年流水交易额口径
|
||||
- `ANNUAL_TURNOVER` 改为以当前项目内最大交易日期作为近一年锚点。
|
||||
- `EXTERNAL_ANNUAL_TURNOVER` 同步改为以当前项目内最大交易日期作为近一年锚点。
|
||||
- 不再依赖系统当前日期 `CURDATE()`。
|
||||
|
||||
2. 物业缴费与房产登记不匹配口径
|
||||
- 物业候选关键词收敛为 `物业、物管、业委会、业主委员会、维修基金、住宅专项维修资金、房屋维修资金`。
|
||||
- 去掉 `中心、社区、大厦` 等泛词,降低普通转账或机构名称误命中的概率。
|
||||
- 使用 `ccdi_asset_info.asset_name` 与流水对手方名称、摘要做名称匹配。
|
||||
- 若已登记正常住宅资产名称能匹配流水对手方或摘要,则不再判定为“物业缴费与房产登记不匹配”。
|
||||
- 命中原因由“名下无房产登记”调整为“名下无匹配房产登记”。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响员工年流水交易额超限规则。
|
||||
- 影响外部人员年流水交易额超限规则。
|
||||
- 影响物业缴费与房产登记不匹配规则。
|
||||
- 不新增模型参数,不改数据库结构,不改数据库脚本。
|
||||
- 不调整外部人员夜间交易残留规则。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 已补充 Mapper XML 单测断言,覆盖近一年锚点和物业资产名称匹配。
|
||||
- `mvn -pl ccdi-project -DskipTests compile` 执行通过。
|
||||
- 已执行 Mapper 静态校验,确认年流水规则不再包含 `CURDATE()`,物业规则使用收敛关键词并保留资产名称匹配,外部夜间交易规则未被调整。
|
||||
- 尝试执行 `mvn -pl ccdi-project "-Dtest=CcdiBankTagAnalysisMapperXmlTest" test`,但在 `testCompile` 阶段被既有无关测试编译错误阻断:`CcdiFileUploadServiceImplTest` 仍使用旧版 `LsfxAnalysisClient.uploadFile` 参数签名,`CcdiBankStatementTest` 仍调用当前 `BankStatementItem` 不存在的旧 setter。
|
||||
@@ -0,0 +1,35 @@
|
||||
# 疑似敏感交易最低金额门槛后端实施记录
|
||||
|
||||
## 修改背景
|
||||
|
||||
疑似敏感交易原先只要支出流水摘要或对手方命中赌博、游戏、彩票等敏感关键词,且支出金额大于 0 即会命中,导致 20 元、50 元等小额支出也进入疑似敏感交易明细。
|
||||
|
||||
参考本地知识库中 LSFX 的个人敏感支出口径,敏感类规则通常会结合金额条件控制误报。本次按业务确认,不新增模型参数,不改数据库脚本,直接在后端规则查询中固定最低金额门槛。
|
||||
|
||||
## 修改内容
|
||||
|
||||
1. 员工疑似敏感交易 `GAMBLING_SENSITIVE_KEYWORD`
|
||||
- 命中条件由 `AMOUNT_DR > 0` 调整为 `AMOUNT_DR >= 200`。
|
||||
- 命中原因追加“达到敏感交易最低金额 200 元”。
|
||||
|
||||
2. 外部人员疑似赌博摘要 `EXTERNAL_GAMBLING_MEMO`
|
||||
- 命中条件由交易金额大于 0 调整为交易金额 `>= 200`。
|
||||
- 交易金额仍按收入、支出两侧最大值判断。
|
||||
- 命中原因追加“达到敏感交易最低金额 200 元”。
|
||||
|
||||
3. 后端单测
|
||||
- 增加 Mapper XML 断言,确保两类敏感关键词规则都保留 200 元最低金额门槛。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响员工疑似赌博模型下的“疑似敏感交易”流水命中。
|
||||
- 影响外部人员可疑赌博模型下的“外部人员疑似赌博摘要”流水命中。
|
||||
- 不新增模型参数,不改 `ccdi_model_param`,不需要执行数据库脚本。
|
||||
- 低于 200 元的敏感词流水不再命中上述两类规则。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 已补充后端 Mapper XML 单测断言。
|
||||
- `mvn -pl ccdi-project -DskipTests compile` 执行通过。
|
||||
- 已执行 Mapper 静态校验,确认员工疑似敏感交易和外部人员疑似赌博摘要均包含 `>= 200` 最低金额条件。
|
||||
- 尝试执行 `mvn -pl ccdi-project "-Dtest=CcdiBankTagAnalysisMapperXmlTest,BankTagRuleConfigResolverTest,CcdiBankTagServiceImplTest" test`,但在 `testCompile` 阶段被既有无关测试编译错误阻断:`CcdiFileUploadServiceImplTest` 仍使用旧版 `LsfxAnalysisClient.uploadFile` 参数签名,`CcdiBankStatementTest` 仍调用当前 `BankStatementItem` 不存在的旧 setter。
|
||||
@@ -370,7 +370,7 @@
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="风险等级">
|
||||
<el-select v-model="form.txnRiskLevel" style="width: 100%" :disabled="analysisReadonly">
|
||||
<el-select v-model="form.txnRiskLevel" style="width: 100%" :disabled="riskLevelReadonly">
|
||||
<el-option label="LOW" value="LOW" />
|
||||
<el-option label="MEDIUM" value="MEDIUM" />
|
||||
<el-option label="HIGH" value="HIGH" />
|
||||
@@ -503,6 +503,12 @@ function createEmptyForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function applyIntermediaryRiskLevel(form) {
|
||||
if (form.ownerType === 'INTERMEDIARY') {
|
||||
form.txnRiskLevel = 'HIGH'
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'CcdiAccountInfo',
|
||||
components: { ImportResultDialog },
|
||||
@@ -587,6 +593,9 @@ export default {
|
||||
isRelationOwner() {
|
||||
return this.form.ownerType === 'RELATION'
|
||||
},
|
||||
isIntermediaryOwner() {
|
||||
return this.form.ownerType === 'INTERMEDIARY'
|
||||
},
|
||||
isInternalOwner() {
|
||||
return this.isEmployeeOwner || this.isRelationOwner
|
||||
},
|
||||
@@ -602,6 +611,9 @@ export default {
|
||||
analysisReadonly() {
|
||||
return this.dialogMode === 'detail' || this.form.bankScope === 'INTERNAL'
|
||||
},
|
||||
riskLevelReadonly() {
|
||||
return this.analysisReadonly || this.isIntermediaryOwner
|
||||
},
|
||||
dialogTitle() {
|
||||
if (this.dialogMode === 'detail') return '账户详情'
|
||||
if (this.dialogMode === 'edit') return '编辑账户'
|
||||
@@ -750,6 +762,9 @@ export default {
|
||||
relationId: data.relationId || undefined,
|
||||
invalidDate: data.invalidDate || ''
|
||||
})
|
||||
if (mode !== 'detail') {
|
||||
applyIntermediaryRiskLevel(this.form)
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.clearDialogValidate())
|
||||
},
|
||||
@@ -776,6 +791,7 @@ export default {
|
||||
this.form.accountName = ''
|
||||
this.form.ownerId = ''
|
||||
}
|
||||
applyIntermediaryRiskLevel(this.form)
|
||||
},
|
||||
async handleStaffChange(staffId) {
|
||||
const staff = this.staffOptions.find(item => item.staffId === staffId)
|
||||
@@ -802,6 +818,7 @@ export default {
|
||||
this.form.accountName = relation.relationName
|
||||
},
|
||||
buildPayload() {
|
||||
applyIntermediaryRiskLevel(this.form)
|
||||
return {
|
||||
id: this.form.id,
|
||||
ownerType: this.form.ownerType,
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="可疑规则">
|
||||
<el-form-item label="预警类型">
|
||||
<el-select
|
||||
v-model="currentSuspiciousType"
|
||||
class="suspicious-filter-select suspicious-type-select"
|
||||
placeholder="全部可疑规则"
|
||||
placeholder="全部预警类型"
|
||||
@change="handleSuspiciousTypeSelectChange"
|
||||
>
|
||||
<el-option
|
||||
@@ -369,7 +369,7 @@ import { getBankStatementDetail } from "@/api/ccdiProjectBankStatement";
|
||||
import { buildFlowEvidenceFingerprint, buildFlowEvidenceSnapshot } from "@/utils/ccdiEvidence";
|
||||
|
||||
const SUSPICIOUS_TYPE_OPTIONS = [
|
||||
{ value: "ALL", label: "全部可疑规则" },
|
||||
{ value: "ALL", label: "全部预警类型" },
|
||||
{ value: "MODEL_RULE", label: "员工规则命中" },
|
||||
{ value: "EXTERNAL_PERSON", label: "外部人员预警" },
|
||||
];
|
||||
@@ -404,6 +404,23 @@ const normalizeRiskModelOptions = (rows) => {
|
||||
});
|
||||
};
|
||||
|
||||
const mergeRiskModelOptions = (...optionGroups) => {
|
||||
const seen = new Set();
|
||||
return optionGroups
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
if (!item || !item.value || seen.has(item.value)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(item.value);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const isExternalModelCode = (value) => (
|
||||
typeof value === "string" && value.indexOf("EXTERNAL_") === 0
|
||||
);
|
||||
|
||||
const ABNORMAL_CUSTOMER_TRANSACTION_RULE_CODE = "ABNORMAL_CUSTOMER_TRANSACTION";
|
||||
const ABNORMAL_CUSTOMER_TRANSACTION_RULE_NAME = "与客户之间非正常资金往来";
|
||||
|
||||
@@ -542,7 +559,13 @@ export default {
|
||||
return this.currentSuspiciousType === "EXTERNAL_PERSON";
|
||||
},
|
||||
displayRiskModelOptions() {
|
||||
return this.isExternalSuspiciousType ? EXTERNAL_RISK_MODEL_OPTIONS : this.riskModelOptions;
|
||||
if (this.isExternalSuspiciousType) {
|
||||
return EXTERNAL_RISK_MODEL_OPTIONS;
|
||||
}
|
||||
if (this.currentSuspiciousType === "ALL") {
|
||||
return mergeRiskModelOptions(this.riskModelOptions, EXTERNAL_RISK_MODEL_OPTIONS);
|
||||
}
|
||||
return this.riskModelOptions;
|
||||
},
|
||||
allModelOptionLabel() {
|
||||
return this.isExternalSuspiciousType ? "全部外部模型" : "全部模型";
|
||||
@@ -589,7 +612,7 @@ export default {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.selectedModelCode && this.selectedModelCode.indexOf("EXTERNAL_") === 0) {
|
||||
if (value === "MODEL_RULE" && isExternalModelCode(this.selectedModelCode)) {
|
||||
this.selectedModelCode = "";
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
"../../src/views/ccdiProject/components/detail/RiskDetailSection.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
[
|
||||
'const mergeRiskModelOptions = (...optionGroups) => {',
|
||||
'const isExternalModelCode = (value) => (',
|
||||
'if (this.currentSuspiciousType === "ALL") {',
|
||||
'return mergeRiskModelOptions(this.riskModelOptions, EXTERNAL_RISK_MODEL_OPTIONS);',
|
||||
'if (value === "MODEL_RULE" && isExternalModelCode(this.selectedModelCode)) {',
|
||||
'this.selectedModelCode = "";',
|
||||
].forEach((token) => assert(source.includes(token), token));
|
||||
|
||||
[
|
||||
'@change="handleModelSelectChange"',
|
||||
'handleModelSelectChange(value) {',
|
||||
'this.currentSuspiciousType = "EXTERNAL_PERSON";',
|
||||
'label="可疑规则"',
|
||||
'placeholder="全部可疑规则"',
|
||||
'{ value: "ALL", label: "全部可疑规则" }',
|
||||
].forEach((token) => assert(!source.includes(token), token));
|
||||
@@ -12,9 +12,10 @@ const source = fs.readFileSync(
|
||||
|
||||
[
|
||||
"涉疑交易明细",
|
||||
"全部可疑人员类型",
|
||||
"名单库命中",
|
||||
"模型规则命中",
|
||||
"预警类型",
|
||||
"全部预警类型",
|
||||
"员工规则命中",
|
||||
"外部人员预警",
|
||||
"本方账户",
|
||||
"对方账户",
|
||||
"关联员工",
|
||||
@@ -29,6 +30,7 @@ const source = fs.readFileSync(
|
||||
].forEach((token) => assert(source.includes(token), token));
|
||||
|
||||
[
|
||||
"label=\"可疑规则\"",
|
||||
"label=\"可疑人员\"",
|
||||
"prop=\"suspiciousPersonName\"",
|
||||
"label=\"关联人\"",
|
||||
|
||||
Reference in New Issue
Block a user