账户库中介风险等级固定为高
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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,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` 不可用,按仓库规则改用已验证可用版本。
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user