Compare commits
84 Commits
979ed9669f
...
codex/pull
| Author | SHA1 | Date | |
|---|---|---|---|
| 75d094dad4 | |||
| 1561df924f | |||
| 657977dfb7 | |||
| 4a75d68bf6 | |||
| b70f967153 | |||
| 6492fca2dd | |||
| e0188b5e5b | |||
| c3b56bdf2a | |||
| 6d90bb4e58 | |||
| 4a7cfde4af | |||
| 15aa9117c3 | |||
| ded07a2c33 | |||
| 7a317204f7 | |||
| 2c6c982ed3 | |||
| 486a499af7 | |||
| c999497693 | |||
| 3f3e9268f2 | |||
| 9c02812675 | |||
| b7e9a8da03 | |||
| bb41fd7e89 | |||
| 8b9226643c | |||
| 6d2f40843a | |||
| 21c4e91f41 | |||
| 87fb6443e6 | |||
| ce66dc3ba8 | |||
| f3c1e2ea93 | |||
| 87b2352001 | |||
| 999350265b | |||
| f8ee1ecf1c | |||
| 35467fd361 | |||
| 64cb847db3 | |||
| bf290c509c | |||
| c5b2033a3d | |||
| d45e9410ef | |||
| 457e6c1d27 | |||
| 850f97ea22 | |||
| de6e6bd628 | |||
| 3a867e5857 | |||
| 19a60c987e | |||
| 7ce721ef93 | |||
| 000e8698a5 | |||
| 9d3e8beceb | |||
| 0ea504f6b3 | |||
| a39594faf8 | |||
| 1b45296df3 | |||
| 1fadb38d99 | |||
| 9917d10e59 | |||
| be443d1b31 | |||
| b822cc202e | |||
| 598f5dec1c | |||
| 0bf73a923f | |||
| ec67794f88 | |||
| 3ef45bc398 | |||
| 37e17ac903 | |||
| d561d068d6 | |||
| 43bc0e4f65 | |||
| 3fe78d8d3a | |||
| 4c58966529 | |||
| 3bc60fedeb | |||
| 4d1acc7484 | |||
| 402a0c3e2f | |||
| 5980ed0790 | |||
| 75cb8967da | |||
| 90a5c42313 | |||
| 356bcdd6de | |||
| 9a60371a8f | |||
| 380f9b4e7a | |||
| 928f65dfca | |||
| c64146ac40 | |||
| 0541ce0ac6 | |||
| 26c639134e | |||
| 0f7b57e824 | |||
| 104e8697fe | |||
| bbc6a2050b | |||
| bf7a4c0538 | |||
| b2e177dd24 | |||
| 2071d04c08 | |||
| 4988ab5944 | |||
| c00d5475e6 | |||
| 0b64532959 | |||
| 9f0ad4ce87 | |||
| 75b5989774 | |||
| d8c069a836 | |||
| 26be75adad |
@@ -39,6 +39,10 @@ public class CcdiAccountInfoQueryDTO implements Serializable {
|
||||
@Schema(description = "账户姓名")
|
||||
private String accountName;
|
||||
|
||||
/** 账户号码 */
|
||||
@Schema(description = "账户号码")
|
||||
private String accountNo;
|
||||
|
||||
/** 账户类型 */
|
||||
@Schema(description = "账户类型")
|
||||
private String accountType;
|
||||
|
||||
@@ -28,6 +28,14 @@ public interface CcdiBaseStaffMapper extends BaseMapper<CcdiBaseStaff> {
|
||||
Page<CcdiBaseStaffVO> selectBaseStaffPageWithDept(@Param("page") Page<CcdiBaseStaffVO> page,
|
||||
@Param("query") CcdiBaseStaffQueryDTO queryDTO);
|
||||
|
||||
/**
|
||||
* 查询员工详情(包含部门名称)
|
||||
*
|
||||
* @param staffId 员工ID
|
||||
* @return 员工详情
|
||||
*/
|
||||
CcdiBaseStaffVO selectBaseStaffByIdWithDept(@Param("staffId") Long staffId);
|
||||
|
||||
int insertOrUpdateBatch(@Param("list") List<CcdiBaseStaff> list);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -119,10 +119,10 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
|
||||
*/
|
||||
@Override
|
||||
public CcdiBaseStaffVO selectBaseStaffById(Long staffId) {
|
||||
CcdiBaseStaff staff = baseStaffMapper.selectById(staffId);
|
||||
CcdiBaseStaffVO vo = convertToVO(staff);
|
||||
if (staff != null) {
|
||||
vo.setAssetInfoList(assetInfoService.selectByFamilyIdAndPersonId(staff.getIdCard(), staff.getIdCard()).stream().map(asset -> {
|
||||
CcdiBaseStaffVO vo = baseStaffMapper.selectBaseStaffByIdWithDept(staffId);
|
||||
if (vo != null) {
|
||||
vo.setStatusDesc(EmployeeStatus.getDescByCode(vo.getStatus()));
|
||||
vo.setAssetInfoList(assetInfoService.selectByFamilyIdAndPersonId(vo.getIdCard(), vo.getIdCard()).stream().map(asset -> {
|
||||
CcdiAssetInfoVO assetInfoVO = new CcdiAssetInfoVO();
|
||||
BeanUtils.copyProperties(asset, assetInfoVO);
|
||||
return assetInfoVO;
|
||||
|
||||
@@ -104,6 +104,9 @@
|
||||
<if test="query.accountName != null and query.accountName != ''">
|
||||
AND ai.account_name LIKE CONCAT('%', #{query.accountName}, '%')
|
||||
</if>
|
||||
<if test="query.accountNo != null and query.accountNo != ''">
|
||||
AND ai.account_no LIKE CONCAT('%', #{query.accountNo}, '%')
|
||||
</if>
|
||||
<if test="query.accountType != null and query.accountType != ''">
|
||||
AND ai.account_type = #{query.accountType}
|
||||
</if>
|
||||
|
||||
@@ -42,7 +42,16 @@
|
||||
AND e.status = #{query.status}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY e.create_time DESC, e.staff_id DESC
|
||||
ORDER BY e.hire_date DESC, e.staff_id DESC
|
||||
</select>
|
||||
|
||||
<select id="selectBaseStaffByIdWithDept" resultMap="CcdiBaseStaffVOResult">
|
||||
SELECT
|
||||
e.staff_id, e.name, e.dept_id, e.id_card, e.phone, e.annual_income, e.hire_date, e.is_party_member, e.status, e.create_time,
|
||||
d.dept_name
|
||||
FROM ccdi_base_staff e
|
||||
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id
|
||||
WHERE e.staff_id = #{staffId}
|
||||
</select>
|
||||
|
||||
<!-- 批量插入或更新员工信息(只更新非null字段) -->
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.ruoyi.info.collection.controller;
|
||||
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
|
||||
import com.ruoyi.info.collection.domain.vo.BaseStaffImportSubmitResultVO;
|
||||
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
|
||||
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
|
||||
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
|
||||
import com.ruoyi.info.collection.utils.EasyExcelUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiBaseStaffControllerTest {
|
||||
|
||||
@InjectMocks
|
||||
private CcdiBaseStaffController controller;
|
||||
|
||||
@Mock
|
||||
private ICcdiBaseStaffService baseStaffService;
|
||||
|
||||
@Mock
|
||||
private ICcdiBaseStaffImportService importAsyncService;
|
||||
|
||||
@Mock
|
||||
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
|
||||
|
||||
@Test
|
||||
void importTemplate_shouldDownloadDualSheetTemplate() {
|
||||
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||
controller.importTemplate(null);
|
||||
|
||||
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(
|
||||
null,
|
||||
CcdiBaseStaffExcel.class,
|
||||
"员工信息",
|
||||
CcdiBaseStaffAssetInfoExcel.class,
|
||||
"员工资产信息",
|
||||
"员工信息维护导入模板"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void importData_shouldWarnWhenBothSheetsAreEmpty() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"base-staff-empty.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"empty".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息")))
|
||||
.thenReturn(List.of());
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
|
||||
.thenReturn(List.of());
|
||||
|
||||
AjaxResult result = controller.importData(file);
|
||||
|
||||
assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
|
||||
assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG));
|
||||
verifyNoInteractions(baseStaffService);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void importData_shouldSubmitOnlyStaffTaskWhenOnlyStaffSheetHasRows() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"base-staff.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"staff".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
CcdiBaseStaffExcel staffExcel = new CcdiBaseStaffExcel();
|
||||
staffExcel.setStaffId(1001L);
|
||||
BaseStaffImportSubmitResultVO submitResult = new BaseStaffImportSubmitResultVO();
|
||||
submitResult.setStaffTaskId("staff-task-1");
|
||||
when(baseStaffService.importBaseStaffWithAssets(List.of(staffExcel), List.of())).thenReturn(submitResult);
|
||||
|
||||
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息")))
|
||||
.thenReturn(List.of(staffExcel));
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
|
||||
.thenReturn(List.of());
|
||||
|
||||
AjaxResult result = controller.importData(file);
|
||||
|
||||
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
|
||||
assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG));
|
||||
Object data = result.get(AjaxResult.DATA_TAG);
|
||||
assertEquals("staff-task-1", data.getClass().getMethod("getStaffTaskId").invoke(data));
|
||||
assertNull(data.getClass().getMethod("getAssetTaskId").invoke(data));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void importData_shouldSubmitTwoTasksWhenBothSheetsHaveRows() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"base-staff-both.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"both".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
CcdiBaseStaffExcel staffExcel = new CcdiBaseStaffExcel();
|
||||
staffExcel.setStaffId(1002L);
|
||||
CcdiBaseStaffAssetInfoExcel assetExcel = new CcdiBaseStaffAssetInfoExcel();
|
||||
assetExcel.setPersonId("320101199001010011");
|
||||
BaseStaffImportSubmitResultVO submitResult = new BaseStaffImportSubmitResultVO();
|
||||
submitResult.setStaffTaskId("staff-task-2");
|
||||
submitResult.setAssetTaskId("asset-task-2");
|
||||
when(baseStaffService.importBaseStaffWithAssets(List.of(staffExcel), List.of(assetExcel))).thenReturn(submitResult);
|
||||
|
||||
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息")))
|
||||
.thenReturn(List.of(staffExcel));
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
|
||||
.thenReturn(List.of(assetExcel));
|
||||
|
||||
AjaxResult result = controller.importData(file);
|
||||
|
||||
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
|
||||
Object data = result.get(AjaxResult.DATA_TAG);
|
||||
assertEquals("staff-task-2", data.getClass().getMethod("getStaffTaskId").invoke(data));
|
||||
assertEquals("asset-task-2", data.getClass().getMethod("getAssetTaskId").invoke(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ruoyi.info.collection.controller;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.info.collection.domain.vo.EnumOptionVO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
class CcdiEnumControllerTest {
|
||||
|
||||
private final CcdiEnumController controller = new CcdiEnumController();
|
||||
|
||||
@Test
|
||||
void getEnterpriseRiskLevelOptions_shouldReturnConfiguredOptions() {
|
||||
AjaxResult result = controller.getEnterpriseRiskLevelOptions();
|
||||
List<?> data = (List<?>) result.get("data");
|
||||
|
||||
assertEquals(3, data.size());
|
||||
EnumOptionVO first = (EnumOptionVO) data.get(0);
|
||||
assertEquals("1", first.getValue());
|
||||
assertEquals("高风险", first.getLabel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEnterpriseSourceOptions_shouldReturnConfiguredOptions() {
|
||||
AjaxResult result = controller.getEnterpriseSourceOptions();
|
||||
List<?> data = (List<?>) result.get("data");
|
||||
|
||||
assertFalse(data.isEmpty());
|
||||
EnumOptionVO first = (EnumOptionVO) data.get(0);
|
||||
assertEquals("GENERAL", first.getValue());
|
||||
assertEquals("一般企业", first.getLabel());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.ruoyi.info.collection.controller;
|
||||
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
|
||||
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO;
|
||||
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO;
|
||||
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
|
||||
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
|
||||
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationService;
|
||||
import com.ruoyi.info.collection.utils.EasyExcelUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiStaffFmyRelationControllerTest {
|
||||
|
||||
@InjectMocks
|
||||
private CcdiStaffFmyRelationController controller;
|
||||
|
||||
@Mock
|
||||
private ICcdiStaffFmyRelationService relationService;
|
||||
|
||||
@Mock
|
||||
private ICcdiStaffFmyRelationImportService relationImportService;
|
||||
|
||||
@Mock
|
||||
private ICcdiAssetInfoImportService assetInfoImportService;
|
||||
|
||||
@Test
|
||||
void importTemplate_shouldDownloadDualSheetTemplate() {
|
||||
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||
controller.importTemplate(null);
|
||||
|
||||
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(
|
||||
null,
|
||||
CcdiStaffFmyRelationExcel.class,
|
||||
"员工亲属关系信息",
|
||||
CcdiAssetInfoExcel.class,
|
||||
"亲属资产信息",
|
||||
"员工亲属关系维护导入模板"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void importData_shouldErrorWhenBothSheetsAreEmpty() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"staff-family-empty.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"empty".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
|
||||
.thenReturn(List.of());
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
|
||||
.thenReturn(List.of());
|
||||
|
||||
AjaxResult result = controller.importData(file);
|
||||
|
||||
assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
|
||||
assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG));
|
||||
verifyNoInteractions(relationService, assetInfoImportService);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void importData_shouldSubmitOnlyRelationTaskWhenOnlyRelationSheetHasRows() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"staff-family-relation.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"relation".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
CcdiStaffFmyRelationExcel relationExcel = new CcdiStaffFmyRelationExcel();
|
||||
relationExcel.setPersonId("320101199001010011");
|
||||
StaffFmyRelationImportSubmitResultVO submitResult = new StaffFmyRelationImportSubmitResultVO();
|
||||
submitResult.setRelationTaskId("relation-task-1");
|
||||
when(relationService.importRelationWithAssets(List.of(relationExcel), List.of())).thenReturn(submitResult);
|
||||
|
||||
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
|
||||
.thenReturn(List.of(relationExcel));
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
|
||||
.thenReturn(List.of());
|
||||
|
||||
AjaxResult result = controller.importData(file);
|
||||
|
||||
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
|
||||
assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG));
|
||||
Object data = result.get(AjaxResult.DATA_TAG);
|
||||
assertEquals("relation-task-1", data.getClass().getMethod("getRelationTaskId").invoke(data));
|
||||
assertNull(data.getClass().getMethod("getAssetTaskId").invoke(data));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void importData_shouldSubmitTwoTasksWhenBothSheetsHaveRows() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file",
|
||||
"staff-family-both.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"both".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
CcdiStaffFmyRelationExcel relationExcel = new CcdiStaffFmyRelationExcel();
|
||||
relationExcel.setPersonId("320101199001010012");
|
||||
CcdiAssetInfoExcel assetExcel = new CcdiAssetInfoExcel();
|
||||
assetExcel.setPersonId("320101199001010099");
|
||||
StaffFmyRelationImportSubmitResultVO submitResult = new StaffFmyRelationImportSubmitResultVO();
|
||||
submitResult.setRelationTaskId("relation-task-2");
|
||||
submitResult.setAssetTaskId("asset-task-2");
|
||||
when(relationService.importRelationWithAssets(List.of(relationExcel), List.of(assetExcel))).thenReturn(submitResult);
|
||||
|
||||
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
|
||||
.thenReturn(List.of(relationExcel));
|
||||
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
|
||||
.thenReturn(List.of(assetExcel));
|
||||
|
||||
AjaxResult result = controller.importData(file);
|
||||
|
||||
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
|
||||
Object data = result.get(AjaxResult.DATA_TAG);
|
||||
assertEquals("relation-task-2", data.getClass().getMethod("getRelationTaskId").invoke(data));
|
||||
assertEquals("asset-task-2", data.getClass().getMethod("getAssetTaskId").invoke(data));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getImportFailures_shouldReturnPagedRowsWithSheetAndRowInfo() {
|
||||
StaffFmyRelationImportFailureVO failure1 = new StaffFmyRelationImportFailureVO();
|
||||
failure1.setSheetName("员工亲属关系信息");
|
||||
failure1.setRowNum(2);
|
||||
failure1.setPersonId("A1");
|
||||
|
||||
StaffFmyRelationImportFailureVO failure2 = new StaffFmyRelationImportFailureVO();
|
||||
failure2.setSheetName("员工亲属关系信息");
|
||||
failure2.setRowNum(3);
|
||||
failure2.setPersonId("A2");
|
||||
|
||||
when(relationImportService.getImportFailures("task-1")).thenReturn(List.of(failure1, failure2));
|
||||
|
||||
TableDataInfo result = controller.getImportFailures("task-1", 2, 1);
|
||||
|
||||
assertEquals(2, result.getTotal());
|
||||
assertEquals(1, result.getRows().size());
|
||||
StaffFmyRelationImportFailureVO row = (StaffFmyRelationImportFailureVO) result.getRows().get(0);
|
||||
assertEquals("员工亲属关系信息", row.getSheetName());
|
||||
assertEquals(3, row.getRowNum());
|
||||
assertEquals("A2", row.getPersonId());
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,18 @@ class CcdiAccountInfoMapperTest {
|
||||
assertTrue(sql.contains("ai.owner_type <> 'credit_customer'"), sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectAccountInfoPage_shouldFilterByAccountNo() throws Exception {
|
||||
MappedStatement mappedStatement = loadMappedStatement(
|
||||
"com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper.selectAccountInfoPage");
|
||||
CcdiAccountInfoQueryDTO queryDTO = new CcdiAccountInfoQueryDTO();
|
||||
queryDTO.setAccountNo("6222");
|
||||
|
||||
String sql = renderSql(mappedStatement, Map.of("query", queryDTO)).toLowerCase();
|
||||
|
||||
assertTrue(sql.contains("ai.account_no like concat('%', ?, '%')"), sql);
|
||||
}
|
||||
|
||||
private MappedStatement loadMappedStatement(String statementId) throws Exception {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ruoyi.info.collection.mapper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiEnterpriseBaseInfoMapperTest {
|
||||
|
||||
@Test
|
||||
void mapperXml_shouldContainPageQueryAndImportColumns() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader()
|
||||
.getResourceAsStream("mapper/info/collection/CcdiEnterpriseBaseInfoMapper.xml")) {
|
||||
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(xml.contains("selectEnterpriseBaseInfoPage"), xml);
|
||||
assertTrue(xml.contains("risk_level"), xml);
|
||||
assertTrue(xml.contains("ent_source"), xml);
|
||||
assertTrue(xml.contains("data_source"), xml);
|
||||
assertTrue(xml.contains("ORDER BY create_time DESC"), xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.info.collection.mapper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiIntermediaryMapperTest {
|
||||
|
||||
@Test
|
||||
void mapperXml_shouldContainThreeRecordTypesAndRelatedKeywordQuery() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader()
|
||||
.getResourceAsStream("mapper/info/collection/CcdiIntermediaryMapper.xml")) {
|
||||
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(xml.contains("INTERMEDIARY"), xml);
|
||||
assertTrue(xml.contains("RELATIVE"), xml);
|
||||
assertTrue(xml.contains("ENTERPRISE_RELATION"), xml);
|
||||
assertTrue(xml.contains("relatedIntermediaryKeyword"), xml);
|
||||
assertTrue(xml.contains("related_intermediary_name"), xml);
|
||||
assertTrue(xml.contains("relation_text"), xml);
|
||||
assertTrue(xml.contains("CAST('实体'"), xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,163 @@
|
||||
package com.ruoyi.info.collection.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysDept;
|
||||
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
|
||||
import com.ruoyi.info.collection.domain.vo.ImportFailureVO;
|
||||
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffImportServiceImpl;
|
||||
import com.ruoyi.system.mapper.SysDeptMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiBaseStaffDualImportServiceTest {
|
||||
|
||||
@InjectMocks
|
||||
private CcdiBaseStaffImportServiceImpl service;
|
||||
|
||||
@Mock
|
||||
private com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper baseStaffMapper;
|
||||
|
||||
@Mock
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Mock
|
||||
private HashOperations<String, Object, Object> hashOperations;
|
||||
|
||||
@Mock
|
||||
private ValueOperations<String, Object> valueOperations;
|
||||
|
||||
@Mock
|
||||
private SysDeptMapper deptMapper;
|
||||
|
||||
@Test
|
||||
void importBaseStaffAsync_shouldTreatExistingEmployeeAsFailureInsteadOfUpdate() {
|
||||
CcdiBaseStaffExcel excel = new CcdiBaseStaffExcel();
|
||||
excel.setStaffId(1001L);
|
||||
excel.setName("张三");
|
||||
excel.setDeptId(10L);
|
||||
excel.setIdCard("11010519491231002X");
|
||||
excel.setPhone("13812345678");
|
||||
excel.setStatus("0");
|
||||
excel.setPartyMember(1);
|
||||
|
||||
CcdiBaseStaff existing = new CcdiBaseStaff();
|
||||
existing.setStaffId(1001L);
|
||||
existing.setIdCard("11010519491231002X");
|
||||
|
||||
when(baseStaffMapper.selectBatchIds(List.of(1001L))).thenReturn(List.of(existing));
|
||||
when(baseStaffMapper.selectList(any())).thenReturn(List.of(existing));
|
||||
lenient().when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
|
||||
|
||||
service.importBaseStaffAsync(List.of(excel), "task-existing");
|
||||
|
||||
verify(baseStaffMapper, never()).insertBatch(any());
|
||||
verify(baseStaffMapper, never()).insertOrUpdateBatch(any());
|
||||
|
||||
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
|
||||
verify(valueOperations).set(eq("import:baseStaff:task-existing:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
|
||||
ImportFailureVO failure = (ImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
|
||||
assertEquals("员工信息", failure.getSheetName());
|
||||
assertEquals(2, failure.getRowNum());
|
||||
assertEquals(1001L, failure.getStaffId());
|
||||
assertEquals("该员工ID已存在", failure.getErrorMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateStaffData_shouldRejectExistingIdCardWhenStaffIdDoesNotExist() {
|
||||
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
|
||||
|
||||
RuntimeException exception = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
RuntimeException.class,
|
||||
() -> service.validateStaffData(buildExcelDto(), Set.of(), Set.of("11010519491231002X"))
|
||||
);
|
||||
|
||||
assertEquals("该身份证号已存在", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importBaseStaffAsync_shouldSaveFailureWhenDeptIsInvalid() {
|
||||
CcdiBaseStaffExcel validExcel = buildExcel(1001L, 10L, "11010519491231002X");
|
||||
CcdiBaseStaffExcel invalidExcel = buildExcel(1002L, 99L, "320101199001010014");
|
||||
|
||||
when(baseStaffMapper.selectBatchIds(List.of(1001L, 1002L))).thenReturn(List.of());
|
||||
when(baseStaffMapper.selectList(any())).thenReturn(List.of());
|
||||
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
|
||||
when(deptMapper.selectDeptById(99L)).thenReturn(null);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
|
||||
|
||||
service.importBaseStaffAsync(List.of(validExcel, invalidExcel), "task-invalid-dept");
|
||||
|
||||
verify(baseStaffMapper).insertBatch(any());
|
||||
|
||||
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
|
||||
verify(valueOperations).set(eq("import:baseStaff:task-invalid-dept:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
|
||||
ImportFailureVO failure = (ImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
|
||||
assertEquals("员工信息", failure.getSheetName());
|
||||
assertEquals(3, failure.getRowNum());
|
||||
assertEquals(1002L, failure.getStaffId());
|
||||
assertEquals("所属部门ID[99]不存在或已停用/删除,请检查机构号", failure.getErrorMessage());
|
||||
|
||||
ArgumentCaptor<Map<String, Object>> statusCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
verify(hashOperations).putAll(eq("import:baseStaff:task-invalid-dept"), statusCaptor.capture());
|
||||
assertEquals("PARTIAL_SUCCESS", statusCaptor.getValue().get("status"));
|
||||
assertEquals(1, statusCaptor.getValue().get("successCount"));
|
||||
assertEquals(1, statusCaptor.getValue().get("failureCount"));
|
||||
}
|
||||
|
||||
private com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO buildExcelDto() {
|
||||
com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO dto = new com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO();
|
||||
dto.setName("李四");
|
||||
dto.setStaffId(2001L);
|
||||
dto.setDeptId(10L);
|
||||
dto.setIdCard("11010519491231002X");
|
||||
dto.setPhone("13812345678");
|
||||
dto.setStatus("0");
|
||||
dto.setPartyMember(1);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private CcdiBaseStaffExcel buildExcel(Long staffId, Long deptId, String idCard) {
|
||||
CcdiBaseStaffExcel excel = new CcdiBaseStaffExcel();
|
||||
excel.setStaffId(staffId);
|
||||
excel.setName("张三");
|
||||
excel.setDeptId(deptId);
|
||||
excel.setIdCard(idCard);
|
||||
excel.setPhone("13812345678");
|
||||
excel.setStatus("0");
|
||||
excel.setPartyMember(1);
|
||||
return excel;
|
||||
}
|
||||
|
||||
private SysDept buildDept(Long deptId, String status, String delFlag) {
|
||||
SysDept dept = new SysDept();
|
||||
dept.setDeptId(deptId);
|
||||
dept.setDeptName("测试部门");
|
||||
dept.setStatus(status);
|
||||
dept.setDelFlag(delFlag);
|
||||
return dept;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ruoyi.info.collection.service;
|
||||
|
||||
import com.ruoyi.info.collection.domain.CcdiBizIntermediary;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiIntermediaryRelativeAddDTO;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiIntermediaryPersonAddDTO;
|
||||
import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiIntermediaryMapper;
|
||||
import com.ruoyi.info.collection.service.impl.CcdiIntermediaryServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiIntermediaryServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private CcdiIntermediaryServiceImpl service;
|
||||
|
||||
@Mock
|
||||
private CcdiBizIntermediaryMapper bizIntermediaryMapper;
|
||||
|
||||
@Mock
|
||||
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
|
||||
|
||||
@Mock
|
||||
private CcdiIntermediaryMapper intermediaryMapper;
|
||||
|
||||
@Mock
|
||||
private CcdiIntermediaryEnterpriseRelationMapper enterpriseRelationMapper;
|
||||
|
||||
@Mock
|
||||
private ICcdiIntermediaryPersonImportService personImportService;
|
||||
|
||||
@Mock
|
||||
private ICcdiIntermediaryEntityImportService entityImportService;
|
||||
|
||||
@Mock
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Test
|
||||
void insertIntermediaryPerson_shouldForceBenrenAndClearRelatedNumId() {
|
||||
CcdiIntermediaryPersonAddDTO addDTO = new CcdiIntermediaryPersonAddDTO();
|
||||
addDTO.setName("测试中介");
|
||||
addDTO.setPersonId("320101199001010011");
|
||||
addDTO.setPersonSubType("配偶");
|
||||
addDTO.setRelatedNumId("parent-id");
|
||||
|
||||
when(bizIntermediaryMapper.selectCount(any())).thenReturn(0L);
|
||||
when(bizIntermediaryMapper.insert(any(CcdiBizIntermediary.class))).thenReturn(1);
|
||||
|
||||
int result = service.insertIntermediaryPerson(addDTO);
|
||||
|
||||
assertEquals(1, result);
|
||||
ArgumentCaptor<CcdiBizIntermediary> captor = ArgumentCaptor.forClass(CcdiBizIntermediary.class);
|
||||
verify(bizIntermediaryMapper).insert(captor.capture());
|
||||
assertEquals("本人", captor.getValue().getPersonSubType());
|
||||
assertNull(captor.getValue().getRelatedNumId());
|
||||
assertEquals("MANUAL", captor.getValue().getDataSource());
|
||||
}
|
||||
|
||||
@Test
|
||||
void insertIntermediaryRelative_shouldRejectBenrenSubType() {
|
||||
CcdiBizIntermediary owner = new CcdiBizIntermediary();
|
||||
owner.setBizId("biz-1");
|
||||
owner.setPersonSubType("本人");
|
||||
|
||||
CcdiIntermediaryRelativeAddDTO addDTO = new CcdiIntermediaryRelativeAddDTO();
|
||||
addDTO.setName("测试亲属");
|
||||
addDTO.setPersonId("320101199001010022");
|
||||
addDTO.setPersonSubType("本人");
|
||||
|
||||
when(bizIntermediaryMapper.selectById("biz-1")).thenReturn(owner);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.insertIntermediaryRelative("biz-1", addDTO));
|
||||
|
||||
assertEquals("亲属关系不能为本人", exception.getMessage());
|
||||
verify(bizIntermediaryMapper, never()).insert(any(CcdiBizIntermediary.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteIntermediaryByIds_shouldDeleteRelativesAndEnterpriseRelationsWhenRemovingOwner() {
|
||||
CcdiBizIntermediary owner = new CcdiBizIntermediary();
|
||||
owner.setBizId("biz-1");
|
||||
owner.setPersonSubType("本人");
|
||||
|
||||
when(bizIntermediaryMapper.selectById("biz-1")).thenReturn(owner);
|
||||
when(bizIntermediaryMapper.delete(any())).thenReturn(2);
|
||||
when(enterpriseRelationMapper.delete(any())).thenReturn(1);
|
||||
when(bizIntermediaryMapper.deleteById("biz-1")).thenReturn(1);
|
||||
|
||||
int result = service.deleteIntermediaryByIds(new String[]{"biz-1"});
|
||||
|
||||
assertEquals(1, result);
|
||||
verify(bizIntermediaryMapper).delete(any());
|
||||
verify(enterpriseRelationMapper).delete(any());
|
||||
verify(bizIntermediaryMapper).deleteById("biz-1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.ruoyi.info.collection.service;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiPurchaseTransactionFeatureContractTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeSupplierListContractsAcrossPurchaseTransactionModels() throws Exception {
|
||||
assertHasField(
|
||||
"com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO",
|
||||
"supplierList"
|
||||
);
|
||||
assertHasField(
|
||||
"com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO",
|
||||
"supplierList"
|
||||
);
|
||||
assertHasField(
|
||||
"com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO",
|
||||
"supplierList"
|
||||
);
|
||||
assertHasField(
|
||||
"com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO",
|
||||
"supplierCount"
|
||||
);
|
||||
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier"));
|
||||
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionSupplierDTO"));
|
||||
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionSupplierVO"));
|
||||
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDefineSupplierSubTableAndBiddingMigrationScripts() throws Exception {
|
||||
String initSql = Files.readString(repoPath("sql/ccdi_purchase_transaction.sql"));
|
||||
assertTrue(initSql.contains("CREATE TABLE `ccdi_purchase_transaction_supplier`"));
|
||||
assertTrue(initSql.contains("`is_bid_winner`"));
|
||||
assertTrue(initSql.contains("`sort_order`"));
|
||||
assertTrue(initSql.contains("utf8mb4_general_ci"));
|
||||
|
||||
String menuSql = Files.readString(repoPath("sql/ccdi_purchase_transaction_menu.sql"));
|
||||
assertTrue(menuSql.contains("招投标信息维护"));
|
||||
|
||||
Path migrationPath = repoPath("sql/migration/2026-04-22-bidding-info-maintenance-supplier-detail.sql");
|
||||
assertTrue(Files.exists(migrationPath), "应提供招投标供应商明细迁移脚本");
|
||||
|
||||
String migrationSql = Files.readString(migrationPath);
|
||||
assertTrue(migrationSql.contains("ccdi_purchase_transaction_supplier"));
|
||||
assertTrue(migrationSql.contains("INSERT INTO ccdi_purchase_transaction_supplier"));
|
||||
assertTrue(migrationSql.contains("UPDATE sys_menu"));
|
||||
assertTrue(migrationSql.contains("招投标信息维护"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseTwoSheetTemplateForBiddingImport() throws Exception {
|
||||
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel"));
|
||||
|
||||
String controller = Files.readString(
|
||||
Path.of("src/main/java/com/ruoyi/info/collection/controller/CcdiPurchaseTransactionController.java")
|
||||
);
|
||||
assertTrue(controller.contains("招投标主信息"));
|
||||
assertTrue(controller.contains("供应商明细"));
|
||||
}
|
||||
|
||||
private void assertHasField(String className, String fieldName) throws Exception {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
Field field = clazz.getDeclaredField(fieldName);
|
||||
assertNotNull(field);
|
||||
}
|
||||
|
||||
private Path repoPath(String relativePath) {
|
||||
return Path.of("..", relativePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package com.ruoyi.info.collection.utils;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.DataValidation;
|
||||
import org.apache.poi.ss.usermodel.DataValidationConstraint;
|
||||
import org.apache.poi.ss.usermodel.DataValidationHelper;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class EasyExcelUtilImportDropdownValidationTest {
|
||||
|
||||
@Test
|
||||
void importExcel_shouldPassWhenAllDictDropdownColumnsKeepListValidation() throws Exception {
|
||||
byte[] bytes = baseStaffWorkbook(true, true, true, 2);
|
||||
|
||||
List<CcdiBaseStaffExcel> rows = EasyExcelUtil.importExcel(
|
||||
new ByteArrayInputStream(bytes),
|
||||
CcdiBaseStaffExcel.class,
|
||||
"员工信息"
|
||||
);
|
||||
|
||||
assertEquals(2, rows.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExcel_shouldFailWhenPartyMemberDropdownIsMissing() throws Exception {
|
||||
byte[] bytes = baseStaffWorkbook(false, true, true, 2);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () ->
|
||||
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("是否党员 列缺少下拉框"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExcel_shouldFailWhenStatusDropdownIsMissing() throws Exception {
|
||||
byte[] bytes = baseStaffWorkbook(true, false, true, 2);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () ->
|
||||
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
|
||||
);
|
||||
|
||||
assertEquals("员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExcel_shouldReportAllMissingDropdownColumnsInSameSheet() throws Exception {
|
||||
byte[] bytes = baseStaffWorkbook(false, false, true, 2);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () ->
|
||||
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
|
||||
);
|
||||
|
||||
assertEquals("员工信息 Sheet 的 是否党员、状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExcel_shouldFailWhenValidationIsNotListType() throws Exception {
|
||||
byte[] bytes = baseStaffWorkbook(true, true, false, 2);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () ->
|
||||
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("状态 列缺少下拉框"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExcel_shouldFailWhenListValidationDoesNotCoverEveryActualDataRow() throws Exception {
|
||||
byte[] bytes = baseStaffWorkbook(true, true, true, 1);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () ->
|
||||
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("状态 列缺少下拉框"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExcel_shouldFailWhenSecondSheetDropdownIsMissing() throws Exception {
|
||||
byte[] bytes = baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown();
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () ->
|
||||
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffAssetInfoExcel.class, "员工资产信息")
|
||||
);
|
||||
|
||||
assertEquals("员工资产信息 Sheet 的 资产状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExcel_shouldSkipDropdownStructureValidationWhenClassHasNoDictDropdownFields() throws Exception {
|
||||
byte[] bytes = plainWorkbookWithoutDropdown();
|
||||
|
||||
List<PlainExcel> rows = EasyExcelUtil.importExcel(
|
||||
new ByteArrayInputStream(bytes),
|
||||
PlainExcel.class,
|
||||
"普通信息"
|
||||
);
|
||||
|
||||
assertEquals(1, rows.size());
|
||||
}
|
||||
|
||||
private byte[] baseStaffWorkbook(boolean partyDropdown, boolean statusDropdown, boolean statusAsList,
|
||||
int statusLastRow) throws Exception {
|
||||
try (Workbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("员工信息");
|
||||
Row header = sheet.createRow(0);
|
||||
String[] headers = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)",
|
||||
"入职时间", "是否党员", "状态"};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
header.createCell(i).setCellValue(headers[i]);
|
||||
}
|
||||
createBaseStaffRow(sheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1");
|
||||
createBaseStaffRow(sheet, 2, "李四", 9020002L, "330106198603031022", "1", "1");
|
||||
|
||||
if (partyDropdown) {
|
||||
addListValidation(sheet, 7, 1, 2, "0", "1");
|
||||
}
|
||||
if (statusDropdown) {
|
||||
if (statusAsList) {
|
||||
addListValidation(sheet, 8, 1, statusLastRow, "0", "1");
|
||||
} else {
|
||||
addIntegerValidation(sheet, 8, 1, 2);
|
||||
}
|
||||
}
|
||||
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown() throws Exception {
|
||||
try (Workbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet staffSheet = workbook.createSheet("员工信息");
|
||||
Row staffHeader = staffSheet.createRow(0);
|
||||
String[] staffHeaders = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)",
|
||||
"入职时间", "是否党员", "状态"};
|
||||
for (int i = 0; i < staffHeaders.length; i++) {
|
||||
staffHeader.createCell(i).setCellValue(staffHeaders[i]);
|
||||
}
|
||||
createBaseStaffRow(staffSheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1");
|
||||
addListValidation(staffSheet, 7, 1, 1, "0", "1");
|
||||
addListValidation(staffSheet, 8, 1, 1, "0", "1");
|
||||
|
||||
Sheet assetSheet = workbook.createSheet("员工资产信息");
|
||||
Row assetHeader = assetSheet.createRow(0);
|
||||
String[] assetHeaders = {"员工身份证号*", "资产大类*", "资产小类*", "资产名称*", "产权占比",
|
||||
"购买/评估日期", "资产原值", "当前估值*", "估值截止日期", "资产状态*", "备注"};
|
||||
for (int i = 0; i < assetHeaders.length; i++) {
|
||||
assetHeader.createCell(i).setCellValue(assetHeaders[i]);
|
||||
}
|
||||
Row assetRow = assetSheet.createRow(1);
|
||||
assetRow.createCell(0).setCellValue("33010619850202101X");
|
||||
assetRow.createCell(1).setCellValue("房产");
|
||||
assetRow.createCell(2).setCellValue("住宅");
|
||||
assetRow.createCell(3).setCellValue("测试住宅");
|
||||
assetRow.createCell(7).setCellValue(1000000D);
|
||||
assetRow.createCell(9).setCellValue("正常");
|
||||
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void createBaseStaffRow(Sheet sheet, int rowIndex, String name, long staffId, String idCard,
|
||||
String partyMember, String status) {
|
||||
Row row = sheet.createRow(rowIndex);
|
||||
row.createCell(0).setCellValue(name);
|
||||
row.createCell(1).setCellValue(staffId);
|
||||
row.createCell(2).setCellValue(103L);
|
||||
row.createCell(3, CellType.STRING).setCellValue(idCard);
|
||||
row.createCell(4, CellType.STRING).setCellValue("13370000001");
|
||||
row.createCell(5).setCellValue(new BigDecimal("180000").doubleValue());
|
||||
row.createCell(6).setCellValue("2026-04-30");
|
||||
row.createCell(7, CellType.STRING).setCellValue(partyMember);
|
||||
row.createCell(8, CellType.STRING).setCellValue(status);
|
||||
}
|
||||
|
||||
private void addListValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow, String... options) {
|
||||
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||
DataValidationConstraint constraint = helper.createExplicitListConstraint(options);
|
||||
DataValidation validation = helper.createValidation(
|
||||
constraint,
|
||||
new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex)
|
||||
);
|
||||
sheet.addValidationData(validation);
|
||||
}
|
||||
|
||||
private void addIntegerValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow) {
|
||||
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||
DataValidationConstraint constraint = helper.createIntegerConstraint(
|
||||
DataValidationConstraint.OperatorType.BETWEEN,
|
||||
"0",
|
||||
"1"
|
||||
);
|
||||
DataValidation validation = helper.createValidation(
|
||||
constraint,
|
||||
new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex)
|
||||
);
|
||||
sheet.addValidationData(validation);
|
||||
}
|
||||
|
||||
private byte[] plainWorkbookWithoutDropdown() throws Exception {
|
||||
try (Workbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("普通信息");
|
||||
Row header = sheet.createRow(0);
|
||||
header.createCell(0).setCellValue("名称");
|
||||
Row row = sheet.createRow(1);
|
||||
row.createCell(0).setCellValue("张三");
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlainExcel {
|
||||
@ExcelProperty(value = "名称", index = 0)
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public class LsfxConstants {
|
||||
|
||||
/** 数据渠道编码 */
|
||||
public static final String DATA_CHANNEL_ZJRCU = "ZJRCU";
|
||||
public static final String DATA_CHANNEL_JZL = "JZL";
|
||||
|
||||
/** 分析类型 */
|
||||
public static final String ANALYSIS_TYPE = "-1";
|
||||
|
||||
@@ -14,7 +14,7 @@ public class FetchInnerFlowRequest {
|
||||
/** 客户身份证号 */
|
||||
private String customerNo;
|
||||
|
||||
/** 数据渠道编码(固定值:ZJRCU) */
|
||||
/** 数据渠道编码(ZJRCU-行内,JZL-金综) */
|
||||
private String dataChannelCode;
|
||||
|
||||
/** 发起请求的时间(格式:yyyyMMdd) */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.ruoyi.lsfx.client;
|
||||
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.response.UploadFileResponse;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LsfxAnalysisClientTest {
|
||||
|
||||
@Mock
|
||||
private HttpUtil httpUtil;
|
||||
|
||||
@InjectMocks
|
||||
private LsfxAnalysisClient client;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void uploadFile_shouldPassOriginalFilenameToMultipartResource() throws Exception {
|
||||
ReflectionTestUtils.setField(client, "baseUrl", "http://lsfx");
|
||||
ReflectionTestUtils.setField(client, "uploadFileEndpoint", "/upload");
|
||||
ReflectionTestUtils.setField(client, "clientId", "client-1");
|
||||
|
||||
Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
|
||||
Files.writeString(tempFile, "content");
|
||||
|
||||
UploadFileResponse response = new UploadFileResponse();
|
||||
response.setData(new UploadFileResponse.UploadData());
|
||||
|
||||
ArgumentCaptor<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
ArgumentCaptor<Map<String, String>> headersCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
when(httpUtil.uploadFile(eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
client.uploadFile(200, tempFile.toFile(), "银行流水A.xlsx");
|
||||
|
||||
assertEquals(200, paramsCaptor.getValue().get("groupId"));
|
||||
Resource filePart = assertInstanceOf(Resource.class, paramsCaptor.getValue().get("files"));
|
||||
assertEquals("银行流水A.xlsx", filePart.getFilename());
|
||||
assertEquals("client-1", headersCaptor.getValue().get(LsfxConstants.HEADER_CLIENT_ID));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ruoyi.lsfx.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HttpUtilTest {
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void uploadFile_shouldUseExplicitResourceFilename() throws Exception {
|
||||
HttpUtil httpUtil = new HttpUtil();
|
||||
ReflectionTestUtils.setField(httpUtil, "restTemplate", restTemplate);
|
||||
|
||||
Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
|
||||
Files.writeString(tempFile, "content");
|
||||
|
||||
ArgumentCaptor<HttpEntity> captor = ArgumentCaptor.forClass(HttpEntity.class);
|
||||
when(restTemplate.postForEntity(eq("http://lsfx/upload"), captor.capture(), eq(String.class)))
|
||||
.thenReturn(ResponseEntity.ok("ok"));
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("groupId", 200);
|
||||
params.put("files", HttpUtil.namedFileResource(tempFile.toFile(), "银行流水A.xlsx"));
|
||||
|
||||
String result = httpUtil.uploadFile("http://lsfx/upload", params, null, String.class);
|
||||
|
||||
assertEquals("ok", result);
|
||||
MultiValueMap<String, Object> body = (MultiValueMap<String, Object>) captor.getValue().getBody();
|
||||
Object filePart = body.getFirst("files");
|
||||
Resource resource = assertInstanceOf(Resource.class, filePart);
|
||||
assertEquals("银行流水A.xlsx", resource.getFilename());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
mock-maker-subclass
|
||||
@@ -18,7 +18,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -72,9 +71,13 @@ public class CcdiBankStatementController extends BaseController {
|
||||
*/
|
||||
@GetMapping("/detail/{bankStatementId}")
|
||||
@Operation(summary = "查询流水详情")
|
||||
public AjaxResult getDetail(@PathVariable Long bankStatementId) {
|
||||
public AjaxResult getDetail(@PathVariable Long bankStatementId, String modelCode, String suspiciousType) {
|
||||
projectAccessService.assertCanReadByBankStatementId(bankStatementId);
|
||||
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(bankStatementId);
|
||||
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(
|
||||
bankStatementId,
|
||||
modelCode,
|
||||
suspiciousType
|
||||
);
|
||||
return AjaxResult.success(detail);
|
||||
}
|
||||
|
||||
@@ -83,7 +86,6 @@ public class CcdiBankStatementController extends BaseController {
|
||||
*/
|
||||
@PostMapping("/export")
|
||||
@Operation(summary = "导出流水明细")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:export')")
|
||||
public void export(HttpServletResponse response, CcdiBankStatementQueryDTO queryDTO) {
|
||||
projectAccessService.assertCanRead(queryDTO.getProjectId());
|
||||
List<CcdiBankStatementExcel> list = bankStatementService.selectStatementListForExport(queryDTO);
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.ruoyi.common.core.page.PageDomain;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.page.TableSupport;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -65,7 +66,7 @@ public class CcdiFileUploadController extends BaseController {
|
||||
return AjaxResult.error("单次最多上传100个文件");
|
||||
}
|
||||
|
||||
// 校验文件大小和格式
|
||||
// 校验文件数量、空文件和大小,文件名业务规则统一在 Service 层处理
|
||||
for (MultipartFile file : files) {
|
||||
if (file.isEmpty()) {
|
||||
return AjaxResult.error("文件不能为空");
|
||||
@@ -73,15 +74,6 @@ public class CcdiFileUploadController extends BaseController {
|
||||
if (file.getSize() > 50 * 1024 * 1024) {
|
||||
return AjaxResult.error("文件 " + file.getOriginalFilename() + " 超过50MB限制");
|
||||
}
|
||||
String fileName = file.getOriginalFilename();
|
||||
if (fileName == null || fileName.trim().isEmpty()) {
|
||||
return AjaxResult.error("文件名不能为空");
|
||||
}
|
||||
String lowerFileName = fileName.toLowerCase();
|
||||
if (!lowerFileName.endsWith(".xlsx") && !lowerFileName.endsWith(".csv")
|
||||
&& !lowerFileName.endsWith(".pdf")) {
|
||||
return AjaxResult.error("文件 " + fileName + " 格式不支持, 仅支持 PDF, CSV, XLSX 文件");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -112,10 +104,10 @@ public class CcdiFileUploadController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交拉取本行信息任务
|
||||
* 提交拉取行内/金综流水任务
|
||||
*/
|
||||
@PostMapping("/pull-bank-info")
|
||||
@Operation(summary = "拉取本行信息", description = "按身份证号批量提交拉取本行信息任务")
|
||||
@Operation(summary = "拉取行内/金综流水", description = "按证件号码批量提交拉取行内/金综流水任务")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
|
||||
public AjaxResult pullBankInfo(@RequestBody CcdiPullBankInfoSubmitDTO dto) {
|
||||
if (dto == null || dto.getProjectId() == null) {
|
||||
@@ -125,7 +117,16 @@ public class CcdiFileUploadController extends BaseController {
|
||||
if (CollectionUtils.isEmpty(dto.getIdCards())) {
|
||||
return AjaxResult.error("身份证号不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate())) {
|
||||
if (!StringUtils.hasText(dto.getDataChannelCode())) {
|
||||
return AjaxResult.error("流水来源不能为空");
|
||||
}
|
||||
String dataChannelCode = dto.getDataChannelCode().trim().toUpperCase();
|
||||
if (!LsfxConstants.DATA_CHANNEL_ZJRCU.equals(dataChannelCode)
|
||||
&& !LsfxConstants.DATA_CHANNEL_JZL.equals(dataChannelCode)) {
|
||||
return AjaxResult.error("流水来源不支持");
|
||||
}
|
||||
if (LsfxConstants.DATA_CHANNEL_ZJRCU.equals(dataChannelCode)
|
||||
&& (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate()))) {
|
||||
return AjaxResult.error("开始日期和结束日期不能为空");
|
||||
}
|
||||
|
||||
@@ -134,6 +135,7 @@ public class CcdiFileUploadController extends BaseController {
|
||||
String batchId = fileUploadService.submitPullBankInfo(
|
||||
dto.getProjectId(),
|
||||
dto.getIdCards(),
|
||||
dataChannelCode,
|
||||
dto.getStartDate(),
|
||||
dto.getEndDate(),
|
||||
userId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQuery
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
|
||||
@@ -33,8 +34,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -67,6 +70,18 @@ public class CcdiProjectOverviewController extends BaseController {
|
||||
return AjaxResult.success(dashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* 排除单条可疑预警
|
||||
*/
|
||||
@PostMapping("/risk-exclusions")
|
||||
@Operation(summary = "排除单条可疑预警")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public AjaxResult excludeRisk(@Validated @RequestBody CcdiProjectRiskExclusionSaveDTO dto) {
|
||||
projectAccessService.assertCanOperate(dto.getProjectId());
|
||||
overviewService.excludeRisk(dto);
|
||||
return AjaxResult.success("排除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询风险人员总览
|
||||
*/
|
||||
@@ -293,9 +308,12 @@ public class CcdiProjectOverviewController extends BaseController {
|
||||
@PostMapping("/risk-details/export")
|
||||
@Operation(summary = "导出风险明细")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
|
||||
projectAccessService.assertCanRead(projectId);
|
||||
overviewService.exportRiskDetails(response, projectId);
|
||||
public void exportRiskDetails(
|
||||
HttpServletResponse response,
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
|
||||
) {
|
||||
projectAccessService.assertCanRead(queryDTO.getProjectId());
|
||||
overviewService.exportRiskDetails(response, queryDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryD
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
|
||||
@@ -16,16 +18,21 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListVO;
|
||||
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -138,4 +145,29 @@ public class CcdiProjectSpecialCheckController extends BaseController {
|
||||
CcdiProjectExtendedTransferDetailVO result = specialCheckService.getExtendedTransferDetail(queryDTO);
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询新增贷款列表
|
||||
*/
|
||||
@GetMapping("/increase-lending/list")
|
||||
@Operation(summary = "查询新增贷款列表")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public AjaxResult getIncreaseLendingList(@Validated CcdiProjectIncreaseLendingQueryDTO queryDTO) {
|
||||
projectAccessService.assertCanRead(queryDTO.getProjectId());
|
||||
CcdiProjectIncreaseLendingListVO result = specialCheckService.getIncreaseLendingList(queryDTO);
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出新增贷款列表
|
||||
*/
|
||||
@PostMapping("/increase-lending/export")
|
||||
@Operation(summary = "导出新增贷款列表")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public void exportIncreaseLending(HttpServletResponse response, @Validated CcdiProjectIncreaseLendingQueryDTO queryDTO) {
|
||||
projectAccessService.assertCanRead(queryDTO.getProjectId());
|
||||
List<CcdiProjectIncreaseLendingExcel> rows = specialCheckService.exportIncreaseLendingList(queryDTO);
|
||||
ExcelUtil<CcdiProjectIncreaseLendingExcel> util = new ExcelUtil<>(CcdiProjectIncreaseLendingExcel.class);
|
||||
util.exportExcel(response, rows, "新增贷款查询");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ruoyi.ccdi.project.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 项目新增贷款查询入参
|
||||
*/
|
||||
@Data
|
||||
public class CcdiProjectIncreaseLendingQueryDTO {
|
||||
|
||||
/** 项目ID */
|
||||
@NotNull(message = "项目ID不能为空")
|
||||
private Long projectId;
|
||||
|
||||
/** 柜员号 */
|
||||
private String staffId;
|
||||
|
||||
/** 员工身份证号 */
|
||||
private String staffIdCard;
|
||||
|
||||
/** 审核人柜员号 */
|
||||
private String approver;
|
||||
|
||||
/** 发放日期起 */
|
||||
private String loanStartDate;
|
||||
|
||||
/** 发放日期止 */
|
||||
private String loanEndDate;
|
||||
|
||||
/** 页码 */
|
||||
private Integer pageNum = 1;
|
||||
|
||||
/** 每页条数 */
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ruoyi.ccdi.project.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 项目结果页排除可疑保存DTO
|
||||
*/
|
||||
@Data
|
||||
public class CcdiProjectRiskExclusionSaveDTO {
|
||||
|
||||
/** 项目ID */
|
||||
@NotNull(message = "项目ID不能为空")
|
||||
private Long projectId;
|
||||
|
||||
/** 人员证件号 */
|
||||
private String staffIdCard;
|
||||
|
||||
/** 规则编码 */
|
||||
@NotBlank(message = "规则编码不能为空")
|
||||
private String ruleCode;
|
||||
|
||||
/** 排除类型:STATEMENT/OBJECT */
|
||||
@NotBlank(message = "排除类型不能为空")
|
||||
private String exclusionType;
|
||||
|
||||
/** 流水ID */
|
||||
private Long bankStatementId;
|
||||
|
||||
/** 排除原因 */
|
||||
@NotBlank(message = "排除原因不能为空")
|
||||
@Size(max = 1000, message = "排除原因不能超过1000个字符")
|
||||
private String excludeReason;
|
||||
}
|
||||
@@ -11,6 +11,9 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
|
||||
/** 项目ID */
|
||||
private Long projectId;
|
||||
|
||||
/** 模型编码 */
|
||||
private String modelCode;
|
||||
|
||||
/** 涉疑类型 */
|
||||
private String suspiciousType;
|
||||
|
||||
@@ -19,4 +22,7 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
|
||||
|
||||
/** 每页数量 */
|
||||
private Integer pageSize;
|
||||
|
||||
/** 是否包含外部人员预警分支 */
|
||||
private Boolean includeExternalPerson;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 拉取本行信息提交参数
|
||||
* 拉取行内/金综流水提交参数
|
||||
*/
|
||||
@Data
|
||||
public class CcdiPullBankInfoSubmitDTO {
|
||||
@@ -16,6 +16,9 @@ public class CcdiPullBankInfoSubmitDTO {
|
||||
/** 身份证号列表 */
|
||||
private List<String> idCards;
|
||||
|
||||
/** 数据渠道编码:ZJRCU-行内,JZL-金综 */
|
||||
private String dataChannelCode;
|
||||
|
||||
/** 开始日期 */
|
||||
private String startDate;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.ruoyi.ccdi.project.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 项目结果页排除可疑记录
|
||||
*/
|
||||
@Data
|
||||
@TableName("ccdi_project_risk_exclusion")
|
||||
public class CcdiProjectRiskExclusion implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键ID */
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 项目ID */
|
||||
private Long projectId;
|
||||
|
||||
/** 人员证件号 */
|
||||
private String staffIdCard;
|
||||
|
||||
/** 规则编码 */
|
||||
private String ruleCode;
|
||||
|
||||
/** 排除类型:STATEMENT/OBJECT */
|
||||
private String exclusionType;
|
||||
|
||||
/** 流水ID */
|
||||
private Long bankStatementId;
|
||||
|
||||
/** 排除原因 */
|
||||
private String excludeReason;
|
||||
|
||||
/** 创建者 */
|
||||
private String createBy;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 更新者 */
|
||||
private String updateBy;
|
||||
|
||||
/** 更新时间 */
|
||||
private Date updateTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.ccdi.project.domain.excel;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -26,4 +27,10 @@ public class CcdiProjectAbnormalAccountExcel {
|
||||
|
||||
@Excel(name = "状态")
|
||||
private String status;
|
||||
|
||||
@Excel(name = "命中原因")
|
||||
private String reasonDetail;
|
||||
|
||||
@Excel(name = "涉及金额")
|
||||
private BigDecimal involvedAmount;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.ruoyi.ccdi.project.domain.excel;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 新增贷款查询导出对象
|
||||
*/
|
||||
@Data
|
||||
public class CcdiProjectIncreaseLendingExcel {
|
||||
|
||||
/** 柜员号 */
|
||||
@Excel(name = "柜员号")
|
||||
private String staffId;
|
||||
|
||||
/** 员工姓名 */
|
||||
@Excel(name = "员工姓名")
|
||||
private String staffName;
|
||||
|
||||
/** 员工身份证 */
|
||||
@Excel(name = "员工身份证")
|
||||
private String staffIdCard;
|
||||
|
||||
/** 部门 */
|
||||
@Excel(name = "部门")
|
||||
private String deptName;
|
||||
|
||||
/** 合同编号 */
|
||||
@Excel(name = "合同编号")
|
||||
private String contractNo;
|
||||
|
||||
/** 放款机构号 */
|
||||
@Excel(name = "放款机构号")
|
||||
private String lendingOrgNo;
|
||||
|
||||
/** 借款人 */
|
||||
@Excel(name = "借款人")
|
||||
private String borrowerName;
|
||||
|
||||
/** 借款人证件号 */
|
||||
@Excel(name = "借款人证件号")
|
||||
private String borrowerCertNo;
|
||||
|
||||
/** 贷款产品 */
|
||||
@Excel(name = "贷款产品")
|
||||
private String loanProduct;
|
||||
|
||||
/** 合同金额 */
|
||||
@Excel(name = "合同金额")
|
||||
private BigDecimal contractAmount;
|
||||
|
||||
/** 当前余额 */
|
||||
@Excel(name = "当前余额")
|
||||
private BigDecimal loanBalance;
|
||||
|
||||
/** 发放日期 */
|
||||
@Excel(name = "发放日期")
|
||||
private String loanStartDate;
|
||||
|
||||
/** 到期日期 */
|
||||
@Excel(name = "到期日期")
|
||||
private String loanEndDate;
|
||||
|
||||
/** 合同状态 */
|
||||
@Excel(name = "合同状态")
|
||||
private String status;
|
||||
|
||||
/** 五级分类 */
|
||||
@Excel(name = "五级分类")
|
||||
private String fiveClassification;
|
||||
|
||||
/** 客户经理ID */
|
||||
@Excel(name = "客户经理ID")
|
||||
private String customerManagerId;
|
||||
|
||||
/** 客户经理 */
|
||||
@Excel(name = "客户经理")
|
||||
private String customerManagerName;
|
||||
|
||||
/** 审批人 */
|
||||
@Excel(name = "审批人")
|
||||
private String approver;
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import lombok.Data;
|
||||
@Data
|
||||
public class CcdiBankStatementHitTagVO {
|
||||
|
||||
/** 模型编码 */
|
||||
private String modelCode;
|
||||
|
||||
/** 规则编码 */
|
||||
private String ruleCode;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -19,4 +20,8 @@ public class CcdiProjectAbnormalAccountItemVO {
|
||||
private String abnormalTime;
|
||||
|
||||
private String status;
|
||||
|
||||
private String reasonDetail;
|
||||
|
||||
private BigDecimal involvedAmount;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,15 @@ public class CcdiProjectFamilyAssetLiabilityListItemVO {
|
||||
/** 家庭总年收入 */
|
||||
private BigDecimal totalIncome;
|
||||
|
||||
/** 本人年收入 */
|
||||
private BigDecimal selfIncome;
|
||||
|
||||
/** 配偶年收入 */
|
||||
private BigDecimal spouseIncome;
|
||||
|
||||
/** 本人入职年限 */
|
||||
private Integer employmentYears;
|
||||
|
||||
/** 家庭总资产 */
|
||||
private BigDecimal totalAsset;
|
||||
|
||||
@@ -33,6 +42,12 @@ public class CcdiProjectFamilyAssetLiabilityListItemVO {
|
||||
/** 收入负债对比金额 */
|
||||
private BigDecimal comparisonAmount;
|
||||
|
||||
/** 可解释收入 */
|
||||
private BigDecimal explainableIncome;
|
||||
|
||||
/** 资产收入倍数 */
|
||||
private BigDecimal assetIncomeRatio;
|
||||
|
||||
/** 风险等级编码 */
|
||||
private String riskLevelCode;
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ public class CcdiProjectFamilyIncomeDetailVO {
|
||||
/** 配偶年收入 */
|
||||
private BigDecimal spouseIncome;
|
||||
|
||||
/** 本人入职年限 */
|
||||
private Integer employmentYears;
|
||||
|
||||
/** 家庭总年收入 */
|
||||
private BigDecimal totalIncome;
|
||||
|
||||
/** 可解释收入 */
|
||||
private BigDecimal explainableIncome;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 项目新增贷款查询列表项
|
||||
*/
|
||||
@Data
|
||||
public class CcdiProjectIncreaseLendingListItemVO {
|
||||
|
||||
/** 员工ID/柜员号 */
|
||||
private String staffId;
|
||||
|
||||
/** 员工姓名 */
|
||||
private String staffName;
|
||||
|
||||
/** 员工身份证号 */
|
||||
private String staffIdCard;
|
||||
|
||||
/** 所属部门 */
|
||||
private String deptName;
|
||||
|
||||
/** 合同编号 */
|
||||
private String contractNo;
|
||||
|
||||
/** 放款机构号 */
|
||||
private String lendingOrgNo;
|
||||
|
||||
/** 借款人名称 */
|
||||
private String borrowerName;
|
||||
|
||||
/** 借款人证件号码 */
|
||||
private String borrowerCertNo;
|
||||
|
||||
/** 贷款产品 */
|
||||
private String loanProduct;
|
||||
|
||||
/** 合同金额 */
|
||||
private BigDecimal contractAmount;
|
||||
|
||||
/** 当前贷款余额 */
|
||||
private BigDecimal loanBalance;
|
||||
|
||||
/** 贷款发放日期 */
|
||||
private String loanStartDate;
|
||||
|
||||
/** 贷款到期日期 */
|
||||
private String loanEndDate;
|
||||
|
||||
/** 合同状态 */
|
||||
private String status;
|
||||
|
||||
/** 五级分类 */
|
||||
private String fiveClassification;
|
||||
|
||||
/** 客户经理ID */
|
||||
private String customerManagerId;
|
||||
|
||||
/** 客户经理姓名 */
|
||||
private String customerManagerName;
|
||||
|
||||
/** 审批人 */
|
||||
private String approver;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 项目新增贷款查询列表结果
|
||||
*/
|
||||
@Data
|
||||
public class CcdiProjectIncreaseLendingListVO {
|
||||
|
||||
/** 列表数据 */
|
||||
private List<CcdiProjectIncreaseLendingListItemVO> rows;
|
||||
|
||||
/** 总数 */
|
||||
private Long total;
|
||||
}
|
||||
@@ -12,6 +12,8 @@ public class CcdiProjectPersonAnalysisObjectRecordVO {
|
||||
|
||||
private String modelCode;
|
||||
|
||||
private String ruleCode;
|
||||
|
||||
private String title;
|
||||
|
||||
private String subtitle;
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.ruoyi.ccdi.project.domain.vo;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 涉疑交易明细行
|
||||
@@ -14,6 +16,14 @@ public class CcdiProjectSuspiciousTransactionItemVO {
|
||||
|
||||
private String trxDate;
|
||||
|
||||
private String leAccountNo;
|
||||
|
||||
private String leAccountName;
|
||||
|
||||
private String customerAccountName;
|
||||
|
||||
private String customerAccountNo;
|
||||
|
||||
private String suspiciousPersonName;
|
||||
|
||||
private String relatedPersonName;
|
||||
@@ -35,4 +45,6 @@ public class CcdiProjectSuspiciousTransactionItemVO {
|
||||
private Boolean hasNameListHit;
|
||||
|
||||
private String nameListHitType;
|
||||
|
||||
private List<CcdiBankStatementHitTagVO> hitTags = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -26,11 +26,15 @@ public interface CcdiBankTagResultMapper extends BaseMapper<CcdiBankTagResult> {
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param bankStatementIds 流水ID列表
|
||||
* @param modelCode 模型编码
|
||||
* @param suspiciousType 预警类型
|
||||
* @return 命中的异常标签列表
|
||||
*/
|
||||
List<CcdiBankStatementHitTagVO> selectStatementTagsByProjectAndStatementIds(
|
||||
@Param("projectId") Long projectId,
|
||||
@Param("bankStatementIds") List<Long> bankStatementIds
|
||||
@Param("bankStatementIds") List<Long> bankStatementIds,
|
||||
@Param("modelCode") String modelCode,
|
||||
@Param("suspiciousType") String suspiciousType
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -163,6 +163,14 @@ public interface CcdiProjectOverviewMapper {
|
||||
*/
|
||||
CcdiProjectExternalRiskSummaryVO selectExternalRiskSummaryByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 判断项目是否存在外部人员主体
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 存在时返回1,否则返回空
|
||||
*/
|
||||
Integer selectExternalPersonSubjectExistsByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 查询外部人员预警模型卡片
|
||||
*
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 项目结果页排除可疑记录 Mapper
|
||||
*/
|
||||
public interface CcdiProjectRiskExclusionMapper extends BaseMapper<CcdiProjectRiskExclusion> {
|
||||
|
||||
/**
|
||||
* 幂等写入排除记录
|
||||
*
|
||||
* @param exclusion 排除记录
|
||||
* @return 写入条数
|
||||
*/
|
||||
int upsertExclusion(@Param("exclusion") CcdiProjectRiskExclusion exclusion);
|
||||
|
||||
/**
|
||||
* 查询待排除命中是否存在
|
||||
*
|
||||
* @param exclusion 排除记录
|
||||
* @return 命中数量
|
||||
*/
|
||||
int countExistingRiskHit(@Param("exclusion") CcdiProjectRiskExclusion exclusion);
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseSupplierVO;
|
||||
@@ -15,6 +17,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListItemVO;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -145,6 +148,28 @@ public interface CcdiProjectSpecialCheckMapper {
|
||||
@Param("query") CcdiProjectExtendedTransferQueryDTO queryDTO
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询项目新增贷款列表
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param queryDTO 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
Page<CcdiProjectIncreaseLendingListItemVO> selectIncreaseLendingPage(
|
||||
@Param("page") Page<CcdiProjectIncreaseLendingListItemVO> page,
|
||||
@Param("query") CcdiProjectIncreaseLendingQueryDTO queryDTO
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询项目新增贷款导出列表
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @return 导出列表
|
||||
*/
|
||||
List<CcdiProjectIncreaseLendingExcel> selectIncreaseLendingExportList(
|
||||
@Param("query") CcdiProjectIncreaseLendingQueryDTO queryDTO
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询专项核查调动拓展详情
|
||||
*
|
||||
|
||||
@@ -49,4 +49,18 @@ public interface ICcdiBankStatementService {
|
||||
* @return 详情
|
||||
*/
|
||||
CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId);
|
||||
|
||||
/**
|
||||
* 按涉疑交易筛选范围查询流水详情
|
||||
*
|
||||
* @param bankStatementId 流水ID
|
||||
* @param modelCode 模型编码
|
||||
* @param suspiciousType 预警类型
|
||||
* @return 详情
|
||||
*/
|
||||
CcdiBankStatementDetailVO getStatementDetail(
|
||||
Long bankStatementId,
|
||||
String modelCode,
|
||||
String suspiciousType
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,10 +35,11 @@ public interface ICcdiFileUploadService {
|
||||
List<String> parseIdCardFile(MultipartFile file);
|
||||
|
||||
/**
|
||||
* 提交拉取本行信息任务
|
||||
* 提交拉取行内/金综流水任务
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param idCards 身份证号列表
|
||||
* @param dataChannelCode 数据渠道编码
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @param userId 当前登录用户ID
|
||||
@@ -47,6 +48,7 @@ public interface ICcdiFileUploadService {
|
||||
*/
|
||||
String submitPullBankInfo(Long projectId,
|
||||
List<String> idCards,
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
Long userId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQuery
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
|
||||
@@ -42,6 +43,13 @@ public interface ICcdiProjectOverviewService {
|
||||
*/
|
||||
CcdiProjectOverviewDashboardVO getDashboard(Long projectId);
|
||||
|
||||
/**
|
||||
* 排除单条可疑预警
|
||||
*
|
||||
* @param dto 排除参数
|
||||
*/
|
||||
void excludeRisk(CcdiProjectRiskExclusionSaveDTO dto);
|
||||
|
||||
/**
|
||||
* 查询风险人员总览
|
||||
*
|
||||
@@ -205,6 +213,18 @@ public interface ICcdiProjectOverviewService {
|
||||
default void exportRiskDetails(HttpServletResponse response, Long projectId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按涉疑交易筛选范围统一导出风险明细
|
||||
*
|
||||
* @param response 响应流
|
||||
* @param queryDTO 涉疑交易筛选条件
|
||||
*/
|
||||
default void exportRiskDetails(
|
||||
HttpServletResponse response,
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键导出结果总览报告
|
||||
*
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryD
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
|
||||
@@ -16,6 +18,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListVO;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 项目专项核查服务接口
|
||||
@@ -91,4 +95,20 @@ public interface ICcdiProjectSpecialCheckService {
|
||||
* @return 详情结果
|
||||
*/
|
||||
CcdiProjectExtendedTransferDetailVO getExtendedTransferDetail(CcdiProjectExtendedTransferDetailQueryDTO queryDTO);
|
||||
|
||||
/**
|
||||
* 查询项目新增贷款列表
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @return 列表结果
|
||||
*/
|
||||
CcdiProjectIncreaseLendingListVO getIncreaseLendingList(CcdiProjectIncreaseLendingQueryDTO queryDTO);
|
||||
|
||||
/**
|
||||
* 导出项目新增贷款列表
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @return 导出列表
|
||||
*/
|
||||
List<CcdiProjectIncreaseLendingExcel> exportIncreaseLendingList(CcdiProjectIncreaseLendingQueryDTO queryDTO);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
|
||||
|
||||
private static final Set<String> ALLOWED_TAB_TYPES = Set.of("all", "in", "out");
|
||||
private static final Set<String> ALLOWED_ORDER_DIRECTIONS = Set.of("asc", "desc");
|
||||
private static final Set<String> ALLOWED_SUSPICIOUS_TYPES =
|
||||
Set.of("ALL", "MODEL_RULE", "EXTERNAL_PERSON", "NAME_LIST");
|
||||
|
||||
@Resource
|
||||
private CcdiBankStatementMapper bankStatementMapper;
|
||||
@@ -69,13 +71,24 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
|
||||
|
||||
@Override
|
||||
public CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId) {
|
||||
return getStatementDetail(bankStatementId, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiBankStatementDetailVO getStatementDetail(
|
||||
Long bankStatementId,
|
||||
String modelCode,
|
||||
String suspiciousType
|
||||
) {
|
||||
CcdiBankStatementDetailVO detail = bankStatementMapper.selectStatementDetailById(bankStatementId);
|
||||
if (detail == null || detail.getProjectId() == null || detail.getBankStatementId() == null) {
|
||||
return detail;
|
||||
}
|
||||
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = loadHitTagMap(
|
||||
detail.getProjectId(),
|
||||
List.of(detail.getBankStatementId())
|
||||
detail.getProjectId(),
|
||||
List.of(detail.getBankStatementId()),
|
||||
normalizeUpperCase(modelCode),
|
||||
normalizeSuspiciousType(suspiciousType)
|
||||
);
|
||||
detail.setHitTags(new ArrayList<>(hitTagMap.getOrDefault(detail.getBankStatementId(), Collections.emptyList())));
|
||||
return detail;
|
||||
@@ -93,18 +106,33 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
|
||||
if (bankStatementIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = loadHitTagMap(projectId, bankStatementIds);
|
||||
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = loadHitTagMap(
|
||||
projectId,
|
||||
bankStatementIds,
|
||||
null,
|
||||
null
|
||||
);
|
||||
rows.forEach(row -> row.setHitTags(new ArrayList<>(
|
||||
hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList())
|
||||
)));
|
||||
}
|
||||
|
||||
private Map<Long, List<CcdiBankStatementHitTagVO>> loadHitTagMap(Long projectId, List<Long> bankStatementIds) {
|
||||
private Map<Long, List<CcdiBankStatementHitTagVO>> loadHitTagMap(
|
||||
Long projectId,
|
||||
List<Long> bankStatementIds,
|
||||
String modelCode,
|
||||
String suspiciousType
|
||||
) {
|
||||
if (projectId == null || bankStatementIds == null || bankStatementIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<CcdiBankStatementHitTagVO> hitTags =
|
||||
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(projectId, bankStatementIds);
|
||||
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
projectId,
|
||||
bankStatementIds,
|
||||
modelCode,
|
||||
suspiciousType
|
||||
);
|
||||
if (hitTags == null || hitTags.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
@@ -165,6 +193,16 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
|
||||
return normalized == null ? null : normalized.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizeUpperCase(String value) {
|
||||
String normalized = normalizeText(value);
|
||||
return normalized == null ? null : normalized.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizeSuspiciousType(String suspiciousType) {
|
||||
String normalized = normalizeUpperCase(suspiciousType);
|
||||
return normalized != null && ALLOWED_SUSPICIOUS_TYPES.contains(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
private String normalizeText(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
|
||||
@@ -46,6 +46,7 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
@@ -64,6 +65,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
private static final int MAX_ERROR_MESSAGE_LENGTH = 2000;
|
||||
private static final Pattern ID_CARD_PATTERN =
|
||||
Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])([0-2]\\d|3[01])\\d{3}[0-9Xx]$");
|
||||
private static final Pattern UPLOAD_FILE_NAME_ID_CARD_PATTERN =
|
||||
Pattern.compile("(?<!\\d)\\d{17}[0-9Xx](?!\\d)");
|
||||
private static final Pattern FILE_NAME_SPACE_PATTERN = Pattern.compile("[\\s\\u3000]+");
|
||||
|
||||
@Data
|
||||
private static class FetchBankStatementResult {
|
||||
@@ -150,6 +154,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
@Override
|
||||
public String submitPullBankInfo(Long projectId,
|
||||
List<String> idCards,
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
Long userId,
|
||||
@@ -157,17 +162,20 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
if (projectId == null) {
|
||||
throw new IllegalArgumentException("项目ID不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(startDate) || !StringUtils.hasText(endDate)) {
|
||||
throw new IllegalArgumentException("开始日期和结束日期不能为空");
|
||||
}
|
||||
if (idCards == null || idCards.isEmpty()) {
|
||||
throw new IllegalArgumentException("身份证号不能为空");
|
||||
}
|
||||
String normalizedDataChannelCode = normalizePullBankInfoDataChannelCode(dataChannelCode);
|
||||
|
||||
LocalDate start = LocalDate.parse(startDate);
|
||||
LocalDate end = LocalDate.parse(endDate);
|
||||
if (start.isAfter(end)) {
|
||||
throw new IllegalArgumentException("开始日期不能晚于结束日期");
|
||||
if (LsfxConstants.DATA_CHANNEL_ZJRCU.equals(normalizedDataChannelCode)) {
|
||||
if (!StringUtils.hasText(startDate) || !StringUtils.hasText(endDate)) {
|
||||
throw new IllegalArgumentException("开始日期和结束日期不能为空");
|
||||
}
|
||||
LocalDate start = LocalDate.parse(startDate);
|
||||
LocalDate end = LocalDate.parse(endDate);
|
||||
if (start.isAfter(end)) {
|
||||
throw new IllegalArgumentException("开始日期不能晚于结束日期");
|
||||
}
|
||||
}
|
||||
|
||||
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
|
||||
@@ -214,13 +222,26 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
CompletableFuture.runAsync(() -> submitPullBankInfoTasks(
|
||||
projectId, lsfxProjectId, records, normalizedIdCards, startDate, endDate, batchId
|
||||
projectId, lsfxProjectId, records, normalizedIdCards,
|
||||
normalizedDataChannelCode, startDate, endDate, batchId
|
||||
));
|
||||
}
|
||||
});
|
||||
return batchId;
|
||||
}
|
||||
|
||||
private String normalizePullBankInfoDataChannelCode(String dataChannelCode) {
|
||||
if (!StringUtils.hasText(dataChannelCode)) {
|
||||
throw new IllegalArgumentException("流水来源不能为空");
|
||||
}
|
||||
String normalized = dataChannelCode.trim().toUpperCase();
|
||||
if (!LsfxConstants.DATA_CHANNEL_ZJRCU.equals(normalized)
|
||||
&& !LsfxConstants.DATA_CHANNEL_JZL.equals(normalized)) {
|
||||
throw new IllegalArgumentException("流水来源不支持");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deleteFileUploadRecord(Long id, Long operatorUserId) {
|
||||
CcdiFileUploadRecord record = recordMapper.selectById(id);
|
||||
@@ -347,6 +368,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
|
||||
log.info("【文件上传】项目信息验证通过: projectId={}, lsfxProjectId={}", projectId, lsfxProjectId);
|
||||
|
||||
List<String> normalizedFileNames = normalizeAndValidateUploadFileNames(files);
|
||||
|
||||
// Critical Fix #2 & #4: 保存临时文件和创建记录在同一个循环中,确保一一对应
|
||||
List<String> tempFilePaths = new ArrayList<>();
|
||||
List<CcdiFileUploadRecord> records = new ArrayList<>();
|
||||
@@ -362,10 +385,11 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
// 同一个循环中保存临时文件和创建记录,确保索引一一对应
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
MultipartFile file = files[i];
|
||||
String normalizedFileName = normalizedFileNames.get(i);
|
||||
|
||||
// 1. 保存临时文件
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String tempFileName = batchId + "_" + i + "_" + System.currentTimeMillis() + "_" + originalFilename;
|
||||
String tempFileName = batchId + "_" + i + "_" + System.currentTimeMillis() + "_" + normalizedFileName;
|
||||
Path tempFilePath = tempDir.resolve(tempFileName);
|
||||
|
||||
Files.copy(file.getInputStream(), tempFilePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
@@ -378,7 +402,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
|
||||
record.setProjectId(projectId);
|
||||
record.setLsfxProjectId(lsfxProjectId);
|
||||
record.setFileName(originalFilename);
|
||||
record.setFileName(normalizedFileName);
|
||||
record.setFileSize(file.getSize());
|
||||
record.setFileStatus("uploading");
|
||||
record.setUploadTime(now);
|
||||
@@ -423,6 +447,42 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
return batchId;
|
||||
}
|
||||
|
||||
private List<String> normalizeAndValidateUploadFileNames(MultipartFile[] files) {
|
||||
List<String> normalizedFileNames = new ArrayList<>();
|
||||
for (MultipartFile file : files) {
|
||||
String originalFileName = file.getOriginalFilename() == null ? "" : file.getOriginalFilename();
|
||||
String normalizedFileName = FILE_NAME_SPACE_PATTERN.matcher(originalFileName).replaceAll("");
|
||||
validateUploadFileName(normalizedFileName);
|
||||
normalizedFileNames.add(normalizedFileName);
|
||||
}
|
||||
return normalizedFileNames;
|
||||
}
|
||||
|
||||
private void validateUploadFileName(String fileName) {
|
||||
if (!StringUtils.hasText(fileName)) {
|
||||
throw new IllegalArgumentException("文件名不能为空");
|
||||
}
|
||||
|
||||
String lowerFileName = fileName.toLowerCase(Locale.ROOT);
|
||||
if (!lowerFileName.endsWith(".xlsx") && !lowerFileName.endsWith(".csv")
|
||||
&& !lowerFileName.endsWith(".pdf")) {
|
||||
throw new IllegalArgumentException("文件 " + fileName + " 格式不支持, 仅支持 PDF, CSV, XLSX 文件");
|
||||
}
|
||||
|
||||
String mainFileName = getMainFileName(fileName);
|
||||
if (!UPLOAD_FILE_NAME_ID_CARD_PATTERN.matcher(mainFileName).find()) {
|
||||
throw new IllegalArgumentException("文件 " + fileName + " 文件名未包含身份证信息");
|
||||
}
|
||||
}
|
||||
|
||||
private String getMainFileName(String fileName) {
|
||||
int extensionIndex = fileName.lastIndexOf('.');
|
||||
if (extensionIndex <= 0) {
|
||||
return fileName;
|
||||
}
|
||||
return fileName.substring(0, extensionIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度线程:循环提交任务到线程池
|
||||
* 支持等待30秒重试机制
|
||||
@@ -537,6 +597,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
Integer lsfxProjectId,
|
||||
List<CcdiFileUploadRecord> records,
|
||||
List<String> idCards,
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
String batchId) {
|
||||
@@ -558,7 +619,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
while (!submitted && retryCount < 2) {
|
||||
try {
|
||||
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(
|
||||
() -> processPullBankInfoAsync(projectId, lsfxProjectId, record, idCard, startDate, endDate),
|
||||
() -> processPullBankInfoAsync(projectId, lsfxProjectId, record, idCard, dataChannelCode, startDate, endDate),
|
||||
fileUploadExecutor
|
||||
);
|
||||
futures.add(future);
|
||||
@@ -597,16 +658,23 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
String idCard,
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate ) {
|
||||
try {
|
||||
String normalizedDataChannelCode = normalizePullBankInfoDataChannelCode(dataChannelCode);
|
||||
FetchInnerFlowRequest request = new FetchInnerFlowRequest();
|
||||
request.setGroupId(lsfxProjectId);
|
||||
request.setCustomerNo(idCard);
|
||||
request.setDataChannelCode(LsfxConstants.DEFAULT_DATA_CHANNEL_CODE);
|
||||
request.setDataChannelCode(normalizedDataChannelCode);
|
||||
request.setRequestDateId(Integer.parseInt(LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)));
|
||||
request.setDataStartDateId(Integer.parseInt(startDate.replace("-", "")));
|
||||
request.setDataEndDateId(Integer.parseInt(endDate.replace("-", "")));
|
||||
if (LsfxConstants.DATA_CHANNEL_JZL.equals(normalizedDataChannelCode)) {
|
||||
request.setDataStartDateId(0);
|
||||
request.setDataEndDateId(0);
|
||||
} else {
|
||||
request.setDataStartDateId(Integer.parseInt(startDate.replace("-", "")));
|
||||
request.setDataEndDateId(Integer.parseInt(endDate.replace("-", "")));
|
||||
}
|
||||
request.setUploadUserId(LsfxConstants.DEFAULT_USER_ID);
|
||||
|
||||
FetchInnerFlowResponse response = lsfxClient.fetchInnerFlow(request);
|
||||
@@ -777,7 +845,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
enterpriseNamesStr, accountNosStr);
|
||||
|
||||
log.info("【文件上传】步骤7: 获取流水数据");
|
||||
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId);
|
||||
String fallbackCretNo = extractIdCardFromFileName(record.getFileName());
|
||||
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId,
|
||||
fallbackCretNo);
|
||||
if (!fetchResult.isSuccess()) {
|
||||
updateFailedRecord(record, fetchResult.getErrorMessage());
|
||||
return;
|
||||
@@ -850,7 +920,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
* @param logId 文件ID
|
||||
*/
|
||||
private FetchBankStatementResult fetchAndSaveBankStatements(Long projectId, Integer groupId,
|
||||
Integer logId) {
|
||||
Integer logId,
|
||||
String fallbackCretNo) {
|
||||
log.info("【文件上传】开始获取流水数据: projectId={}, groupId={}, logId={}",
|
||||
projectId, groupId, logId);
|
||||
|
||||
@@ -915,6 +986,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
if (statement != null) {
|
||||
statement.setBatchId(logId);
|
||||
statement.setProjectId(projectId);
|
||||
fillMissingCretNo(statement, fallbackCretNo);
|
||||
normalizeDedupFields(statement);
|
||||
batchList.add(statement);
|
||||
|
||||
@@ -958,6 +1030,22 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
private String extractIdCardFromFileName(String fileName) {
|
||||
if (!StringUtils.hasText(fileName)) {
|
||||
return null;
|
||||
}
|
||||
String mainFileName = getMainFileName(fileName);
|
||||
Matcher matcher = UPLOAD_FILE_NAME_ID_CARD_PATTERN.matcher(mainFileName);
|
||||
return matcher.find() ? matcher.group() : null;
|
||||
}
|
||||
|
||||
private void fillMissingCretNo(CcdiBankStatement statement, String fallbackCretNo) {
|
||||
if (statement == null || StringUtils.hasText(statement.getCretNo()) || !StringUtils.hasText(fallbackCretNo)) {
|
||||
return;
|
||||
}
|
||||
statement.setCretNo(fallbackCretNo);
|
||||
}
|
||||
|
||||
private void cleanupBankStatements(Long projectId, Integer logId) {
|
||||
bankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId);
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
|
||||
writer.subsection("3. 异常账户信息表(共" + report.getAbnormalAccounts().size() + "条)");
|
||||
writer.table(
|
||||
List.of("账号", "开户人", "银行", "异常类型", "异常发生时间", "状态"),
|
||||
List.of("账号", "开户人", "银行", "异常类型", "异常发生时间", "状态", "命中原因", "涉及金额"),
|
||||
report.getAbnormalAccounts().stream()
|
||||
.map(item -> List.of(
|
||||
maskAccount(item.getAccountNo()),
|
||||
@@ -238,10 +238,12 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
safeText(item.getBankName()),
|
||||
safeText(item.getAbnormalType()),
|
||||
safeText(item.getAbnormalTime()),
|
||||
safeText(item.getStatus())
|
||||
safeText(item.getStatus()),
|
||||
safeText(item.getReasonDetail()),
|
||||
formatMoney(item.getInvolvedAmount())
|
||||
))
|
||||
.collect(Collectors.toList()),
|
||||
new float[] { 0.18F, 0.13F, 0.2F, 0.23F, 0.14F, 0.12F },
|
||||
new float[] { 0.12F, 0.1F, 0.14F, 0.14F, 0.11F, 0.08F, 0.22F, 0.09F },
|
||||
"暂无异常账户信息"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQuery
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
|
||||
@@ -18,6 +19,7 @@ import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
@@ -57,14 +59,18 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectRiskExclusionMapper;
|
||||
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -80,6 +86,16 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
|
||||
private static final String ACTION_LABEL = "查看详情";
|
||||
|
||||
private static final String SUSPICIOUS_TYPE_ALL = "ALL";
|
||||
|
||||
private static final String SUSPICIOUS_TYPE_MODEL_RULE = "MODEL_RULE";
|
||||
|
||||
private static final String SUSPICIOUS_TYPE_EXTERNAL_PERSON = "EXTERNAL_PERSON";
|
||||
|
||||
private static final String EXCLUSION_TYPE_STATEMENT = "STATEMENT";
|
||||
|
||||
private static final String EXCLUSION_TYPE_OBJECT = "OBJECT";
|
||||
|
||||
@Resource
|
||||
private CcdiProjectOverviewMapper overviewMapper;
|
||||
|
||||
@@ -98,6 +114,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
@Resource
|
||||
private CcdiBankTagResultMapper bankTagResultMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiProjectRiskExclusionMapper riskExclusionMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;
|
||||
|
||||
@@ -133,6 +152,24 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void excludeRisk(CcdiProjectRiskExclusionSaveDTO dto) {
|
||||
CcdiProjectRiskExclusion exclusion = buildRiskExclusion(dto);
|
||||
CcdiProject project = getRequiredProject(exclusion.getProjectId());
|
||||
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus()) || Integer.valueOf(1).equals(project.getIsArchived())) {
|
||||
throw new ServiceException("已归档项目暂不允许排除可疑");
|
||||
}
|
||||
if (CcdiProjectStatusConstants.TAGGING.equals(project.getStatus())) {
|
||||
throw new ServiceException("项目正在打标中,暂不允许排除可疑");
|
||||
}
|
||||
if (riskExclusionMapper.countExistingRiskHit(exclusion) <= 0) {
|
||||
throw new ServiceException("未找到可排除的预警命中");
|
||||
}
|
||||
riskExclusionMapper.upsertExclusion(exclusion);
|
||||
refreshOverviewEmployeeResults(exclusion.getProjectId(), exclusion.getUpdateBy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(CcdiProjectRiskPeopleQueryDTO queryDTO) {
|
||||
Long projectId = queryDTO.getProjectId();
|
||||
@@ -188,6 +225,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
queryDTO.getStaffIdCard()
|
||||
));
|
||||
attachStatementHitTags(statementRows, queryDTO.getProjectId());
|
||||
statementRows = statementRows.stream()
|
||||
.filter(row -> row.getHitTags() != null && !row.getHitTags().isEmpty())
|
||||
.toList();
|
||||
normalizeObjectRows(objectRows);
|
||||
|
||||
CcdiProjectPersonAnalysisDetailVO detail = new CcdiProjectPersonAnalysisDetailVO();
|
||||
@@ -335,6 +375,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
normalizeSuspiciousTransactionQuery(queryDTO);
|
||||
if (isSuspiciousTransactionScopeMismatch(queryDTO)) {
|
||||
return emptySuspiciousTransactionPage();
|
||||
}
|
||||
prepareSuspiciousTransactionExternalBranch(queryDTO);
|
||||
|
||||
if (isExternalScopeWithoutSubject(queryDTO)) {
|
||||
return emptySuspiciousTransactionPage();
|
||||
}
|
||||
|
||||
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(
|
||||
defaultPageNum(queryDTO.getPageNum()),
|
||||
@@ -342,9 +390,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
);
|
||||
Page<CcdiProjectSuspiciousTransactionItemVO> resultPage =
|
||||
overviewMapper.selectSuspiciousTransactionPage(page, queryDTO);
|
||||
List<CcdiProjectSuspiciousTransactionItemVO> rows =
|
||||
defaultList(resultPage == null ? null : resultPage.getRecords());
|
||||
attachSuspiciousTransactionHitTags(rows, queryDTO);
|
||||
|
||||
CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
|
||||
result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords()));
|
||||
result.setRows(rows);
|
||||
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
|
||||
return result;
|
||||
}
|
||||
@@ -355,6 +406,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
normalizeSuspiciousTransactionQuery(queryDTO);
|
||||
if (isSuspiciousTransactionScopeMismatch(queryDTO)) {
|
||||
return List.of();
|
||||
}
|
||||
prepareSuspiciousTransactionExternalBranch(queryDTO);
|
||||
|
||||
if (isExternalScopeWithoutSubject(queryDTO)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return defaultList(overviewMapper.selectReportSuspiciousTransactionList(queryDTO)).stream()
|
||||
.map(this::buildSuspiciousTransactionExcelRow)
|
||||
@@ -419,9 +478,17 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||
queryDTO.setProjectId(projectId);
|
||||
queryDTO.setSuspiciousType("ALL");
|
||||
queryDTO.setSuspiciousType(SUSPICIOUS_TYPE_ALL);
|
||||
exportRiskDetails(response, queryDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportRiskDetails(
|
||||
HttpServletResponse response,
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
|
||||
) {
|
||||
List<CcdiProjectSuspiciousTransactionExcel> suspiciousRows = exportSuspiciousTransactions(queryDTO);
|
||||
Long projectId = queryDTO.getProjectId();
|
||||
List<CcdiProjectEmployeeCreditNegativeExcel> creditRows = exportEmployeeCreditNegative(projectId);
|
||||
List<CcdiProjectAbnormalAccountExcel> abnormalRows = exportAbnormalAccountPeople(projectId);
|
||||
try {
|
||||
@@ -608,6 +675,60 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
getRequiredProject(projectId);
|
||||
}
|
||||
|
||||
private CcdiProjectRiskExclusion buildRiskExclusion(CcdiProjectRiskExclusionSaveDTO dto) {
|
||||
if (dto == null) {
|
||||
throw new ServiceException("排除参数不能为空");
|
||||
}
|
||||
String exclusionType = normalizeExclusionType(dto.getExclusionType());
|
||||
String ruleCode = normalizeRequired(dto.getRuleCode(), "规则编码不能为空").toUpperCase();
|
||||
String excludeReason = normalizeRequired(dto.getExcludeReason(), "排除原因不能为空");
|
||||
if (excludeReason.length() > 1000) {
|
||||
throw new ServiceException("排除原因不能超过1000个字符");
|
||||
}
|
||||
CcdiProjectRiskExclusion exclusion = new CcdiProjectRiskExclusion();
|
||||
exclusion.setProjectId(dto.getProjectId());
|
||||
exclusion.setRuleCode(ruleCode);
|
||||
exclusion.setExclusionType(exclusionType);
|
||||
exclusion.setExcludeReason(excludeReason);
|
||||
exclusion.setStaffIdCard(normalizeBlankToNull(dto.getStaffIdCard()));
|
||||
exclusion.setBankStatementId(dto.getBankStatementId());
|
||||
if (EXCLUSION_TYPE_STATEMENT.equals(exclusionType) && exclusion.getBankStatementId() == null) {
|
||||
throw new ServiceException("流水型排除必须指定流水ID");
|
||||
}
|
||||
if (EXCLUSION_TYPE_OBJECT.equals(exclusionType) && (exclusion.getStaffIdCard() == null || exclusion.getStaffIdCard().isBlank())) {
|
||||
throw new ServiceException("对象型排除必须指定人员证件号");
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
Date now = new Date();
|
||||
exclusion.setCreateBy(operator);
|
||||
exclusion.setCreateTime(now);
|
||||
exclusion.setUpdateBy(operator);
|
||||
exclusion.setUpdateTime(now);
|
||||
return exclusion;
|
||||
}
|
||||
|
||||
private String normalizeExclusionType(String exclusionType) {
|
||||
String type = normalizeRequired(exclusionType, "排除类型不能为空").toUpperCase();
|
||||
if (!EXCLUSION_TYPE_STATEMENT.equals(type) && !EXCLUSION_TYPE_OBJECT.equals(type)) {
|
||||
throw new ServiceException("排除类型不正确");
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private String normalizeRequired(String value, String message) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String normalizeBlankToNull(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private void normalizeRiskModelPeopleQuery(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
|
||||
if (queryDTO.getMatchMode() == null || queryDTO.getMatchMode().isBlank()) {
|
||||
queryDTO.setMatchMode("ANY");
|
||||
@@ -625,13 +746,64 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
}
|
||||
|
||||
private void normalizeSuspiciousTransactionQuery(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
|
||||
if (queryDTO.getModelCode() != null && !queryDTO.getModelCode().isBlank()) {
|
||||
queryDTO.setModelCode(queryDTO.getModelCode().trim().toUpperCase());
|
||||
} else {
|
||||
queryDTO.setModelCode(null);
|
||||
}
|
||||
if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
|
||||
queryDTO.setSuspiciousType("ALL");
|
||||
queryDTO.setSuspiciousType(SUSPICIOUS_TYPE_ALL);
|
||||
return;
|
||||
}
|
||||
queryDTO.setSuspiciousType(queryDTO.getSuspiciousType().trim().toUpperCase());
|
||||
}
|
||||
|
||||
private void prepareSuspiciousTransactionExternalBranch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
|
||||
if (!shouldCheckExternalPersonBranch(queryDTO)) {
|
||||
queryDTO.setIncludeExternalPerson(false);
|
||||
return;
|
||||
}
|
||||
queryDTO.setIncludeExternalPerson(hasExternalPersonSubject(queryDTO.getProjectId()));
|
||||
}
|
||||
|
||||
private boolean shouldCheckExternalPersonBranch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
|
||||
if (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType())) {
|
||||
return true;
|
||||
}
|
||||
return SUSPICIOUS_TYPE_ALL.equals(queryDTO.getSuspiciousType())
|
||||
&& (queryDTO.getModelCode() == null || isExternalModelCode(queryDTO.getModelCode()));
|
||||
}
|
||||
|
||||
private boolean hasExternalPersonSubject(Long projectId) {
|
||||
return overviewMapper.selectExternalPersonSubjectExistsByProjectId(projectId) != null;
|
||||
}
|
||||
|
||||
private boolean isExternalScopeWithoutSubject(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
|
||||
return (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType())
|
||||
|| isExternalModelCode(queryDTO.getModelCode()))
|
||||
&& !Boolean.TRUE.equals(queryDTO.getIncludeExternalPerson());
|
||||
}
|
||||
|
||||
private boolean isSuspiciousTransactionScopeMismatch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
|
||||
if (queryDTO.getModelCode() == null) {
|
||||
return false;
|
||||
}
|
||||
boolean externalModel = isExternalModelCode(queryDTO.getModelCode());
|
||||
return (SUSPICIOUS_TYPE_MODEL_RULE.equals(queryDTO.getSuspiciousType()) && externalModel)
|
||||
|| (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType()) && !externalModel);
|
||||
}
|
||||
|
||||
private boolean isExternalModelCode(String modelCode) {
|
||||
return modelCode != null && modelCode.startsWith("EXTERNAL_");
|
||||
}
|
||||
|
||||
private CcdiProjectSuspiciousTransactionPageVO emptySuspiciousTransactionPage() {
|
||||
CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
|
||||
result.setRows(List.of());
|
||||
result.setTotal(0L);
|
||||
return result;
|
||||
}
|
||||
|
||||
private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) {
|
||||
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
|
||||
stat.setKey(key);
|
||||
@@ -733,6 +905,8 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
row.setAbnormalType(item.getAbnormalType());
|
||||
row.setAbnormalTime(item.getAbnormalTime());
|
||||
row.setStatus(item.getStatus());
|
||||
row.setReasonDetail(item.getReasonDetail());
|
||||
row.setInvolvedAmount(item.getInvolvedAmount());
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -837,7 +1011,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
return;
|
||||
}
|
||||
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = defaultList(
|
||||
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(projectId, bankStatementIds)
|
||||
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
projectId,
|
||||
bankStatementIds,
|
||||
null,
|
||||
null
|
||||
)
|
||||
).stream().filter(item -> item.getBankStatementId() != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
CcdiBankStatementHitTagVO::getBankStatementId,
|
||||
@@ -849,6 +1028,39 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
)));
|
||||
}
|
||||
|
||||
private void attachSuspiciousTransactionHitTags(
|
||||
List<CcdiProjectSuspiciousTransactionItemVO> rows,
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
|
||||
) {
|
||||
if (rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Long> bankStatementIds = rows.stream()
|
||||
.map(CcdiProjectSuspiciousTransactionItemVO::getBankStatementId)
|
||||
.filter(item -> item != null)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (bankStatementIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = defaultList(
|
||||
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
queryDTO.getProjectId(),
|
||||
bankStatementIds,
|
||||
queryDTO.getModelCode(),
|
||||
queryDTO.getSuspiciousType()
|
||||
)
|
||||
).stream().filter(item -> item.getBankStatementId() != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
CcdiBankStatementHitTagVO::getBankStatementId,
|
||||
LinkedHashMap::new,
|
||||
Collectors.toList()
|
||||
));
|
||||
rows.forEach(row -> row.setHitTags(new ArrayList<>(
|
||||
hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList())
|
||||
)));
|
||||
}
|
||||
|
||||
private void normalizeObjectRows(List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows) {
|
||||
objectRows.forEach(row -> {
|
||||
if (row.getRiskTags() == null) {
|
||||
|
||||
@@ -106,7 +106,7 @@ public class CcdiProjectRiskDetailWorkbookExporter {
|
||||
|
||||
private void writeAbnormalAccountSheet(Sheet sheet, List<CcdiProjectAbnormalAccountExcel> rows) {
|
||||
Row header = sheet.createRow(0);
|
||||
String[] headers = { "账号", "开户人", "银行", "异常类型", "异常发生时间", "状态" };
|
||||
String[] headers = { "账号", "开户人", "银行", "异常类型", "异常发生时间", "状态", "命中原因", "涉及金额" };
|
||||
writeHeader(header, headers);
|
||||
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
@@ -118,6 +118,8 @@ public class CcdiProjectRiskDetailWorkbookExporter {
|
||||
row.createCell(3).setCellValue(safeText(item.getAbnormalType()));
|
||||
row.createCell(4).setCellValue(safeText(item.getAbnormalTime()));
|
||||
row.createCell(5).setCellValue(safeText(item.getStatus()));
|
||||
row.createCell(6).setCellValue(safeText(item.getReasonDetail()));
|
||||
row.createCell(7).setCellValue(safeNumber(item.getInvolvedAmount()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryD
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
|
||||
@@ -25,10 +27,13 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemV
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyIncomeDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
@@ -178,6 +183,37 @@ public class CcdiProjectSpecialCheckServiceImpl implements ICcdiProjectSpecialCh
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiProjectIncreaseLendingListVO getIncreaseLendingList(CcdiProjectIncreaseLendingQueryDTO queryDTO) {
|
||||
validateIncreaseLendingQuery(queryDTO);
|
||||
|
||||
Page<CcdiProjectIncreaseLendingListItemVO> page = new Page<>(
|
||||
defaultPageNum(queryDTO.getPageNum()),
|
||||
defaultPageSize(queryDTO.getPageSize())
|
||||
);
|
||||
Page<CcdiProjectIncreaseLendingListItemVO> resultPage = specialCheckMapper.selectIncreaseLendingPage(page, queryDTO);
|
||||
|
||||
CcdiProjectIncreaseLendingListVO result = new CcdiProjectIncreaseLendingListVO();
|
||||
result.setRows(resultPage == null ? List.of() : defaultList(resultPage.getRecords()));
|
||||
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CcdiProjectIncreaseLendingExcel> exportIncreaseLendingList(CcdiProjectIncreaseLendingQueryDTO queryDTO) {
|
||||
validateIncreaseLendingQuery(queryDTO);
|
||||
return defaultList(specialCheckMapper.selectIncreaseLendingExportList(queryDTO));
|
||||
}
|
||||
|
||||
private void validateIncreaseLendingQuery(CcdiProjectIncreaseLendingQueryDTO queryDTO) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
if (StringUtils.isBlank(queryDTO.getStaffId())
|
||||
&& StringUtils.isBlank(queryDTO.getStaffIdCard())
|
||||
&& StringUtils.isBlank(queryDTO.getApprover())) {
|
||||
throw new ServiceException("柜员号、员工身份证和审核人柜员号至少填写一项");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureProjectExists(Long projectId) {
|
||||
CcdiProject project = projectMapper.selectById(projectId);
|
||||
if (project == null) {
|
||||
@@ -191,6 +227,7 @@ public class CcdiProjectSpecialCheckServiceImpl implements ICcdiProjectSpecialCh
|
||||
incomeDetail.setSelfIncome(BigDecimal.ZERO);
|
||||
incomeDetail.setSpouseIncome(BigDecimal.ZERO);
|
||||
incomeDetail.setTotalIncome(BigDecimal.ZERO);
|
||||
incomeDetail.setExplainableIncome(BigDecimal.ZERO);
|
||||
detail.setIncomeDetail(incomeDetail);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1690,6 +1730,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and tx.txDate < ai.invalid_date
|
||||
group by staff.id_card, ai.account_no, ai.invalid_date
|
||||
) t
|
||||
where t.windowTotalAmount >= 500000
|
||||
or t.windowMaxSingleAmount >= 100000
|
||||
</select>
|
||||
|
||||
<select id="selectDormantAccountLargeActivationObjects" resultMap="BankTagObjectHitResultMap">
|
||||
@@ -1710,6 +1752,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
staff.id_card AS objectKey,
|
||||
ai.account_no AS accountNo,
|
||||
ai.effective_date AS effectiveDate,
|
||||
project_window.projectStartDate AS projectStartDate,
|
||||
min(tx.txDate) AS firstTxDate,
|
||||
timestampdiff(MONTH, ai.effective_date, min(tx.txDate)) AS dormantMonths,
|
||||
round(sum(tx.tradeTotalAmount), 2) AS windowTotalAmount,
|
||||
@@ -1717,6 +1760,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from ccdi_account_info ai
|
||||
inner join ccdi_base_staff staff
|
||||
on staff.id_card = ai.owner_id
|
||||
inner join (
|
||||
select
|
||||
min(COALESCE(
|
||||
STR_TO_DATE(LEFT(TRIM(project_bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s'),
|
||||
STR_TO_DATE(LEFT(TRIM(project_bs.TRX_DATE), 10), '%Y-%m-%d')
|
||||
)) AS projectStartDate
|
||||
from ccdi_bank_statement project_bs
|
||||
where project_bs.project_id = #{projectId}
|
||||
) project_window
|
||||
inner join (
|
||||
select
|
||||
trim(bs.LE_ACCOUNT_NO) AS accountNo,
|
||||
@@ -1734,8 +1786,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where ai.owner_type = 'EMPLOYEE'
|
||||
and ai.status = 1
|
||||
and ai.effective_date is not null
|
||||
group by staff.id_card, ai.account_no, ai.effective_date
|
||||
and project_window.projectStartDate is not null
|
||||
group by staff.id_card, ai.account_no, ai.effective_date, project_window.projectStartDate
|
||||
having min(tx.txDate) >= DATE_ADD(ai.effective_date, INTERVAL 6 MONTH)
|
||||
and project_window.projectStartDate <= DATE_SUB(min(tx.txDate), INTERVAL 6 MONTH)
|
||||
) t
|
||||
where t.windowTotalAmount >= 500000
|
||||
or t.windowMaxSingleAmount >= 100000
|
||||
|
||||
@@ -30,6 +30,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="CcdiBankStatementHitTagVOResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO">
|
||||
<result property="modelCode" column="model_code"/>
|
||||
<result property="ruleCode" column="rule_code"/>
|
||||
<result property="ruleName" column="rule_name"/>
|
||||
<result property="riskLevel" column="risk_level"/>
|
||||
@@ -47,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectStatementTagsByProjectAndStatementIds" resultMap="CcdiBankStatementHitTagVOResultMap">
|
||||
select
|
||||
model_code,
|
||||
rule_code,
|
||||
rule_name,
|
||||
risk_level,
|
||||
@@ -55,10 +57,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from ccdi_bank_statement_tag_result
|
||||
where project_id = #{projectId}
|
||||
and bank_statement_id is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = ccdi_bank_statement_tag_result.project_id
|
||||
and ex.rule_code = ccdi_bank_statement_tag_result.rule_code
|
||||
and ex.exclusion_type = 'STATEMENT'
|
||||
and ex.bank_statement_id = ccdi_bank_statement_tag_result.bank_statement_id
|
||||
)
|
||||
and bank_statement_id IN
|
||||
<foreach collection="bankStatementIds" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
<if test="modelCode != null and modelCode != ''">
|
||||
and model_code = #{modelCode}
|
||||
</if>
|
||||
<choose>
|
||||
<when test="suspiciousType == 'MODEL_RULE'">
|
||||
and left(model_code, 9) != 'EXTERNAL_'
|
||||
</when>
|
||||
<when test="suspiciousType == 'EXTERNAL_PERSON'">
|
||||
and left(model_code, 9) = 'EXTERNAL_'
|
||||
</when>
|
||||
</choose>
|
||||
order by bank_statement_id asc, id asc
|
||||
</select>
|
||||
|
||||
|
||||
@@ -79,6 +79,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
on dept.dept_id = coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id)
|
||||
where tr.project_id = #{projectId}
|
||||
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = tr.project_id
|
||||
and ex.rule_code = tr.rule_code
|
||||
and (
|
||||
(ex.exclusion_type = 'STATEMENT'
|
||||
and ex.bank_statement_id = tr.bank_statement_id)
|
||||
or (ex.exclusion_type = 'OBJECT'
|
||||
and ex.staff_id_card = coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card))
|
||||
)
|
||||
)
|
||||
</sql>
|
||||
|
||||
<delete id="deleteByProjectId">
|
||||
|
||||
@@ -68,6 +68,10 @@
|
||||
<resultMap id="SuspiciousTransactionItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO">
|
||||
<id property="bankStatementId" column="bankStatementId"/>
|
||||
<result property="trxDate" column="trxDate"/>
|
||||
<result property="leAccountNo" column="leAccountNo"/>
|
||||
<result property="leAccountName" column="leAccountName"/>
|
||||
<result property="customerAccountName" column="customerAccountName"/>
|
||||
<result property="customerAccountNo" column="customerAccountNo"/>
|
||||
<result property="suspiciousPersonName" column="suspiciousPersonName"/>
|
||||
<result property="relatedPersonName" column="relatedPersonName"/>
|
||||
<result property="relatedStaffName" column="relatedStaffName"/>
|
||||
@@ -88,6 +92,8 @@
|
||||
<result property="abnormalType" column="abnormalType"/>
|
||||
<result property="abnormalTime" column="abnormal_time"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="reasonDetail" column="reasonDetail"/>
|
||||
<result property="involvedAmount" column="involved_amount"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="ReportUploadSubjectResultMap"
|
||||
@@ -187,6 +193,18 @@
|
||||
on relation.person_id = family_staff.id_card
|
||||
where tr.project_id = #{projectId}
|
||||
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = tr.project_id
|
||||
and ex.rule_code = tr.rule_code
|
||||
and (
|
||||
(ex.exclusion_type = 'STATEMENT'
|
||||
and ex.bank_statement_id = tr.bank_statement_id)
|
||||
or (ex.exclusion_type = 'OBJECT'
|
||||
and ex.staff_id_card = coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card))
|
||||
)
|
||||
)
|
||||
</sql>
|
||||
|
||||
<sql id="employeeRiskAggregateSql">
|
||||
@@ -662,6 +680,14 @@
|
||||
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
|
||||
and counter_intermediary.name = trim(bs.CUSTOMER_ACCOUNT_NAME)
|
||||
where trim(ifnull(bs.LE_ACCOUNT_NAME, '')) != trim(ifnull(bs.CUSTOMER_ACCOUNT_NAME, ''))
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = tr.project_id
|
||||
and ex.rule_code = tr.rule_code
|
||||
and ex.exclusion_type = 'STATEMENT'
|
||||
and ex.bank_statement_id = tr.bank_statement_id
|
||||
)
|
||||
|
||||
union all
|
||||
|
||||
@@ -689,6 +715,14 @@
|
||||
and tr.object_type = 'EXTERNAL_CERT_NO'
|
||||
and tr.object_key = subject.cert_no
|
||||
and tr.model_code in <include refid="externalModelCodeFilterSql"/>
|
||||
where not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = tr.project_id
|
||||
and ex.rule_code = tr.rule_code
|
||||
and ex.exclusion_type = 'OBJECT'
|
||||
and ex.staff_id_card = subject.cert_no
|
||||
)
|
||||
</sql>
|
||||
|
||||
<sql id="externalPersonAggregateSql">
|
||||
@@ -783,6 +817,22 @@
|
||||
and risk.cert_no = subject.cert_no
|
||||
</select>
|
||||
|
||||
<select id="selectExternalPersonSubjectExistsByProjectId" resultType="java.lang.Integer">
|
||||
select 1
|
||||
from ccdi_bank_statement bs
|
||||
left join ccdi_base_staff staff
|
||||
on staff.id_card = bs.cret_no
|
||||
left join ccdi_staff_fmy_relation relation
|
||||
on relation.status = 1
|
||||
and relation.relation_cert_no = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and bs.cret_no is not null
|
||||
and trim(bs.cret_no) != ''
|
||||
and staff.id_card is null
|
||||
and relation.relation_cert_no is null
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectExternalRiskModelCardsByProjectId" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO">
|
||||
<bind name="externalProjectId" value="projectId"/>
|
||||
select
|
||||
@@ -972,6 +1022,29 @@
|
||||
from ccdi_bank_statement_tag_result tr
|
||||
where tr.project_id = #{query.projectId}
|
||||
and tr.bank_statement_id is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = tr.project_id
|
||||
and ex.rule_code = tr.rule_code
|
||||
and ex.exclusion_type = 'STATEMENT'
|
||||
and ex.bank_statement_id = tr.bank_statement_id
|
||||
)
|
||||
<include refid="suspiciousTransactionModelScopeSql"/>
|
||||
</sql>
|
||||
|
||||
<sql id="suspiciousTransactionModelScopeSql">
|
||||
<if test="query.modelCode != null and query.modelCode != ''">
|
||||
and tr.model_code = #{query.modelCode}
|
||||
</if>
|
||||
<choose>
|
||||
<when test="query.suspiciousType == 'MODEL_RULE'">
|
||||
and left(tr.model_code, 9) != 'EXTERNAL_'
|
||||
</when>
|
||||
<when test="query.suspiciousType == 'EXTERNAL_PERSON'">
|
||||
and left(tr.model_code, 9) = 'EXTERNAL_'
|
||||
</when>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<sql id="suspiciousTransactionNameHitSql">
|
||||
@@ -1122,26 +1195,28 @@
|
||||
<include refid="suspiciousTransactionNameHitSql"/>
|
||||
) name_hits on name_hits.bankStatementId = base.bankStatementId
|
||||
|
||||
union all
|
||||
<if test="query.includeExternalPerson == true">
|
||||
union all
|
||||
|
||||
select
|
||||
external_hits.bankStatementId,
|
||||
external_hits.trxDate,
|
||||
external_hits.relatedPersonName,
|
||||
external_hits.relatedStaffName,
|
||||
external_hits.relatedStaffCode,
|
||||
external_hits.relationType,
|
||||
external_hits.userMemo,
|
||||
external_hits.cashType,
|
||||
external_hits.displayAmount,
|
||||
external_hits.hasModelRuleHit,
|
||||
external_hits.hasNameListHit,
|
||||
external_hits.suspiciousPersonName,
|
||||
external_hits.matchPriority,
|
||||
external_hits.nameListHitType
|
||||
from (
|
||||
<include refid="externalSuspiciousTransactionSql"/>
|
||||
) external_hits
|
||||
select
|
||||
external_hits.bankStatementId,
|
||||
external_hits.trxDate,
|
||||
external_hits.relatedPersonName,
|
||||
external_hits.relatedStaffName,
|
||||
external_hits.relatedStaffCode,
|
||||
external_hits.relationType,
|
||||
external_hits.userMemo,
|
||||
external_hits.cashType,
|
||||
external_hits.displayAmount,
|
||||
external_hits.hasModelRuleHit,
|
||||
external_hits.hasNameListHit,
|
||||
external_hits.suspiciousPersonName,
|
||||
external_hits.matchPriority,
|
||||
external_hits.nameListHitType
|
||||
from (
|
||||
<include refid="externalSuspiciousTransactionSql"/>
|
||||
) external_hits
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<sql id="suspiciousTransactionAggregatedSql">
|
||||
@@ -1203,6 +1278,23 @@
|
||||
where final_result.hasModelRuleHit = 1 or final_result.hasNameListHit = 1
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="query.modelCode != null and query.modelCode != ''">
|
||||
and exists (
|
||||
select 1
|
||||
from ccdi_bank_statement_tag_result model_filter
|
||||
where model_filter.project_id = #{query.projectId}
|
||||
and model_filter.bank_statement_id = final_result.bankStatementId
|
||||
and model_filter.model_code = #{query.modelCode}
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = model_filter.project_id
|
||||
and ex.rule_code = model_filter.rule_code
|
||||
and ex.exclusion_type = 'STATEMENT'
|
||||
and ex.bank_statement_id = model_filter.bank_statement_id
|
||||
)
|
||||
)
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="selectSuspiciousTransactionPage" resultMap="SuspiciousTransactionItemResultMap">
|
||||
@@ -1213,6 +1305,10 @@
|
||||
select
|
||||
final_result.bankStatementId,
|
||||
final_result.trxDate,
|
||||
bs.LE_ACCOUNT_NO as leAccountNo,
|
||||
bs.LE_ACCOUNT_NAME as leAccountName,
|
||||
bs.CUSTOMER_ACCOUNT_NAME as customerAccountName,
|
||||
bs.CUSTOMER_ACCOUNT_NO as customerAccountNo,
|
||||
final_result.suspiciousPersonName,
|
||||
final_result.relatedPersonName,
|
||||
final_result.relatedStaffName,
|
||||
@@ -1227,6 +1323,8 @@
|
||||
from (
|
||||
<include refid="suspiciousTransactionAggregatedSql"/>
|
||||
) final_result
|
||||
inner join ccdi_bank_statement bs
|
||||
on bs.bank_statement_id = final_result.bankStatementId
|
||||
<include refid="suspiciousTransactionFilterSql"/>
|
||||
order by final_result.trxDate desc, final_result.bankStatementId desc
|
||||
</select>
|
||||
@@ -1235,6 +1333,10 @@
|
||||
select
|
||||
final_result.bankStatementId,
|
||||
final_result.trxDate,
|
||||
bs.LE_ACCOUNT_NO as leAccountNo,
|
||||
bs.LE_ACCOUNT_NAME as leAccountName,
|
||||
bs.CUSTOMER_ACCOUNT_NAME as customerAccountName,
|
||||
bs.CUSTOMER_ACCOUNT_NO as customerAccountNo,
|
||||
final_result.suspiciousPersonName,
|
||||
final_result.relatedPersonName,
|
||||
final_result.relatedStaffName,
|
||||
@@ -1249,6 +1351,8 @@
|
||||
from (
|
||||
<include refid="suspiciousTransactionAggregatedSql"/>
|
||||
) final_result
|
||||
inner join ccdi_bank_statement bs
|
||||
on bs.bank_statement_id = final_result.bankStatementId
|
||||
<include refid="suspiciousTransactionFilterSql"/>
|
||||
order by final_result.trxDate desc, final_result.bankStatementId desc
|
||||
</select>
|
||||
@@ -1293,6 +1397,15 @@
|
||||
from ccdi_bank_statement_tag_result tr
|
||||
where tr.project_id = #{query.projectId}
|
||||
and tr.bank_statement_id is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = tr.project_id
|
||||
and ex.rule_code = tr.rule_code
|
||||
and ex.exclusion_type = 'STATEMENT'
|
||||
and ex.bank_statement_id = tr.bank_statement_id
|
||||
)
|
||||
<include refid="suspiciousTransactionModelScopeSql"/>
|
||||
group by tr.bank_statement_id
|
||||
) tag_result on tag_result.bank_statement_id = final_result.bankStatementId
|
||||
<include refid="suspiciousTransactionFilterSql"/>
|
||||
@@ -1362,7 +1475,12 @@
|
||||
when account.status = 1 then '正常'
|
||||
when account.status = 2 then '已销户'
|
||||
else cast(account.status as char)
|
||||
end as status
|
||||
end as status,
|
||||
tr.reason_detail as reasonDetail,
|
||||
greatest(
|
||||
ifnull(cast(substring_index(substring_index(tr.reason_detail, '累计交易金额', -1), '元', 1) as decimal(18, 2)), 0),
|
||||
ifnull(cast(substring_index(substring_index(tr.reason_detail, '单笔最大金额', -1), '元', 1) as decimal(18, 2)), 0)
|
||||
) as involved_amount
|
||||
from ccdi_bank_statement_tag_result tr
|
||||
inner join ccdi_account_info account
|
||||
on account.owner_type = 'EMPLOYEE'
|
||||
@@ -1389,7 +1507,9 @@
|
||||
abnormal.bankName,
|
||||
abnormal.abnormalType,
|
||||
abnormal.abnormal_time,
|
||||
abnormal.status
|
||||
abnormal.status,
|
||||
abnormal.reasonDetail,
|
||||
abnormal.involved_amount
|
||||
from (
|
||||
<include refid="abnormalAccountBaseSql"/>
|
||||
where tr.project_id = #{query.projectId}
|
||||
@@ -1412,7 +1532,9 @@
|
||||
abnormal.bankName,
|
||||
abnormal.abnormalType,
|
||||
abnormal.abnormal_time,
|
||||
abnormal.status
|
||||
abnormal.status,
|
||||
abnormal.reasonDetail,
|
||||
abnormal.involved_amount
|
||||
from (
|
||||
<include refid="abnormalAccountBaseSql"/>
|
||||
where tr.project_id = #{projectId}
|
||||
@@ -1522,6 +1644,7 @@
|
||||
<select id="selectPersonAnalysisObjectRows" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO">
|
||||
select
|
||||
max(tr.model_code) as modelCode,
|
||||
tr.rule_code as ruleCode,
|
||||
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 '员工对象'
|
||||
@@ -1538,6 +1661,14 @@
|
||||
and tr.object_key = relation.relation_cert_no
|
||||
where tr.project_id = #{projectId}
|
||||
and tr.bank_statement_id is null
|
||||
and not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = tr.project_id
|
||||
and ex.rule_code = tr.rule_code
|
||||
and ex.exclusion_type = 'OBJECT'
|
||||
and ex.staff_id_card = #{staffIdCard}
|
||||
)
|
||||
and (
|
||||
tr.object_key = #{staffIdCard}
|
||||
or exists (
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiProjectRiskExclusionMapper">
|
||||
|
||||
<resultMap id="CcdiProjectRiskExclusionMap"
|
||||
type="com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion">
|
||||
<id property="id" column="id"/>
|
||||
<result property="projectId" column="project_id"/>
|
||||
<result property="staffIdCard" column="staff_id_card"/>
|
||||
<result property="ruleCode" column="rule_code"/>
|
||||
<result property="exclusionType" column="exclusion_type"/>
|
||||
<result property="bankStatementId" column="bank_statement_id"/>
|
||||
<result property="excludeReason" column="exclude_reason"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
<result property="remark" column="remark"/>
|
||||
</resultMap>
|
||||
|
||||
<insert id="upsertExclusion">
|
||||
insert into ccdi_project_risk_exclusion (
|
||||
project_id, staff_id_card, rule_code, exclusion_type, bank_statement_id,
|
||||
exclude_reason, create_by, create_time, update_by, update_time, remark
|
||||
) values (
|
||||
#{exclusion.projectId}, #{exclusion.staffIdCard}, #{exclusion.ruleCode},
|
||||
#{exclusion.exclusionType}, #{exclusion.bankStatementId}, #{exclusion.excludeReason},
|
||||
#{exclusion.createBy}, #{exclusion.createTime}, #{exclusion.updateBy},
|
||||
#{exclusion.updateTime}, #{exclusion.remark}
|
||||
)
|
||||
on duplicate key update
|
||||
exclude_reason = values(exclude_reason),
|
||||
update_by = values(update_by),
|
||||
update_time = values(update_time),
|
||||
remark = values(remark)
|
||||
</insert>
|
||||
|
||||
<select id="countExistingRiskHit" resultType="int">
|
||||
select count(1)
|
||||
from ccdi_bank_statement_tag_result tr
|
||||
where tr.project_id = #{exclusion.projectId}
|
||||
and tr.rule_code = #{exclusion.ruleCode}
|
||||
<choose>
|
||||
<when test="exclusion.exclusionType == 'STATEMENT'">
|
||||
and tr.bank_statement_id = #{exclusion.bankStatementId}
|
||||
</when>
|
||||
<otherwise>
|
||||
and (
|
||||
tr.object_key = #{exclusion.staffIdCard}
|
||||
or exists (
|
||||
select 1
|
||||
from ccdi_bank_statement bs
|
||||
where bs.bank_statement_id = tr.bank_statement_id
|
||||
and bs.cret_no = #{exclusion.staffIdCard}
|
||||
)
|
||||
or exists (
|
||||
select 1
|
||||
from ccdi_staff_fmy_relation relation
|
||||
where relation.status = 1
|
||||
and relation.person_id = #{exclusion.staffIdCard}
|
||||
and relation.relation_cert_no = tr.object_key
|
||||
)
|
||||
or exists (
|
||||
select 1
|
||||
from ccdi_staff_fmy_relation relation
|
||||
inner join ccdi_bank_statement bs
|
||||
on bs.cret_no = relation.relation_cert_no
|
||||
where relation.status = 1
|
||||
and relation.person_id = #{exclusion.staffIdCard}
|
||||
and bs.bank_statement_id = tr.bank_statement_id
|
||||
)
|
||||
)
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -9,9 +9,14 @@
|
||||
<result property="staffName" column="staff_name"/>
|
||||
<result property="deptName" column="dept_name"/>
|
||||
<result property="totalIncome" column="total_income"/>
|
||||
<result property="selfIncome" column="self_income"/>
|
||||
<result property="spouseIncome" column="spouse_income"/>
|
||||
<result property="employmentYears" column="employment_years"/>
|
||||
<result property="totalAsset" column="total_asset"/>
|
||||
<result property="totalDebt" column="total_debt"/>
|
||||
<result property="comparisonAmount" column="comparison_amount"/>
|
||||
<result property="explainableIncome" column="explainable_income"/>
|
||||
<result property="assetIncomeRatio" column="asset_income_ratio"/>
|
||||
<result property="riskLevelCode" column="risk_level_code"/>
|
||||
<result property="riskLevelName" column="risk_level_name"/>
|
||||
</resultMap>
|
||||
@@ -67,13 +72,37 @@
|
||||
<result property="transferDate" column="transfer_date"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="IncreaseLendingListItemResultMap"
|
||||
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListItemVO">
|
||||
<result property="staffId" column="staff_id"/>
|
||||
<result property="staffName" column="staff_name"/>
|
||||
<result property="staffIdCard" column="staff_id_card"/>
|
||||
<result property="deptName" column="dept_name"/>
|
||||
<result property="contractNo" column="contract_no"/>
|
||||
<result property="lendingOrgNo" column="lending_org_no"/>
|
||||
<result property="borrowerName" column="borrower_name"/>
|
||||
<result property="borrowerCertNo" column="borrower_cert_no"/>
|
||||
<result property="loanProduct" column="loan_product"/>
|
||||
<result property="contractAmount" column="contract_amount"/>
|
||||
<result property="loanBalance" column="loan_balance"/>
|
||||
<result property="loanStartDate" column="loan_start_date"/>
|
||||
<result property="loanEndDate" column="loan_end_date"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="fiveClassification" column="five_classification"/>
|
||||
<result property="customerManagerId" column="customer_manager_id"/>
|
||||
<result property="customerManagerName" column="customer_manager_name"/>
|
||||
<result property="approver" column="approver"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="FamilyAssetLiabilityDetailResultMap"
|
||||
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO">
|
||||
<association property="incomeDetail"
|
||||
javaType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyIncomeDetailVO">
|
||||
<result property="selfIncome" column="income_self_income"/>
|
||||
<result property="spouseIncome" column="income_spouse_income"/>
|
||||
<result property="employmentYears" column="income_employment_years"/>
|
||||
<result property="totalIncome" column="income_total_income"/>
|
||||
<result property="explainableIncome" column="income_explainable_income"/>
|
||||
</association>
|
||||
<association property="assetDetail"
|
||||
javaType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetDetailVO">
|
||||
@@ -104,9 +133,14 @@
|
||||
<result property="staffName" column="summary_staff_name"/>
|
||||
<result property="deptName" column="summary_dept_name"/>
|
||||
<result property="totalIncome" column="summary_total_income"/>
|
||||
<result property="selfIncome" column="summary_self_income"/>
|
||||
<result property="spouseIncome" column="summary_spouse_income"/>
|
||||
<result property="employmentYears" column="summary_employment_years"/>
|
||||
<result property="totalAsset" column="summary_total_asset"/>
|
||||
<result property="totalDebt" column="summary_total_debt"/>
|
||||
<result property="comparisonAmount" column="summary_comparison_amount"/>
|
||||
<result property="explainableIncome" column="summary_explainable_income"/>
|
||||
<result property="assetIncomeRatio" column="summary_asset_income_ratio"/>
|
||||
<result property="riskLevelCode" column="summary_risk_level_code"/>
|
||||
<result property="riskLevelName" column="summary_risk_level_name"/>
|
||||
</association>
|
||||
@@ -172,44 +206,58 @@
|
||||
aggregated.staff_name,
|
||||
aggregated.dept_name,
|
||||
aggregated.total_income,
|
||||
aggregated.self_income,
|
||||
aggregated.spouse_income,
|
||||
aggregated.employment_years,
|
||||
aggregated.total_asset,
|
||||
aggregated.total_debt,
|
||||
aggregated.comparison_amount,
|
||||
aggregated.explainable_income,
|
||||
aggregated.asset_income_ratio,
|
||||
case
|
||||
when aggregated.missing_asset_info = 1 or aggregated.missing_debt_info = 1 then 'MISSING_INFO'
|
||||
when comparison_amount <= total_asset * 1.5 then 'NORMAL'
|
||||
when comparison_amount > total_asset * 1.5 and comparison_amount <= total_asset * 3 then 'RISK'
|
||||
when comparison_amount > total_asset * 3 then 'HIGH'
|
||||
when aggregated.missing_info = 1 then 'MISSING_INFO'
|
||||
when aggregated.asset_income_ratio <= 1.5 then 'NORMAL'
|
||||
when aggregated.asset_income_ratio > 1.5 and aggregated.asset_income_ratio <= 3 then 'RISK'
|
||||
when aggregated.asset_income_ratio > 3 then 'HIGH'
|
||||
else 'HIGH'
|
||||
end as risk_level_code,
|
||||
case
|
||||
when aggregated.missing_asset_info = 1 or aggregated.missing_debt_info = 1 then '缺少信息'
|
||||
when comparison_amount <= total_asset * 1.5 then '正常'
|
||||
when comparison_amount > total_asset * 1.5 and comparison_amount <= total_asset * 3 then '存在风险'
|
||||
when comparison_amount > total_asset * 3 then '高风险'
|
||||
else '高风险'
|
||||
when aggregated.missing_info = 1 then '缺少信息'
|
||||
when aggregated.asset_income_ratio <= 1.5 then '正常'
|
||||
when aggregated.asset_income_ratio > 1.5 and aggregated.asset_income_ratio <= 3 then '关注'
|
||||
when aggregated.asset_income_ratio > 3 then '高关注'
|
||||
else '高关注'
|
||||
end as risk_level_name
|
||||
from (
|
||||
select
|
||||
source.*,
|
||||
case
|
||||
when source.self_asset_record_count = 0
|
||||
or source.spouse_staff_asset_record_count = 0 then 1
|
||||
when source.self_income <= 0
|
||||
or source.employment_years is null
|
||||
or source.total_asset <= 0 then 1
|
||||
else 0
|
||||
end as missing_asset_info,
|
||||
end as missing_info,
|
||||
source.self_income * source.employment_years
|
||||
+ source.spouse_income
|
||||
+ source.total_debt as explainable_income,
|
||||
source.self_income * source.employment_years
|
||||
+ source.spouse_income
|
||||
+ source.total_debt as comparison_amount,
|
||||
case
|
||||
when source.self_debt_record_count = 0
|
||||
or source.spouse_staff_debt_record_count = 0 then 1
|
||||
else 0
|
||||
end as missing_debt_info,
|
||||
when source.self_income <= 0
|
||||
or source.employment_years is null
|
||||
or source.total_asset <= 0
|
||||
or (source.self_income * source.employment_years + source.spouse_income + source.total_debt) <= 0 then null
|
||||
else source.total_asset / (source.self_income * source.employment_years + source.spouse_income + source.total_debt)
|
||||
end as asset_income_ratio,
|
||||
case
|
||||
when source.self_asset_record_count = 0
|
||||
or source.spouse_staff_asset_record_count = 0
|
||||
or source.self_debt_record_count = 0
|
||||
or source.spouse_staff_debt_record_count = 0 then 4
|
||||
when source.comparison_amount <= source.total_asset * 1.5 then 1
|
||||
when source.comparison_amount <= source.total_asset * 3 then 2
|
||||
when source.comparison_amount > source.total_asset * 3 then 3
|
||||
when source.self_income <= 0
|
||||
or source.employment_years is null
|
||||
or source.total_asset <= 0
|
||||
or (source.self_income * source.employment_years + source.spouse_income + source.total_debt) <= 0 then 0
|
||||
when source.total_asset / (source.self_income * source.employment_years + source.spouse_income + source.total_debt) <= 1.5 then 1
|
||||
when source.total_asset / (source.self_income * source.employment_years + source.spouse_income + source.total_debt) <= 3 then 2
|
||||
when source.total_asset / (source.self_income * source.employment_years + source.spouse_income + source.total_debt) > 3 then 3
|
||||
else 3
|
||||
end as risk_level_sort
|
||||
from (
|
||||
@@ -218,6 +266,12 @@
|
||||
scope.staff_code,
|
||||
scope.staff_name,
|
||||
scope.dept_name,
|
||||
coalesce(base_staff.annual_income, 0) as self_income,
|
||||
coalesce(spouse.spouse_income, 0) as spouse_income,
|
||||
case
|
||||
when base_staff.hire_date is null then null
|
||||
else greatest(1, ceiling(datediff(curdate(), base_staff.hire_date) / 365))
|
||||
end as employment_years,
|
||||
coalesce(base_staff.annual_income, 0) + coalesce(spouse.spouse_income, 0) as total_income,
|
||||
coalesce((
|
||||
select count(1)
|
||||
@@ -272,15 +326,7 @@
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as total_debt,
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
+ coalesce(spouse.spouse_income, 0)
|
||||
+ coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as comparison_amount
|
||||
), 0) as total_debt
|
||||
from (
|
||||
<include refid="projectEmployeeScopeSql"/>
|
||||
) scope
|
||||
@@ -306,7 +352,9 @@
|
||||
aggregated.dept_name,
|
||||
aggregated.self_income as income_self_income,
|
||||
aggregated.spouse_income as income_spouse_income,
|
||||
aggregated.employment_years as income_employment_years,
|
||||
aggregated.total_income as income_total_income,
|
||||
aggregated.explainable_income as income_explainable_income,
|
||||
aggregated.missing_self_asset_info as asset_missing_self_asset_info,
|
||||
aggregated.self_total_asset as asset_self_total_asset,
|
||||
aggregated.spouse_total_asset as asset_spouse_total_asset,
|
||||
@@ -320,36 +368,61 @@
|
||||
aggregated.staff_name as summary_staff_name,
|
||||
aggregated.dept_name as summary_dept_name,
|
||||
aggregated.total_income as summary_total_income,
|
||||
aggregated.self_income as summary_self_income,
|
||||
aggregated.spouse_income as summary_spouse_income,
|
||||
aggregated.employment_years as summary_employment_years,
|
||||
aggregated.total_asset as summary_total_asset,
|
||||
aggregated.total_debt as summary_total_debt,
|
||||
aggregated.comparison_amount as summary_comparison_amount,
|
||||
aggregated.explainable_income as summary_explainable_income,
|
||||
aggregated.asset_income_ratio as summary_asset_income_ratio,
|
||||
case
|
||||
when aggregated.missing_self_asset_info = 1 or aggregated.missing_self_debt_info = 1 then 'MISSING_INFO'
|
||||
when comparison_amount <= total_asset * 1.5 then 'NORMAL'
|
||||
when comparison_amount > total_asset * 1.5 and comparison_amount <= total_asset * 3 then 'RISK'
|
||||
when comparison_amount > total_asset * 3 then 'HIGH'
|
||||
when aggregated.missing_info = 1 then 'MISSING_INFO'
|
||||
when aggregated.asset_income_ratio <= 1.5 then 'NORMAL'
|
||||
when aggregated.asset_income_ratio > 1.5 and aggregated.asset_income_ratio <= 3 then 'RISK'
|
||||
when aggregated.asset_income_ratio > 3 then 'HIGH'
|
||||
else 'HIGH'
|
||||
end as summary_risk_level_code,
|
||||
case
|
||||
when aggregated.missing_self_asset_info = 1 or aggregated.missing_self_debt_info = 1 then '缺少信息'
|
||||
when comparison_amount <= total_asset * 1.5 then '正常'
|
||||
when comparison_amount > total_asset * 1.5 and comparison_amount <= total_asset * 3 then '存在风险'
|
||||
when comparison_amount > total_asset * 3 then '高风险'
|
||||
else '高风险'
|
||||
when aggregated.missing_info = 1 then '缺少信息'
|
||||
when aggregated.asset_income_ratio <= 1.5 then '正常'
|
||||
when aggregated.asset_income_ratio > 1.5 and aggregated.asset_income_ratio <= 3 then '关注'
|
||||
when aggregated.asset_income_ratio > 3 then '高关注'
|
||||
else '高关注'
|
||||
end as summary_risk_level_name
|
||||
from (
|
||||
select
|
||||
source.*,
|
||||
case
|
||||
when source.self_asset_record_count = 0
|
||||
or source.spouse_staff_asset_record_count = 0 then 1
|
||||
when source.self_income <= 0
|
||||
or source.employment_years is null
|
||||
or source.total_asset <= 0 then 1
|
||||
else 0
|
||||
end as missing_self_asset_info,
|
||||
case
|
||||
when source.self_debt_record_count = 0
|
||||
or source.spouse_staff_debt_record_count = 0 then 1
|
||||
else 0
|
||||
end as missing_self_debt_info
|
||||
end as missing_self_debt_info,
|
||||
case
|
||||
when source.self_income <= 0
|
||||
or source.employment_years is null
|
||||
or source.total_asset <= 0 then 1
|
||||
else 0
|
||||
end as missing_info,
|
||||
source.self_income * source.employment_years
|
||||
+ source.spouse_income
|
||||
+ source.total_debt as explainable_income,
|
||||
source.self_income * source.employment_years
|
||||
+ source.spouse_income
|
||||
+ source.total_debt as comparison_amount,
|
||||
case
|
||||
when source.self_income <= 0
|
||||
or source.employment_years is null
|
||||
or source.total_asset <= 0
|
||||
or (source.self_income * source.employment_years + source.spouse_income + source.total_debt) <= 0 then null
|
||||
else source.total_asset / (source.self_income * source.employment_years + source.spouse_income + source.total_debt)
|
||||
end as asset_income_ratio
|
||||
from (
|
||||
select
|
||||
#{projectId} as project_id,
|
||||
@@ -361,6 +434,10 @@
|
||||
spouse.spouse_is_staff,
|
||||
coalesce(base_staff.annual_income, 0) as self_income,
|
||||
coalesce(spouse.spouse_income, 0) as spouse_income,
|
||||
case
|
||||
when base_staff.hire_date is null then null
|
||||
else greatest(1, ceiling(datediff(curdate(), base_staff.hire_date) / 365))
|
||||
end as employment_years,
|
||||
coalesce(base_staff.annual_income, 0) + coalesce(spouse.spouse_income, 0) as total_income,
|
||||
coalesce((
|
||||
select count(1)
|
||||
@@ -449,15 +526,7 @@
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as total_debt,
|
||||
coalesce(base_staff.annual_income, 0)
|
||||
+ coalesce(spouse.spouse_income, 0)
|
||||
+ coalesce((
|
||||
select sum(coalesce(debt.principal_balance, 0))
|
||||
from ccdi_debts_info debt
|
||||
where debt.person_id = scope.staff_id_card
|
||||
or (spouse.spouse_id_card is not null and debt.person_id = spouse.spouse_id_card)
|
||||
), 0) as comparison_amount
|
||||
), 0) as total_debt
|
||||
from (
|
||||
<include refid="projectEmployeeScopeSql"/>
|
||||
) scope
|
||||
@@ -806,4 +875,95 @@
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="selectIncreaseLendingPage" resultMap="IncreaseLendingListItemResultMap">
|
||||
select
|
||||
trim(loan.customer_manager_id) as staff_id,
|
||||
coalesce(staff.name, loan.customer_manager_name) as staff_name,
|
||||
staff.id_card as staff_id_card,
|
||||
dept.dept_name,
|
||||
loan.nfaacono as contract_no,
|
||||
loan.nfaabrno as lending_org_no,
|
||||
loan.borrower_name,
|
||||
loan.borrower_cert_no,
|
||||
loan.loan_product,
|
||||
loan.contract_amount,
|
||||
loan.loan_balance,
|
||||
date_format(loan.loan_start_date, '%Y-%m-%d') as loan_start_date,
|
||||
date_format(loan.loan_end_date, '%Y-%m-%d') as loan_end_date,
|
||||
loan.status,
|
||||
loan.five_classification,
|
||||
loan.customer_manager_id,
|
||||
loan.customer_manager_name,
|
||||
loan.approver
|
||||
from ccdi_increase_lending loan
|
||||
left join ccdi_base_staff staff
|
||||
on trim(loan.customer_manager_id) = cast(staff.staff_id as char)
|
||||
left join sys_dept dept
|
||||
on dept.dept_id = staff.dept_id
|
||||
<where>
|
||||
<if test="query.staffId != null and query.staffId != ''">
|
||||
and trim(loan.customer_manager_id) = #{query.staffId}
|
||||
</if>
|
||||
<if test="query.staffIdCard != null and query.staffIdCard != ''">
|
||||
and staff.id_card = #{query.staffIdCard}
|
||||
</if>
|
||||
<if test="query.approver != null and query.approver != ''">
|
||||
and trim(loan.approver) = #{query.approver}
|
||||
</if>
|
||||
<if test="query.loanStartDate != null and query.loanStartDate != ''">
|
||||
and loan.loan_start_date >= #{query.loanStartDate}
|
||||
</if>
|
||||
<if test="query.loanEndDate != null and query.loanEndDate != ''">
|
||||
and loan.loan_start_date <= #{query.loanEndDate}
|
||||
</if>
|
||||
</where>
|
||||
order by loan.loan_start_date desc, loan.nfaacono desc
|
||||
</select>
|
||||
|
||||
<select id="selectIncreaseLendingExportList"
|
||||
resultType="com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel">
|
||||
select
|
||||
trim(loan.customer_manager_id) as staff_id,
|
||||
coalesce(staff.name, loan.customer_manager_name) as staff_name,
|
||||
staff.id_card as staff_id_card,
|
||||
dept.dept_name,
|
||||
loan.nfaacono as contract_no,
|
||||
loan.nfaabrno as lending_org_no,
|
||||
loan.borrower_name,
|
||||
loan.borrower_cert_no,
|
||||
loan.loan_product,
|
||||
loan.contract_amount,
|
||||
loan.loan_balance,
|
||||
date_format(loan.loan_start_date, '%Y-%m-%d') as loan_start_date,
|
||||
date_format(loan.loan_end_date, '%Y-%m-%d') as loan_end_date,
|
||||
loan.status,
|
||||
loan.five_classification,
|
||||
loan.customer_manager_id,
|
||||
loan.customer_manager_name,
|
||||
loan.approver
|
||||
from ccdi_increase_lending loan
|
||||
left join ccdi_base_staff staff
|
||||
on trim(loan.customer_manager_id) = cast(staff.staff_id as char)
|
||||
left join sys_dept dept
|
||||
on dept.dept_id = staff.dept_id
|
||||
<where>
|
||||
<if test="query.staffId != null and query.staffId != ''">
|
||||
and trim(loan.customer_manager_id) = #{query.staffId}
|
||||
</if>
|
||||
<if test="query.staffIdCard != null and query.staffIdCard != ''">
|
||||
and staff.id_card = #{query.staffIdCard}
|
||||
</if>
|
||||
<if test="query.approver != null and query.approver != ''">
|
||||
and trim(loan.approver) = #{query.approver}
|
||||
</if>
|
||||
<if test="query.loanStartDate != null and query.loanStartDate != ''">
|
||||
and loan.loan_start_date >= #{query.loanStartDate}
|
||||
</if>
|
||||
<if test="query.loanEndDate != null and query.loanEndDate != ''">
|
||||
and loan.loan_start_date <= #{query.loanEndDate}
|
||||
</if>
|
||||
</where>
|
||||
order by loan.loan_start_date desc, loan.nfaacono desc
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiBankStatementService;
|
||||
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -39,6 +40,9 @@ class CcdiBankStatementControllerTest {
|
||||
@Mock
|
||||
private ICcdiBankStatementService bankStatementService;
|
||||
|
||||
@Mock
|
||||
private CcdiProjectAccessService projectAccessService;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
@@ -80,12 +84,17 @@ class CcdiBankStatementControllerTest {
|
||||
void detail_shouldReturnAjaxResultSuccess() {
|
||||
CcdiBankStatementDetailVO detailVO = new CcdiBankStatementDetailVO();
|
||||
detailVO.setBankStatementId(1000L);
|
||||
when(bankStatementService.getStatementDetail(1000L)).thenReturn(detailVO);
|
||||
when(bankStatementService.getStatementDetail(
|
||||
1000L, "SUSPICIOUS_GAMBLING", "MODEL_RULE"
|
||||
)).thenReturn(detailVO);
|
||||
|
||||
AjaxResult result = controller.getDetail(1000L);
|
||||
AjaxResult result = controller.getDetail(1000L, "SUSPICIOUS_GAMBLING", "MODEL_RULE");
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals(detailVO, result.get("data"));
|
||||
verify(bankStatementService).getStatementDetail(
|
||||
1000L, "SUSPICIOUS_GAMBLING", "MODEL_RULE"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
package com.ruoyi.ccdi.project.controller;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiPullBankInfoSubmitDTO;
|
||||
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -31,6 +37,14 @@ class CcdiFileUploadControllerTest {
|
||||
@Mock
|
||||
private ICcdiFileUploadService fileUploadService;
|
||||
|
||||
@Mock
|
||||
private CcdiProjectAccessService projectAccessService;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseIdCardFile_shouldReturnAjaxResultSuccess() {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
@@ -46,38 +60,96 @@ class CcdiFileUploadControllerTest {
|
||||
assertEquals(200, result.get("code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchUpload_shouldDelegateFilenameValidationToService() {
|
||||
MultipartFile[] files = new MultipartFile[]{
|
||||
new MockMultipartFile(
|
||||
"files",
|
||||
"330101199001010011 - 流水.xl sx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"content".getBytes(StandardCharsets.UTF_8)
|
||||
)
|
||||
};
|
||||
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.batchUploadFiles(PROJECT_ID, files, "admin"))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.batchUpload(PROJECT_ID, files);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals("batch-1", result.get("data"));
|
||||
verify(projectAccessService).assertCanOperate(PROJECT_ID);
|
||||
verify(fileUploadService).batchUploadFiles(PROJECT_ID, files, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pullBankInfo_shouldUseCurrentLoginUserInfo() {
|
||||
CcdiPullBankInfoSubmitDTO dto = new CcdiPullBankInfoSubmitDTO();
|
||||
dto.setProjectId(PROJECT_ID);
|
||||
dto.setIdCards(List.of("110101199001018888"));
|
||||
dto.setDataChannelCode("ZJRCU");
|
||||
dto.setStartDate("2026-03-01");
|
||||
dto.setEndDate("2026-03-10");
|
||||
|
||||
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
|
||||
mocked.when(SecurityUtils::getUserId).thenReturn(9527L);
|
||||
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "2026-03-01", "2026-03-10", 9527L, "admin"))
|
||||
.thenReturn("batch-1");
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "ZJRCU", "2026-03-01", "2026-03-10", 9527L, "admin"))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.pullBankInfo(dto);
|
||||
AjaxResult result = controller.pullBankInfo(dto);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
}
|
||||
assertEquals(200, result.get("code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pullBankInfo_shouldAllowJzlWithoutDateRange() {
|
||||
CcdiPullBankInfoSubmitDTO dto = new CcdiPullBankInfoSubmitDTO();
|
||||
dto.setProjectId(PROJECT_ID);
|
||||
dto.setIdCards(List.of("110101199001018888"));
|
||||
dto.setDataChannelCode("JZL");
|
||||
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "JZL", null, null, 9527L, "admin"))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.pullBankInfo(dto);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pullBankInfo_shouldRejectUnsupportedDataChannelCode() {
|
||||
CcdiPullBankInfoSubmitDTO dto = new CcdiPullBankInfoSubmitDTO();
|
||||
dto.setProjectId(PROJECT_ID);
|
||||
dto.setIdCards(List.of("110101199001018888"));
|
||||
dto.setDataChannelCode("OTHER");
|
||||
|
||||
AjaxResult result = controller.pullBankInfo(dto);
|
||||
|
||||
assertEquals(500, result.get("code"));
|
||||
assertEquals("流水来源不支持", result.get("msg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteFile_shouldUseCurrentLoginUserId() {
|
||||
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
|
||||
mocked.when(SecurityUtils::getUserId).thenReturn(9527L);
|
||||
when(fileUploadService.deleteFileUploadRecord(123L, 9527L))
|
||||
.thenReturn("删除成功");
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.deleteFileUploadRecord(123L, 9527L))
|
||||
.thenReturn("删除成功");
|
||||
|
||||
AjaxResult result = controller.deleteFile(123L);
|
||||
AjaxResult result = controller.deleteFile(123L);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals("删除成功", result.get("msg"));
|
||||
verify(fileUploadService).deleteFileUploadRecord(123L, 9527L);
|
||||
}
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals("删除成功", result.get("msg"));
|
||||
verify(fileUploadService).deleteFileUploadRecord(123L, 9527L);
|
||||
}
|
||||
|
||||
private void setLoginUser(Long userId, String username) {
|
||||
SysUser user = new SysUser();
|
||||
user.setUserName(username);
|
||||
LoginUser loginUser = new LoginUser(user, Set.of("*:*:*"));
|
||||
loginUser.setUserId(userId);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ class CcdiProjectOverviewControllerContractTest {
|
||||
Method method = controllerClass.getMethod(
|
||||
"exportRiskDetails",
|
||||
HttpServletResponse.class,
|
||||
Long.class
|
||||
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO")
|
||||
);
|
||||
PostMapping postMapping = method.getAnnotation(PostMapping.class);
|
||||
Operation operation = method.getAnnotation(Operation.class);
|
||||
@@ -198,7 +198,7 @@ class CcdiProjectOverviewControllerContractTest {
|
||||
assertEquals("/report/export", requestMapping.value()[0]);
|
||||
assertEquals(List.of(RequestMethod.GET, RequestMethod.POST), Arrays.asList(requestMapping.method()));
|
||||
assertNotNull(operation);
|
||||
assertEquals("一键导出结果总览报告", operation.summary());
|
||||
assertEquals("导出结果总览报告", operation.summary());
|
||||
assertNotNull(preAuthorize);
|
||||
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
|
||||
}
|
||||
@@ -211,7 +211,10 @@ class CcdiProjectOverviewControllerContractTest {
|
||||
.map(Field::getName)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertEquals(List.of("projectId", "suspiciousType", "pageNum", "pageSize"), fieldNames);
|
||||
assertEquals(
|
||||
List.of("projectId", "modelCode", "suspiciousType", "pageNum", "pageSize", "includeExternalPerson"),
|
||||
fieldNames
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,6 +18,7 @@ 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.service.ICcdiProjectOverviewService;
|
||||
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -52,6 +53,9 @@ class CcdiProjectOverviewControllerTest {
|
||||
@Mock
|
||||
private ICcdiProjectOverviewService overviewService;
|
||||
|
||||
@Mock
|
||||
private CcdiProjectAccessService projectAccessService;
|
||||
|
||||
@Test
|
||||
void shouldExposeDashboardEndpoint() throws Exception {
|
||||
when(overviewService.getDashboard(40L)).thenReturn(new CcdiProjectOverviewDashboardVO());
|
||||
@@ -308,15 +312,20 @@ class CcdiProjectOverviewControllerTest {
|
||||
@Test
|
||||
void shouldExposeRiskDetailsExportEndpoint() throws Exception {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO =
|
||||
new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||
queryDTO.setProjectId(40L);
|
||||
queryDTO.setModelCode("SUSPICIOUS_GAMBLING");
|
||||
queryDTO.setSuspiciousType("MODEL_RULE");
|
||||
|
||||
controller.exportRiskDetails(response, 40L);
|
||||
controller.exportRiskDetails(response, queryDTO);
|
||||
|
||||
verify(overviewService).exportRiskDetails(same(response), same(40L));
|
||||
verify(overviewService).exportRiskDetails(same(response), same(queryDTO));
|
||||
|
||||
Method method = CcdiProjectOverviewController.class.getMethod(
|
||||
"exportRiskDetails",
|
||||
jakarta.servlet.http.HttpServletResponse.class,
|
||||
Long.class
|
||||
CcdiProjectSuspiciousTransactionQueryDTO.class
|
||||
);
|
||||
PostMapping postMapping = method.getAnnotation(PostMapping.class);
|
||||
Operation operation = method.getAnnotation(Operation.class);
|
||||
|
||||
@@ -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(), "付款金额应该匹配");
|
||||
|
||||
@@ -72,6 +72,26 @@ class CcdiBankTagAnalysisMapperXmlTest {
|
||||
assertTrue(xml.contains("AS logId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dormantAccountLargeActivation_shouldRequireCompleteObservationWindow() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String selectSql = extractSelectSql(xml, "selectDormantAccountLargeActivationObjects");
|
||||
|
||||
assertTrue(selectSql.contains("project_window.projectStartDate"), selectSql);
|
||||
assertTrue(selectSql.contains("project_window.projectStartDate is not null"), selectSql);
|
||||
assertTrue(selectSql.contains("project_window.projectStartDate <= DATE_SUB(min(tx.txDate), INTERVAL 6 MONTH)"), selectSql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void suddenAccountClosure_shouldRequireLargeTransactionInClosureWindow() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
String selectSql = extractSelectSql(xml, "selectSuddenAccountClosureObjects");
|
||||
|
||||
assertTrue(selectSql.contains("tx.txDate >= DATE_SUB(ai.invalid_date, INTERVAL 30 DAY)"), selectSql);
|
||||
assertTrue(selectSql.contains("where t.windowTotalAmount >= 500000"), selectSql);
|
||||
assertTrue(selectSql.contains("or t.windowMaxSingleAmount >= 100000"), selectSql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void houseOrCarExpenseRule_shouldJoinBankStatementAndReturnStatementHitFields() throws Exception {
|
||||
String xml = readXml(RESOURCE);
|
||||
@@ -152,6 +172,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);
|
||||
|
||||
@@ -32,6 +32,7 @@ class CcdiBankTagResultMapperXmlTest {
|
||||
.orElse(null);
|
||||
|
||||
assertNotNull(method, "应提供按项目和流水ID批量查询异常标签的方法");
|
||||
assertTrue(method.getParameterCount() == 4, "标签批量查询应接收模型和预警类型范围");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -42,6 +43,9 @@ class CcdiBankTagResultMapperXmlTest {
|
||||
assertTrue(xml.contains("selectStatementTagsByProjectAndStatementIds"), xml);
|
||||
assertTrue(xml.contains("bank_statement_id IN"), xml);
|
||||
assertTrue(xml.contains("project_id = #{projectId}"), xml);
|
||||
assertTrue(xml.contains("model_code = #{modelCode}"), xml);
|
||||
assertTrue(xml.contains("left(model_code, 9) != 'EXTERNAL_'"), xml);
|
||||
assertTrue(xml.contains("left(model_code, 9) = 'EXTERNAL_'"), xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiBankTagTaskMapperXmlTest {
|
||||
|
||||
private static final String RESOURCE = "mapper/ccdi/project/CcdiBankTagTaskMapper.xml";
|
||||
|
||||
@Test
|
||||
void mapper_shouldExposeLatestFailedTaskQuery() {
|
||||
Method method = Arrays.stream(CcdiBankTagTaskMapper.class.getDeclaredMethods())
|
||||
.filter(item -> "selectLatestFailedTaskByProjectId".equals(item.getName()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
assertNotNull(method, "应提供查询项目最近失败打标任务的方法");
|
||||
}
|
||||
|
||||
@Test
|
||||
void xml_shouldSelectLatestFailedTaskByProjectId() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(xml.contains("selectLatestFailedTaskByProjectId"), xml);
|
||||
assertTrue(xml.contains("from ccdi_bank_tag_task"), xml);
|
||||
assertTrue(xml.contains("project_id = #{projectId}"), xml);
|
||||
assertTrue(xml.contains("status = 'FAILED'"), xml);
|
||||
assertTrue(xml.contains("order by id desc"), xml);
|
||||
assertTrue(xml.contains("limit 1"), xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiProjectExtendedPurchaseSupplierContractTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeExtendedPurchaseSupplierDetailQuery() throws Exception {
|
||||
Class<?> mapperClass = Class.forName("com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper");
|
||||
|
||||
Method method = mapperClass.getMethod("selectExtendedPurchaseSuppliers", Long.class, String.class);
|
||||
assertEquals(List.class, method.getReturnType());
|
||||
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml"));
|
||||
assertTrue(xml.contains("select id=\"selectExtendedPurchaseSuppliers\""));
|
||||
assertTrue(xml.contains("ccdi_purchase_transaction_supplier"));
|
||||
assertTrue(xml.contains("is_bid_winner"));
|
||||
assertTrue(xml.contains("sort_order"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiProjectMapperXmlTest {
|
||||
|
||||
@Test
|
||||
void projectQueriesShouldApplyProjectVisibilityAndDeletedFilters() throws Exception {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml"));
|
||||
|
||||
assertFalse(xml.contains("params.dataScope"), xml);
|
||||
assertFalse(xml.contains("${queryDTO.params.dataScope}"), xml);
|
||||
|
||||
String listSql = extractSelect(xml, "selectProjectPage");
|
||||
assertTrue(listSql.contains("LEFT JOIN sys_user u ON p.create_by = u.user_name"), listSql);
|
||||
assertTrue(listSql.contains("scope != null and !scope.viewAllProjects"), listSql);
|
||||
assertTrue(listSql.contains("p.create_by = #{scope.username}"), listSql);
|
||||
assertTrue(listSql.contains("queryDTO.includeDeleted"), listSql);
|
||||
assertTrue(listSql.contains("p.del_flag = '2'"), listSql);
|
||||
assertTrue(listSql.contains("p.status = '5'"), listSql);
|
||||
assertTrue(listSql.contains("p.del_flag = '0'"), listSql);
|
||||
assertTrue(listSql.contains("p.status != '5'"), listSql);
|
||||
|
||||
String historySql = extractSelect(xml, "selectHistoryProjects");
|
||||
assertTrue(historySql.contains("p.status in ('1', '2')"), historySql);
|
||||
assertTrue(historySql.contains("p.del_flag = '0'"), historySql);
|
||||
|
||||
assertTrue(xml.contains("<update id=\"markProjectDeleted\">"), xml);
|
||||
assertTrue(xml.contains("status = '5'"), xml);
|
||||
assertTrue(xml.contains("del_flag = '2'"), xml);
|
||||
assertTrue(xml.contains("<update id=\"restoreDeletedProject\">"), xml);
|
||||
assertTrue(xml.contains("status = '1'"), xml);
|
||||
assertTrue(xml.contains("del_flag = '0'"), xml);
|
||||
assertTrue(xml.contains("is_archived = 0"), xml);
|
||||
}
|
||||
|
||||
private String extractSelect(String xml, String selectId) {
|
||||
String start = "<select id=\"" + selectId + "\"";
|
||||
int startIndex = xml.indexOf(start);
|
||||
assertTrue(startIndex >= 0, "missing select: " + selectId);
|
||||
int endIndex = xml.indexOf("</select>", startIndex);
|
||||
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
|
||||
return xml.substring(startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
@@ -91,12 +91,19 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
|
||||
String suspiciousSql = extractSelect(xml, "selectSuspiciousTransactionPage");
|
||||
String modelHitSql = extractSqlFragment(xml, "suspiciousTransactionModelHitSql");
|
||||
String modelScopeSql = extractSqlFragment(xml, "suspiciousTransactionModelScopeSql");
|
||||
String mergedSql = extractSqlFragment(xml, "suspiciousTransactionMergedSql");
|
||||
String aggregatedSql = extractSqlFragment(xml, "suspiciousTransactionAggregatedSql");
|
||||
String externalSubjectExistsSql = extractSelect(xml, "selectExternalPersonSubjectExistsByProjectId");
|
||||
|
||||
assertTrue(modelHitSql.contains("from ccdi_bank_statement_tag_result tr"), modelHitSql);
|
||||
assertTrue(modelHitSql.contains("tr.bank_statement_id is not null"), modelHitSql);
|
||||
assertFalse(modelHitSql.contains("rule_name like '%可疑%'"), modelHitSql);
|
||||
assertFalse(modelHitSql.contains("ABNORMAL_CUSTOMER_TRANSACTION"), modelHitSql);
|
||||
assertTrue(modelHitSql.contains("suspiciousTransactionModelScopeSql"), modelHitSql);
|
||||
assertTrue(modelScopeSql.contains("tr.model_code = #{query.modelCode}"), modelScopeSql);
|
||||
assertTrue(modelScopeSql.contains("left(tr.model_code, 9) != 'EXTERNAL_'"), modelScopeSql);
|
||||
assertTrue(modelScopeSql.contains("left(tr.model_code, 9) = 'EXTERNAL_'"), modelScopeSql);
|
||||
assertTrue(suspiciousSql.contains("ccdi_biz_intermediary"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("ccdi_enterprise_base_info"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("group by merged.bankStatementId"), suspiciousSql);
|
||||
@@ -104,12 +111,21 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("hasNameListHit"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("final_result.nameListHitType"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("bs.LE_ACCOUNT_NO as leAccountNo"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("bs.CUSTOMER_ACCOUNT_NO as customerAccountNo"), suspiciousSql);
|
||||
assertTrue(suspiciousSql.contains("bs.bank_statement_id = final_result.bankStatementId"), suspiciousSql);
|
||||
assertTrue(mergedSql.contains("<if test=\"query.includeExternalPerson == true\">"), mergedSql);
|
||||
assertTrue(mergedSql.contains("<include refid=\"externalSuspiciousTransactionSql\"/>"), mergedSql);
|
||||
assertTrue(externalSubjectExistsSql.contains("limit 1"), externalSubjectExistsSql);
|
||||
assertTrue(externalSubjectExistsSql.contains("staff.id_card is null"), externalSubjectExistsSql);
|
||||
assertTrue(externalSubjectExistsSql.contains("relation.relation_cert_no is null"), externalSubjectExistsSql);
|
||||
|
||||
String reportSuspiciousSql = extractSelect(xml, "selectReportSuspiciousTransactionList");
|
||||
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '中介'"), reportSuspiciousSql);
|
||||
assertTrue(reportSuspiciousSql.contains("疑似与中介往来"), reportSuspiciousSql);
|
||||
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '信贷客户'"), reportSuspiciousSql);
|
||||
assertTrue(reportSuspiciousSql.contains("与信贷客户之间非正常资金往来"), reportSuspiciousSql);
|
||||
assertTrue(reportSuspiciousSql.contains("suspiciousTransactionModelScopeSql"), reportSuspiciousSql);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -195,6 +211,8 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
assertTrue(abnormalPageSql.contains("tr.bank_statement_id is null"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("account.owner_type = 'EMPLOYEE'"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("tr.reason_detail"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("abnormal.reasonDetail"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("abnormal.involved_amount"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("instr(tr.reason_detail, account.account_no) > 0"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("when account.status = 1 then '正常'"), abnormalPageSql);
|
||||
assertTrue(abnormalPageSql.contains("when account.status = 2 then '已销户'"), abnormalPageSql);
|
||||
@@ -209,10 +227,15 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
assertTrue(abnormalExportSql.contains("tr.bank_statement_id is null"), abnormalExportSql);
|
||||
assertTrue(abnormalExportSql.contains("account.owner_type = 'EMPLOYEE'"), abnormalExportSql);
|
||||
assertTrue(abnormalExportSql.contains("tr.reason_detail"), abnormalExportSql);
|
||||
assertTrue(abnormalExportSql.contains("abnormal.reasonDetail"), abnormalExportSql);
|
||||
assertTrue(abnormalExportSql.contains("abnormal.involved_amount"), abnormalExportSql);
|
||||
assertTrue(
|
||||
abnormalExportSql.contains("order by abnormal_time desc, account.account_no asc, tr.rule_code asc"),
|
||||
abnormalExportSql
|
||||
);
|
||||
|
||||
assertTrue(xml.contains("tr.reason_detail as reasonDetail"), xml);
|
||||
assertTrue(xml.contains("as involved_amount"), xml);
|
||||
}
|
||||
|
||||
private String extractSelect(String xml, String selectId) {
|
||||
|
||||
@@ -129,7 +129,9 @@ class CcdiBankStatementServiceImplTest {
|
||||
CcdiBankStatementHitTagVO hitTag = new CcdiBankStatementHitTagVO();
|
||||
hitTag.setBankStatementId(51274L);
|
||||
hitTag.setRuleName("大额存现交易");
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(51274L)))
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
43L, List.of(51274L), null, null
|
||||
))
|
||||
.thenReturn(List.of(hitTag));
|
||||
|
||||
List<CcdiBankStatementExcel> result = service.selectStatementListForExport(queryDTO);
|
||||
@@ -152,7 +154,9 @@ class CcdiBankStatementServiceImplTest {
|
||||
hitTag.setBankStatementId(51274L);
|
||||
hitTag.setRuleCode("LARGE_CASH_DEPOSIT");
|
||||
hitTag.setRuleName("大额存现交易");
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(51274L)))
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
43L, List.of(51274L), null, null
|
||||
))
|
||||
.thenReturn(List.of(hitTag));
|
||||
|
||||
Page<CcdiBankStatementListVO> result = service.selectStatementPage(page, queryDTO);
|
||||
@@ -170,7 +174,9 @@ class CcdiBankStatementServiceImplTest {
|
||||
CcdiBankStatementHitTagVO hitTag = new CcdiBankStatementHitTagVO();
|
||||
hitTag.setBankStatementId(200L);
|
||||
hitTag.setRuleName("大额存现交易");
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(200L)))
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
43L, List.of(200L), null, null
|
||||
))
|
||||
.thenReturn(List.of(hitTag));
|
||||
|
||||
CcdiBankStatementDetailVO result = service.getStatementDetail(200L);
|
||||
@@ -179,4 +185,21 @@ class CcdiBankStatementServiceImplTest {
|
||||
assertEquals(1, result.getHitTags().size());
|
||||
assertEquals("大额存现交易", result.getHitTags().get(0).getRuleName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStatementDetail_shouldApplySuspiciousTransactionScope() {
|
||||
CcdiBankStatementDetailVO detailVO = new CcdiBankStatementDetailVO();
|
||||
detailVO.setBankStatementId(200L);
|
||||
detailVO.setProjectId(43L);
|
||||
when(bankStatementMapper.selectStatementDetailById(200L)).thenReturn(detailVO);
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
43L, List.of(200L), "SUSPICIOUS_GAMBLING", "MODEL_RULE"
|
||||
)).thenReturn(List.of());
|
||||
|
||||
service.getStatementDetail(200L, "suspicious_gambling", "model_rule");
|
||||
|
||||
verify(bankTagResultMapper).selectStatementTagsByProjectAndStatementIds(
|
||||
43L, List.of(200L), "SUSPICIOUS_GAMBLING", "MODEL_RULE"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import ch.qos.logback.core.read.ListAppender;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
|
||||
@@ -15,6 +16,8 @@ import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
|
||||
import com.ruoyi.lsfx.domain.response.CheckParseStatusResponse;
|
||||
import com.ruoyi.lsfx.domain.response.DeleteFilesResponse;
|
||||
@@ -143,6 +146,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
String batchId = service.submitPullBankInfo(
|
||||
PROJECT_ID,
|
||||
List.of("110101199001018888", "110101199001019999"),
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-03-01",
|
||||
"2026-03-10",
|
||||
9527L,
|
||||
@@ -170,6 +174,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
() -> service.submitPullBankInfo(
|
||||
PROJECT_ID,
|
||||
List.of("3301"),
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-01-01",
|
||||
"2026-01-31",
|
||||
1L,
|
||||
@@ -193,6 +198,88 @@ class CcdiFileUploadServiceImplTest {
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchUploadFiles_shouldNormalizeFilenameBeforeSavingRecordAndTempFile() throws Exception {
|
||||
setField("uploadPath", tempDir.toString());
|
||||
mockProjectWithLsfxProjectId();
|
||||
|
||||
AtomicReference<List<CcdiFileUploadRecord>> inserted = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
List<CcdiFileUploadRecord> records = invocation.getArgument(0);
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
records.get(i).setId((long) (i + 1));
|
||||
}
|
||||
inserted.set(new ArrayList<>(records));
|
||||
return records.size();
|
||||
}).when(recordMapper).insertBatch(any());
|
||||
|
||||
MultipartFile file = new MockMultipartFile(
|
||||
"files",
|
||||
"330101199001010011 - 流 水 .xl sx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"content".getBytes()
|
||||
);
|
||||
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
String batchId = service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester");
|
||||
|
||||
assertNotNull(batchId);
|
||||
assertNotNull(inserted.get());
|
||||
assertEquals("330101199001010011-流水.xlsx", inserted.get().get(0).getFileName());
|
||||
Path tempRoot = tempDir.resolve("temp");
|
||||
assertTrue(Files.exists(tempRoot));
|
||||
try (var paths = Files.list(tempRoot)) {
|
||||
assertTrue(paths.anyMatch(path ->
|
||||
path.getFileName().toString().endsWith("_330101199001010011-流水.xlsx")));
|
||||
}
|
||||
} finally {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchUploadFiles_shouldRejectFileNameWithoutIdCardBeforeSavingTempFile() throws Exception {
|
||||
setField("uploadPath", tempDir.toString());
|
||||
mockProjectWithLsfxProjectId();
|
||||
|
||||
MultipartFile file = new MockMultipartFile(
|
||||
"files",
|
||||
"普通流水.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"content".getBytes()
|
||||
);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("身份证"));
|
||||
assertFalse(Files.exists(tempDir.resolve("temp")));
|
||||
verify(recordMapper, never()).insertBatch(any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchUploadFiles_shouldRejectBlankNormalizedFilenameBeforeSavingTempFile() throws Exception {
|
||||
setField("uploadPath", tempDir.toString());
|
||||
mockProjectWithLsfxProjectId();
|
||||
|
||||
MultipartFile file = new MockMultipartFile(
|
||||
"files",
|
||||
" \u3000 ",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"content".getBytes()
|
||||
);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("文件名不能为空"));
|
||||
assertFalse(Files.exists(tempDir.resolve("temp")));
|
||||
verify(recordMapper, never()).insertBatch(any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitTasksAsync_shouldNotCreateLocalBatchLogFiles() throws Exception {
|
||||
setField("uploadPath", tempDir.toString());
|
||||
@@ -355,6 +442,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
LSFX_PROJECT_ID,
|
||||
record,
|
||||
"110101199001018888",
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-03-01",
|
||||
"2026-03-10"
|
||||
);
|
||||
@@ -367,6 +455,34 @@ class CcdiFileUploadServiceImplTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void processPullBankInfoAsync_shouldFetchJzlWithZeroDateRange() {
|
||||
when(lsfxClient.fetchInnerFlow(any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
|
||||
service.processPullBankInfoAsync(
|
||||
PROJECT_ID,
|
||||
LSFX_PROJECT_ID,
|
||||
record,
|
||||
"110101199001018888",
|
||||
LsfxConstants.DATA_CHANNEL_JZL,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
verify(lsfxClient).fetchInnerFlow(argThat((FetchInnerFlowRequest request) ->
|
||||
LsfxConstants.DATA_CHANNEL_JZL.equals(request.getDataChannelCode())
|
||||
&& Integer.valueOf(0).equals(request.getDataStartDateId())
|
||||
&& Integer.valueOf(0).equals(request.getDataEndDateId())
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldUploadToLsfxWithOriginalRecordFileName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("原始流水.xlsx")))
|
||||
@@ -388,6 +504,44 @@ class CcdiFileUploadServiceImplTest {
|
||||
), eq("原始流水.xlsx"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldBackfillMissingCretNoFromUploadFileName() throws IOException {
|
||||
AtomicReference<List<CcdiBankStatement>> insertedStatements = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
List<CcdiBankStatement> statements = invocation.getArgument(0);
|
||||
insertedStatements.set(new ArrayList<>(statements));
|
||||
return statements.size();
|
||||
}).when(bankStatementMapper).insertBatch(any());
|
||||
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(PROJECT_ID);
|
||||
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(1);
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("张三_330101199001010011_流水.xlsx")))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
GetBankStatementRequest request = invocation.getArgument(0);
|
||||
if (Integer.valueOf(1).equals(request.getPageSize())) {
|
||||
return buildBankStatementCountResponse(1);
|
||||
}
|
||||
return buildBankStatementResponseWithBlankCretNo();
|
||||
});
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("张三_330101199001010011_流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
|
||||
assertNotNull(insertedStatements.get());
|
||||
assertEquals(1, insertedStatements.get().size());
|
||||
assertEquals("330101199001010011", insertedStatements.get().get(0).getCretNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldKeepOriginalFileNameWhenStatusReturnsDifferentName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
@@ -439,7 +593,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted() {
|
||||
void deleteFileUploadRecord_shouldDeleteLocalBankStatementsAndMarkDeleted() {
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setProjectId(PROJECT_ID);
|
||||
record.setLsfxProjectId(LSFX_PROJECT_ID);
|
||||
@@ -449,7 +603,6 @@ class CcdiFileUploadServiceImplTest {
|
||||
project.setProjectId(PROJECT_ID);
|
||||
|
||||
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
|
||||
when(lsfxClient.deleteFiles(any())).thenReturn(buildDeleteFilesResponse());
|
||||
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(2);
|
||||
when(recordMapper.updateById(any(CcdiFileUploadRecord.class))).thenReturn(1);
|
||||
@@ -457,12 +610,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
|
||||
|
||||
assertEquals("删除成功,已开始项目重新打标", result);
|
||||
verify(lsfxClient).deleteFiles(argThat(request ->
|
||||
request.getGroupId().equals(LSFX_PROJECT_ID)
|
||||
&& request.getUserId().equals(9527)
|
||||
&& request.getLogIds().length == 1
|
||||
&& request.getLogIds()[0].equals(LOG_ID)
|
||||
));
|
||||
verify(lsfxClient, never()).deleteFiles(any());
|
||||
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
|
||||
verify(recordMapper).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
RECORD_ID.equals(item.getId()) && "deleted".equals(item.getFileStatus())
|
||||
@@ -500,21 +648,23 @@ class CcdiFileUploadServiceImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails() {
|
||||
void deleteFileUploadRecord_shouldSkipLsfxDeleteApi() {
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileStatus("parsed_success");
|
||||
record.setLogId(LOG_ID);
|
||||
record.setLsfxProjectId(LSFX_PROJECT_ID);
|
||||
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
|
||||
when(lsfxClient.deleteFiles(any())).thenThrow(new RuntimeException("lsfx delete failed"));
|
||||
when(recordMapper.updateById(any(CcdiFileUploadRecord.class))).thenReturn(1);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> service.deleteFileUploadRecord(RECORD_ID, 9527L));
|
||||
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
|
||||
|
||||
verify(bankStatementMapper, never()).deleteByProjectIdAndBatchId(any(), any());
|
||||
verify(bankTagService, never()).submitAutoRebuild(any(), any());
|
||||
verify(recordMapper, never()).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
assertEquals("删除成功,已开始项目重新打标", result);
|
||||
verify(lsfxClient, never()).deleteFiles(any());
|
||||
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
|
||||
verify(recordMapper).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
"deleted".equals(item.getFileStatus())
|
||||
));
|
||||
verify(bankTagService).submitAutoRebuild(PROJECT_ID, TriggerType.AUTO_FILE_DELETE);
|
||||
}
|
||||
|
||||
// @Test
|
||||
@@ -639,7 +789,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
"fetchAndSaveBankStatements",
|
||||
PROJECT_ID,
|
||||
LSFX_PROJECT_ID,
|
||||
LOG_ID
|
||||
LOG_ID,
|
||||
null
|
||||
);
|
||||
|
||||
assertTrue(Boolean.TRUE.equals(ReflectionTestUtils.getField(result, "success")));
|
||||
@@ -754,6 +905,13 @@ class CcdiFileUploadServiceImplTest {
|
||||
}).when(recordMapper).updateById(any(CcdiFileUploadRecord.class));
|
||||
}
|
||||
|
||||
private void mockProjectWithLsfxProjectId() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(PROJECT_ID);
|
||||
project.setLsfxProjectId(LSFX_PROJECT_ID);
|
||||
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||
}
|
||||
|
||||
private CcdiFileUploadRecord buildRecord() {
|
||||
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
|
||||
record.setId(RECORD_ID);
|
||||
@@ -844,6 +1002,27 @@ class CcdiFileUploadServiceImplTest {
|
||||
return response;
|
||||
}
|
||||
|
||||
private GetBankStatementResponse buildBankStatementCountResponse(int totalCount) {
|
||||
GetBankStatementResponse.BankStatementData data = new GetBankStatementResponse.BankStatementData();
|
||||
data.setTotalCount(totalCount);
|
||||
|
||||
GetBankStatementResponse response = new GetBankStatementResponse();
|
||||
response.setData(data);
|
||||
return response;
|
||||
}
|
||||
|
||||
private GetBankStatementResponse buildBankStatementResponseWithBlankCretNo() {
|
||||
GetBankStatementResponse.BankStatementItem item = new GetBankStatementResponse.BankStatementItem();
|
||||
item.setBankStatementId(1L);
|
||||
item.setLeName("测试主体");
|
||||
item.setAccountNo("62220001");
|
||||
item.setCustomerName("交易对手");
|
||||
item.setCustomerAccountNo("62220002");
|
||||
item.setDrAmount(BigDecimal.TEN);
|
||||
item.setCretNo(null);
|
||||
return buildBankStatementResponseWithItems(1, List.of(item));
|
||||
}
|
||||
|
||||
private void invokeSubmitTasksAsync(List<String> tempFilePaths,
|
||||
List<CcdiFileUploadRecord> records,
|
||||
String batchId) throws Exception {
|
||||
@@ -872,9 +1051,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"));
|
||||
|
||||
@@ -163,6 +163,9 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
||||
|
||||
private String resolveTestFontPath() {
|
||||
List<String> candidates = List.of(
|
||||
"C:/Windows/Fonts/msyh.ttc",
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
"C:/Windows/Fonts/simsun.ttc",
|
||||
"/System/Library/Fonts/STHeiti Medium.ttc",
|
||||
"/System/Library/Fonts/STHeiti Light.ttc",
|
||||
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
||||
@@ -325,6 +328,8 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
||||
row.setAbnormalType("突然销户");
|
||||
row.setAbnormalTime("2026-03-20");
|
||||
row.setStatus("已销户");
|
||||
row.setReasonDetail("账户6222000000000003销户前存在异常交易");
|
||||
row.setInvolvedAmount(new BigDecimal("120000.00"));
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -65,6 +66,8 @@ class CcdiProjectOverviewServiceAbnormalAccountTest {
|
||||
item.setAbnormalType("突然销户");
|
||||
item.setAbnormalTime("2026-03-20");
|
||||
item.setStatus("已销户");
|
||||
item.setReasonDetail("账户6222000000000001销户前存在异常交易");
|
||||
item.setInvolvedAmount(new BigDecimal("120000.00"));
|
||||
|
||||
Page<CcdiProjectAbnormalAccountItemVO> resultPage = new Page<>(1, 5);
|
||||
resultPage.setRecords(List.of(item));
|
||||
@@ -78,6 +81,8 @@ class CcdiProjectOverviewServiceAbnormalAccountTest {
|
||||
assertEquals(1L, result.getTotal());
|
||||
assertEquals("6222000000000001", result.getRows().getFirst().getAccountNo());
|
||||
assertEquals("突然销户", result.getRows().getFirst().getAbnormalType());
|
||||
assertEquals("账户6222000000000001销户前存在异常交易", result.getRows().getFirst().getReasonDetail());
|
||||
assertEquals(new BigDecimal("120000.00"), result.getRows().getFirst().getInvolvedAmount());
|
||||
verify(overviewMapper).selectAbnormalAccountPage(
|
||||
argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L),
|
||||
argThat(query -> query.getProjectId().equals(40L))
|
||||
@@ -115,6 +120,8 @@ class CcdiProjectOverviewServiceAbnormalAccountTest {
|
||||
item.setAbnormalType("休眠账户大额启用");
|
||||
item.setAbnormalTime("2025-08-01");
|
||||
item.setStatus("正常");
|
||||
item.setReasonDetail("账户6222000000000002休眠后大额启用");
|
||||
item.setInvolvedAmount(new BigDecimal("500000.00"));
|
||||
when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(item));
|
||||
|
||||
List<CcdiProjectAbnormalAccountExcel> rows = service.exportAbnormalAccountPeople(40L);
|
||||
@@ -126,6 +133,8 @@ class CcdiProjectOverviewServiceAbnormalAccountTest {
|
||||
assertEquals("休眠账户大额启用", rows.getFirst().getAbnormalType());
|
||||
assertEquals("2025-08-01", rows.getFirst().getAbnormalTime());
|
||||
assertEquals("正常", rows.getFirst().getStatus());
|
||||
assertEquals("账户6222000000000002休眠后大额启用", rows.getFirst().getReasonDetail());
|
||||
assertEquals(new BigDecimal("500000.00"), rows.getFirst().getInvolvedAmount());
|
||||
verify(overviewMapper).selectAbnormalAccountList(40L);
|
||||
}
|
||||
|
||||
|
||||
@@ -295,6 +295,8 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
abnormalItem.setAbnormalType("突然销户");
|
||||
abnormalItem.setAbnormalTime("2026-03-20");
|
||||
abnormalItem.setStatus("已销户");
|
||||
abnormalItem.setReasonDetail("账户6222000000000001销户前存在异常交易");
|
||||
abnormalItem.setInvolvedAmount(new BigDecimal("120000.00"));
|
||||
when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(abnormalItem));
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
@@ -316,7 +318,10 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
rows.size() == 1 && "李四".equals(rows.getFirst().getPersonName())
|
||||
),
|
||||
argThat((List<CcdiProjectAbnormalAccountExcel> rows) ->
|
||||
rows.size() == 1 && "6222000000000001".equals(rows.getFirst().getAccountNo())
|
||||
rows.size() == 1
|
||||
&& "6222000000000001".equals(rows.getFirst().getAccountNo())
|
||||
&& "账户6222000000000001销户前存在异常交易".equals(rows.getFirst().getReasonDetail())
|
||||
&& new BigDecimal("120000.00").equals(rows.getFirst().getInvolvedAmount())
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -387,7 +392,9 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
hitTag.setRuleCode("RULE_A");
|
||||
hitTag.setRuleName("大额转账");
|
||||
hitTag.setRiskLevel("HIGH");
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(40L, List.of(1L)))
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
40L, List.of(1L), null, null
|
||||
))
|
||||
.thenReturn(List.of(hitTag));
|
||||
|
||||
CcdiProjectPersonAnalysisObjectRecordVO objectRow = new CcdiProjectPersonAnalysisObjectRecordVO();
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
|
||||
@@ -26,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -93,6 +95,135 @@ class CcdiProjectOverviewServiceSuspiciousTransactionTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncludeExternalBranchWhenExternalSubjectExistsForAllSuspiciousTransactions() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(40L);
|
||||
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||
when(overviewMapper.selectExternalPersonSubjectExistsByProjectId(40L)).thenReturn(1);
|
||||
|
||||
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(1, 10);
|
||||
page.setRecords(List.of());
|
||||
page.setTotal(0);
|
||||
when(overviewMapper.selectSuspiciousTransactionPage(any(Page.class), any(CcdiProjectSuspiciousTransactionQueryDTO.class)))
|
||||
.thenReturn(page);
|
||||
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||
queryDTO.setProjectId(40L);
|
||||
|
||||
CcdiProjectSuspiciousTransactionPageVO result = service.getSuspiciousTransactions(queryDTO);
|
||||
|
||||
assertEquals(0L, result.getTotal());
|
||||
verify(overviewMapper).selectSuspiciousTransactionPage(
|
||||
any(Page.class),
|
||||
argThat(query -> "ALL".equals(query.getSuspiciousType())
|
||||
&& Boolean.TRUE.equals(query.getIncludeExternalPerson()))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipExternalBranchAndAttachSelectedEmployeeModelTags() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(40L);
|
||||
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||
|
||||
CcdiProjectSuspiciousTransactionItemVO item = new CcdiProjectSuspiciousTransactionItemVO();
|
||||
item.setBankStatementId(101L);
|
||||
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(1, 10);
|
||||
page.setRecords(List.of(item));
|
||||
page.setTotal(1);
|
||||
when(overviewMapper.selectSuspiciousTransactionPage(
|
||||
any(Page.class), any(CcdiProjectSuspiciousTransactionQueryDTO.class)
|
||||
)).thenReturn(page);
|
||||
|
||||
CcdiBankStatementHitTagVO tag = new CcdiBankStatementHitTagVO();
|
||||
tag.setBankStatementId(101L);
|
||||
tag.setModelCode("SUSPICIOUS_GAMBLING");
|
||||
tag.setRuleName("疑似赌博");
|
||||
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
|
||||
40L, List.of(101L), "SUSPICIOUS_GAMBLING", "ALL"
|
||||
)).thenReturn(List.of(tag));
|
||||
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||
queryDTO.setProjectId(40L);
|
||||
queryDTO.setModelCode("suspicious_gambling");
|
||||
queryDTO.setSuspiciousType("all");
|
||||
|
||||
CcdiProjectSuspiciousTransactionPageVO result = service.getSuspiciousTransactions(queryDTO);
|
||||
|
||||
assertEquals("疑似赌博", result.getRows().getFirst().getHitTags().getFirst().getRuleName());
|
||||
verify(overviewMapper, never()).selectExternalPersonSubjectExistsByProjectId(40L);
|
||||
verify(overviewMapper).selectSuspiciousTransactionPage(
|
||||
any(Page.class),
|
||||
argThat(query -> !Boolean.TRUE.equals(query.getIncludeExternalPerson()))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepEmployeeRuleScopeSeparateFromExternalModels() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(40L);
|
||||
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||
|
||||
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(1, 10);
|
||||
page.setRecords(List.of());
|
||||
page.setTotal(0);
|
||||
when(overviewMapper.selectSuspiciousTransactionPage(
|
||||
any(Page.class), any(CcdiProjectSuspiciousTransactionQueryDTO.class)
|
||||
)).thenReturn(page);
|
||||
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||
queryDTO.setProjectId(40L);
|
||||
queryDTO.setSuspiciousType("MODEL_RULE");
|
||||
|
||||
service.getSuspiciousTransactions(queryDTO);
|
||||
|
||||
verify(overviewMapper, never()).selectExternalPersonSubjectExistsByProjectId(40L);
|
||||
verify(overviewMapper).selectSuspiciousTransactionPage(
|
||||
any(Page.class),
|
||||
argThat(query -> "MODEL_RULE".equals(query.getSuspiciousType())
|
||||
&& !Boolean.TRUE.equals(query.getIncludeExternalPerson()))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyExternalSuspiciousTransactionsWhenExternalSubjectDoesNotExist() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(40L);
|
||||
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||
when(overviewMapper.selectExternalPersonSubjectExistsByProjectId(40L)).thenReturn(null);
|
||||
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||
queryDTO.setProjectId(40L);
|
||||
queryDTO.setSuspiciousType("external_person");
|
||||
|
||||
CcdiProjectSuspiciousTransactionPageVO result = service.getSuspiciousTransactions(queryDTO);
|
||||
|
||||
assertEquals(0L, result.getTotal());
|
||||
assertTrue(result.getRows().isEmpty());
|
||||
verify(overviewMapper, never()).selectSuspiciousTransactionPage(
|
||||
any(Page.class),
|
||||
any(CcdiProjectSuspiciousTransactionQueryDTO.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyExternalSuspiciousTransactionExportWhenExternalSubjectDoesNotExist() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(40L);
|
||||
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||
when(overviewMapper.selectExternalPersonSubjectExistsByProjectId(40L)).thenReturn(null);
|
||||
|
||||
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||
queryDTO.setProjectId(40L);
|
||||
queryDTO.setSuspiciousType("EXTERNAL_PERSON");
|
||||
|
||||
List<CcdiProjectSuspiciousTransactionExcel> rows = service.exportSuspiciousTransactions(queryDTO);
|
||||
|
||||
assertTrue(rows.isEmpty());
|
||||
verify(overviewMapper, never()).selectReportSuspiciousTransactionList(any(CcdiProjectSuspiciousTransactionQueryDTO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExportSuspiciousTransactionsWithCurrentFilter() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
|
||||
@@ -47,6 +47,8 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
|
||||
abnormalRow.setAbnormalType("突然销户");
|
||||
abnormalRow.setAbnormalTime("2026-03-20");
|
||||
abnormalRow.setStatus("已销户");
|
||||
abnormalRow.setReasonDetail("账户6222000000000001销户前存在异常交易");
|
||||
abnormalRow.setInvolvedAmount(new BigDecimal("120000.00"));
|
||||
|
||||
exporter.export(response, 40L, List.of(suspiciousRow), List.of(creditRow), List.of(abnormalRow));
|
||||
|
||||
@@ -78,12 +80,16 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
|
||||
assertEquals("异常类型", workbook.getSheetAt(2).getRow(0).getCell(3).getStringCellValue());
|
||||
assertEquals("异常发生时间", workbook.getSheetAt(2).getRow(0).getCell(4).getStringCellValue());
|
||||
assertEquals("状态", workbook.getSheetAt(2).getRow(0).getCell(5).getStringCellValue());
|
||||
assertEquals("命中原因", workbook.getSheetAt(2).getRow(0).getCell(6).getStringCellValue());
|
||||
assertEquals("涉及金额", workbook.getSheetAt(2).getRow(0).getCell(7).getStringCellValue());
|
||||
assertEquals("6222000000000001", workbook.getSheetAt(2).getRow(1).getCell(0).getStringCellValue());
|
||||
assertEquals("李四", workbook.getSheetAt(2).getRow(1).getCell(1).getStringCellValue());
|
||||
assertEquals("中国农业银行", workbook.getSheetAt(2).getRow(1).getCell(2).getStringCellValue());
|
||||
assertEquals("突然销户", workbook.getSheetAt(2).getRow(1).getCell(3).getStringCellValue());
|
||||
assertEquals("2026-03-20", workbook.getSheetAt(2).getRow(1).getCell(4).getStringCellValue());
|
||||
assertEquals("已销户", workbook.getSheetAt(2).getRow(1).getCell(5).getStringCellValue());
|
||||
assertEquals("账户6222000000000001销户前存在异常交易", workbook.getSheetAt(2).getRow(1).getCell(6).getStringCellValue());
|
||||
assertEquals(120000D, workbook.getSheetAt(2).getRow(1).getCell(7).getNumericCellValue());
|
||||
assertEquals(2, workbook.getSheetAt(2).getPhysicalNumberOfRows());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,11 @@ class CcdiBankTagRuleSqlMetadataTest {
|
||||
"2026-03-31-create-ccdi-account-info-and-abnormal-account-rules.sql");
|
||||
|
||||
assertAll(
|
||||
() -> assertTrue(migrationSql.contains("员工本人账户已销户,且销户日前30天内仍存在交易记录。"),
|
||||
() -> assertTrue(migrationSql.contains("员工本人账户已销户,且销户日前30天内存在大额资金流动。"),
|
||||
"SUDDEN_ACCOUNT_CLOSURE 应使用设计文档中的业务口径"),
|
||||
() -> assertTrue(migrationSql.contains("员工本人账户开户后长期未使用,首次启用后出现大额资金流动。"),
|
||||
"DORMANT_ACCOUNT_LARGE_ACTIVATION 应使用设计文档中的业务口径"),
|
||||
() -> assertTrue(migrationSql.contains("真实规则:识别员工本人账户销户前30天内仍有交易的员工对象"),
|
||||
() -> assertTrue(migrationSql.contains("真实规则:识别员工本人账户销户前30天内存在大额资金流动的员工对象"),
|
||||
"SUDDEN_ACCOUNT_CLOSURE 应同步真实规则说明"),
|
||||
() -> assertTrue(migrationSql.contains("真实规则:识别长期休眠后首次启用即出现大额资金流动的员工对象"),
|
||||
"DORMANT_ACCOUNT_LARGE_ACTIVATION 应同步真实规则说明")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
mock-maker-subclass
|
||||
211
docs/design/2026-07-09-project-risk-exclusion-design.md
Normal file
211
docs/design/2026-07-09-project-risk-exclusion-design.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# 项目结果总览排除可疑改造设计
|
||||
|
||||
## 1. 背景
|
||||
|
||||
项目详情的结果总览当前直接消费银行流水打标结果与员工结果快照。用户在核查过程中如果确认某条预警不是可疑问题,只能在认知上忽略,系统没有持久化排除能力。刷新页面、重新进入项目或导出报告时,该预警仍会按有效命中展示。
|
||||
|
||||
仓库初始化 SQL 中已存在 `ccdi_project_risk_exclusion` 表,语义是“项目结果页排除可疑记录表”,字段能够承载项目、人员或流水、规则编码、排除类型与排除原因。本次改造使用该表完成排除记录持久化,不删除原始打标结果。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
1. 用户可在项目分析详情的异常明细中对单条预警执行“排除可疑”。
|
||||
2. 排除对象精确到单条规则命中,不影响同一人员或同一流水的其他规则。
|
||||
3. 排除后当前页面立即刷新,风险人员、模型预警次数、命中人数、涉疑交易明细按有效命中重新展示。
|
||||
4. 浏览器刷新或重新进入项目后,已排除预警仍不作为有效预警展示。
|
||||
5. 本阶段不做“恢复预警”入口,保持最短闭环。
|
||||
|
||||
## 3. 非目标
|
||||
|
||||
1. 不删除 `ccdi_bank_statement_tag_result` 原始命中结果。
|
||||
2. 不使用 `localStorage`、`sessionStorage` 或 Cookie 保存排除状态。
|
||||
3. 不做批量排除、恢复排除、排除记录管理页。
|
||||
4. 不改变打标规则执行逻辑;重新打标后仍生成原始命中,展示查询阶段通过排除表过滤。
|
||||
|
||||
## 4. 用户操作链路
|
||||
|
||||
### 4.1 流水明细型预警
|
||||
|
||||
示例:某笔流水同时命中“大额转账交易”和“疑似敏感交易”。
|
||||
|
||||
1. 用户进入项目详情 > 结果总览。
|
||||
2. 点击风险人员或模型命中人员的“查看项目”。
|
||||
3. 在项目分析详情中进入“异常明细”。
|
||||
4. 在“流水异常明细”表格中,异常标签旁展示小号操作“排除可疑”。
|
||||
5. 用户点击某个标签的“排除可疑”,填写排除原因并确认。
|
||||
6. 系统只排除该笔流水上的该条规则标签。
|
||||
|
||||
排除键:
|
||||
|
||||
```text
|
||||
project_id + bank_statement_id + rule_code + exclusion_type = STATEMENT
|
||||
```
|
||||
|
||||
效果:
|
||||
|
||||
1. 被排除的标签不再显示。
|
||||
2. 同一笔流水的其他标签继续显示。
|
||||
3. 如果该笔流水没有剩余有效标签,则不再出现在异常流水明细中。
|
||||
4. 相关模型预警次数减少。
|
||||
|
||||
### 4.2 对象型 / 人员型预警
|
||||
|
||||
示例:某人命中“年流水交易额超限”,该预警是按人员聚合生成,不对应单笔流水。
|
||||
|
||||
1. 用户进入项目详情 > 结果总览。
|
||||
2. 点击该人员的“查看项目”。
|
||||
3. 在项目分析详情中进入“异常明细”。
|
||||
4. 在“对象异常明细”卡片右上角展示小号操作“排除可疑”,样式与“加入证据库”保持一致。
|
||||
5. 用户点击“排除可疑”,填写排除原因并确认。
|
||||
6. 系统只排除该人员命中的该条规则。
|
||||
|
||||
排除键:
|
||||
|
||||
```text
|
||||
project_id + staff_id_card + rule_code + exclusion_type = OBJECT
|
||||
```
|
||||
|
||||
效果:
|
||||
|
||||
1. 该对象型预警卡片不再显示。
|
||||
2. 该人员其他规则命中继续保留。
|
||||
3. 如果该人员没有剩余有效规则,则从风险人员列表中移除。
|
||||
4. 如果该人员仍有其他有效规则,则人员仍展示,但模型数、规则标签和风险等级按剩余有效命中重新计算。
|
||||
|
||||
## 5. 数据设计
|
||||
|
||||
使用既有表 `ccdi_project_risk_exclusion`。
|
||||
|
||||
关键字段:
|
||||
|
||||
| 字段 | 用途 |
|
||||
| --- | --- |
|
||||
| `project_id` | 项目 ID |
|
||||
| `staff_id_card` | 对象型预警对应人员证件号;外部人员也使用证件号 |
|
||||
| `rule_code` | 被排除规则编码 |
|
||||
| `exclusion_type` | `STATEMENT` 或 `OBJECT` |
|
||||
| `bank_statement_id` | 流水型预警对应流水 ID |
|
||||
| `exclude_reason` | 排除原因 |
|
||||
|
||||
唯一约束:
|
||||
|
||||
1. 流水型:`project_id + rule_code + exclusion_type + bank_statement_id`
|
||||
2. 对象型:`project_id + staff_id_card + rule_code + exclusion_type`
|
||||
|
||||
如果目标环境缺少该表,实施时补充增量 SQL,使用 `utf8mb4` 与 `utf8mb4_general_ci`。
|
||||
|
||||
## 6. 后端设计
|
||||
|
||||
### 6.1 新增接口
|
||||
|
||||
新增“排除可疑”接口:
|
||||
|
||||
```text
|
||||
POST /ccdi/project/overview/risk-exclusions
|
||||
```
|
||||
|
||||
请求字段:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `projectId` | 是 | 项目 ID |
|
||||
| `exclusionType` | 是 | `STATEMENT` 或 `OBJECT` |
|
||||
| `ruleCode` | 是 | 规则编码 |
|
||||
| `bankStatementId` | 流水型必填 | 流水 ID |
|
||||
| `staffIdCard` | 对象型必填 | 人员证件号 |
|
||||
| `excludeReason` | 是 | 排除原因 |
|
||||
|
||||
校验规则:
|
||||
|
||||
1. `exclusionType=STATEMENT` 时必须传 `bankStatementId`。
|
||||
2. `exclusionType=OBJECT` 时必须传 `staffIdCard`。
|
||||
3. `excludeReason` 不能为空,长度不超过 1000。
|
||||
4. 先校验用户对项目有读写权限;归档或只读项目不允许排除。
|
||||
5. 重复排除视为幂等成功,更新原因和更新时间。
|
||||
|
||||
### 6.2 查询过滤
|
||||
|
||||
所有结果总览相关查询应统一过滤排除表。
|
||||
|
||||
过滤规则:
|
||||
|
||||
1. 读取 `ccdi_bank_statement_tag_result` 时左关联排除表。
|
||||
2. 流水型命中用 `project_id + rule_code + bank_statement_id + STATEMENT` 匹配。
|
||||
3. 对象型命中用 `project_id + rule_code + staff_id_card/object_key + OBJECT` 匹配。
|
||||
4. 匹配到排除记录的命中不进入后续聚合、列表、标签组装、导出。
|
||||
|
||||
重点影响范围:
|
||||
|
||||
1. 风险人员列表。
|
||||
2. 风险模型卡片。
|
||||
3. 风险模型命中人员。
|
||||
4. 人员项目分析详情。
|
||||
5. 涉疑交易明细。
|
||||
6. 一键 PDF 报告与 Excel 导出。
|
||||
7. 外部人员预警及外部人员详情。
|
||||
|
||||
### 6.3 员工结果快照
|
||||
|
||||
当前风险人员列表读取 `ccdi_project_overview_employee_result` 快照。为了让“排除可疑”后统计即时变化,实施时应在排除成功后触发当前项目员工结果快照重算,或将列表查询切回有效命中实时聚合。
|
||||
|
||||
本次推荐:
|
||||
|
||||
1. 排除接口写入排除表后,同步触发当前项目结果总览员工快照重算。
|
||||
2. 重算逻辑只使用未排除命中。
|
||||
3. 页面刷新后读取更新后的快照。
|
||||
|
||||
理由:保留现有列表分页性能与页面结构,改动集中,不引入双口径。
|
||||
|
||||
## 7. 前端设计
|
||||
|
||||
### 7.1 操作入口
|
||||
|
||||
流水异常明细:
|
||||
|
||||
1. 异常标签展示为标签 + 小号文字按钮。
|
||||
2. 按钮文案为“排除可疑”。
|
||||
3. 按钮只作用于当前标签,不作用于整行。
|
||||
|
||||
对象异常明细:
|
||||
|
||||
1. 卡片右上角增加小号按钮“排除可疑”。
|
||||
2. 样式仿照“加入证据库”,保持轻量。
|
||||
3. 不新增大按钮、不改变卡片主视觉。
|
||||
|
||||
### 7.2 确认弹窗
|
||||
|
||||
使用 Element UI 弹窗或对话框,要求用户填写排除原因。
|
||||
|
||||
确认文案需要明确影响范围:
|
||||
|
||||
1. 流水型:仅排除当前流水的当前规则标签。
|
||||
2. 对象型:仅排除当前人员的当前规则预警。
|
||||
|
||||
### 7.3 刷新策略
|
||||
|
||||
排除成功后,前端通知父组件刷新:
|
||||
|
||||
1. 重新加载项目分析详情。
|
||||
2. 重新加载风险人员列表。
|
||||
3. 重新加载风险模型卡片与命中人员。
|
||||
4. 重新加载涉疑交易明细。
|
||||
|
||||
不做局部假刷新,避免页面统计与后端状态不一致。
|
||||
|
||||
## 8. 风险与约束
|
||||
|
||||
1. 如果只过滤详情、不刷新快照,风险人员列表和模型统计会不一致;必须统一刷新统计口径。
|
||||
2. 对象型预警没有 `bank_statement_id`,必须用人员证件号和规则编码定位。
|
||||
3. 外部人员对象型预警同样需要按证件号处理,不能只适配员工。
|
||||
4. 排除记录不删除原始命中,因此重新打标不会覆盖排除结果。
|
||||
|
||||
## 9. 验证场景
|
||||
|
||||
1. 流水一条多标签,排除其中一个标签后另一个标签仍显示。
|
||||
2. 排除流水标签后模型预警次数减少。
|
||||
3. 排除对象型“年流水交易额超限”后对象卡片消失。
|
||||
4. 人员仅剩一条对象型预警时,排除后人员不再出现在风险人员列表。
|
||||
5. 人员有多条预警时,排除一条后人员仍保留,标签和统计减少。
|
||||
6. 刷新浏览器后排除结果仍生效。
|
||||
7. 导出报告不包含已排除预警。
|
||||
8. 归档或只读项目不允许排除。
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 涉疑交易明细默认分页调整设计
|
||||
|
||||
## 目标
|
||||
|
||||
将项目结果预览中的“涉疑交易明细”默认每页条数由 5 条调整为 10 条,提升列表首屏信息量。
|
||||
|
||||
## 设计
|
||||
|
||||
- 仅调整涉疑交易明细默认分页条数。
|
||||
- 员工征信负面和异常账户关联人员继续默认每页 5 条。
|
||||
- 继续使用现有分页选项 `5 / 10 / 20 / 50`,不修改后端接口、数据库或用户手动切换分页条数的行为。
|
||||
- 项目切换时,涉疑交易明细恢复为每页 10 条。
|
||||
|
||||
## 验证
|
||||
|
||||
- 静态单元测试确认涉疑交易默认值为 10。
|
||||
- 静态单元测试确认另外两个列表仍使用默认值 5。
|
||||
- 执行前端生产构建,并在真实项目页面确认首次请求的 `pageSize` 为 10。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 上传流水文件名校验后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
上传流水文件时,后端统一过滤文件名中的空白字符,并要求过滤后的文件名主干包含 18 位身份证号片段。任一文件不满足规则时整批拦截,不创建上传记录、不保存临时文件、不调用流水分析平台。
|
||||
|
||||
## 实施内容
|
||||
|
||||
- `CcdiFileUploadController.batchUpload` 只保留项目 ID、文件数量、空文件和文件大小校验,文件名业务校验统一下沉到服务层。
|
||||
- `CcdiFileUploadServiceImpl.batchUploadFiles` 在保存临时文件前完成整批文件名归一化和校验。
|
||||
- 文件名归一化规则为:`originalFilename == null` 时按空字符串处理,再移除半角空白与全角空格。
|
||||
- 扩展名校验、身份证号片段校验、上传记录 `fileName` 和流水平台 multipart filename 均使用归一化后的文件名。
|
||||
- 身份证号片段校验使用 `(?<!\d)\d{17}[0-9Xx](?!\d)` 在文件名主干中查找,不做校验位算法。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 覆盖含空格文件名归一化后写入上传记录,并用于临时文件名。
|
||||
- 覆盖无身份证号片段、空白文件名在保存临时文件前拦截。
|
||||
- 覆盖 Controller 不再按原始文件名提前拦截,能把文件名业务规则交给 Service。
|
||||
- 执行 `mvn -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest,CcdiFileUploadControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`。
|
||||
- 执行 `mvn -pl ccdi-project -am -DskipTests compile`。
|
||||
|
||||
## 范围说明
|
||||
|
||||
- 本次不修改前端页面。
|
||||
- 本次不修改流水平台返回 DTO。
|
||||
- 本次不修改 `CcdiBankStatement.fromResponse` 或 `ccdi_bank_statement` 落库逻辑。
|
||||
@@ -0,0 +1,264 @@
|
||||
# 项目结果总览排除可疑后端实施计划
|
||||
|
||||
## 1. 目标
|
||||
|
||||
后端提供“排除可疑”持久化接口,并在结果总览所有预警查询、统计、导出链路中统一过滤已排除命中。排除粒度为单条规则命中,不删除原始打标结果。
|
||||
|
||||
## 2. 涉及范围
|
||||
|
||||
模块:
|
||||
|
||||
1. `ccdi-project`
|
||||
2. `ruoyi-admin` 装配依赖无需调整
|
||||
3. `sql/migration/`
|
||||
|
||||
重点文件:
|
||||
|
||||
1. `CcdiProjectOverviewController`
|
||||
2. `ICcdiProjectOverviewService`
|
||||
3. `CcdiProjectOverviewServiceImpl`
|
||||
4. `CcdiProjectOverviewMapper`
|
||||
5. `CcdiBankTagResultMapper`
|
||||
6. 新增排除记录实体、DTO、Mapper
|
||||
|
||||
## 3. 数据库实施
|
||||
|
||||
### 3.1 增量脚本
|
||||
|
||||
新增 SQL:
|
||||
|
||||
```text
|
||||
sql/migration/2026-07-09-create-project-risk-exclusion.sql
|
||||
```
|
||||
|
||||
内容使用 `CREATE TABLE IF NOT EXISTS ccdi_project_risk_exclusion`,字段与 `sql/ccdi_prod_init.sql` 保持一致,并显式声明:
|
||||
|
||||
```sql
|
||||
DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
```
|
||||
|
||||
### 3.2 表用途
|
||||
|
||||
`ccdi_project_risk_exclusion` 保存排除记录:
|
||||
|
||||
1. `STATEMENT`:单笔流水上的单个规则标签。
|
||||
2. `OBJECT`:某个对象或人员上的单个规则预警。
|
||||
|
||||
## 4. 后端对象
|
||||
|
||||
### 4.1 新增 Entity
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectRiskExclusion.java
|
||||
```
|
||||
|
||||
字段对应表结构,实体类使用 Lombok `@Data`,不继承 `BaseEntity`。
|
||||
|
||||
### 4.2 新增 DTO
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectRiskExclusionSaveDTO.java
|
||||
```
|
||||
|
||||
字段:
|
||||
|
||||
1. `projectId`
|
||||
2. `staffIdCard`
|
||||
3. `ruleCode`
|
||||
4. `exclusionType`
|
||||
5. `bankStatementId`
|
||||
6. `excludeReason`
|
||||
|
||||
校验:
|
||||
|
||||
1. `projectId` 必填。
|
||||
2. `ruleCode` 必填。
|
||||
3. `exclusionType` 必填且只允许 `STATEMENT`、`OBJECT`。
|
||||
4. `excludeReason` 必填且最大 1000 字符。
|
||||
5. `STATEMENT` 要求 `bankStatementId`。
|
||||
6. `OBJECT` 要求 `staffIdCard`。
|
||||
|
||||
### 4.3 新增 Mapper
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectRiskExclusionMapper.java
|
||||
ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectRiskExclusionMapper.xml
|
||||
```
|
||||
|
||||
方法:
|
||||
|
||||
1. `upsertExclusion`
|
||||
2. `selectByProjectId`
|
||||
3. `selectStatementExclusions`
|
||||
4. `selectObjectExclusions`
|
||||
|
||||
`upsertExclusion` 使用唯一键实现幂等写入;重复排除时更新 `exclude_reason/update_by/update_time`。
|
||||
|
||||
## 5. 接口实施
|
||||
|
||||
在 `CcdiProjectOverviewController` 新增:
|
||||
|
||||
```text
|
||||
POST /ccdi/project/overview/risk-exclusions
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```java
|
||||
AjaxResult.success("排除成功")
|
||||
```
|
||||
|
||||
权限:
|
||||
|
||||
1. `@PreAuthorize("@ss.hasPermi('ccdi:project:query')")`
|
||||
2. 使用 `projectAccessService.assertCanRead(projectId)` 校验项目访问。
|
||||
3. 使用项目状态或访问服务校验当前项目可操作;归档或只读项目返回错误。
|
||||
|
||||
## 6. Service 实施
|
||||
|
||||
在 `ICcdiProjectOverviewService` 增加:
|
||||
|
||||
```java
|
||||
void excludeRisk(CcdiProjectRiskExclusionSaveDTO dto);
|
||||
```
|
||||
|
||||
`CcdiProjectOverviewServiceImpl` 实现流程:
|
||||
|
||||
1. 校验 DTO。
|
||||
2. 校验规则是否存在于当前项目有效命中中,避免写入无效排除记录。
|
||||
3. 写入 `ccdi_project_risk_exclusion`。
|
||||
4. 触发当前项目结果总览员工快照重算。
|
||||
5. 返回成功。
|
||||
|
||||
## 7. 查询过滤实施
|
||||
|
||||
### 7.1 统一过滤 SQL
|
||||
|
||||
在 `CcdiProjectOverviewMapper.xml` 中新增复用 SQL 片段:
|
||||
|
||||
```xml
|
||||
not exists (
|
||||
select 1
|
||||
from ccdi_project_risk_exclusion ex
|
||||
where ex.project_id = tr.project_id
|
||||
and ex.rule_code = tr.rule_code
|
||||
and (
|
||||
(ex.exclusion_type = 'STATEMENT' and ex.bank_statement_id = tr.bank_statement_id)
|
||||
or
|
||||
(ex.exclusion_type = 'OBJECT' and ex.staff_id_card = resolved_staff_id_card)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
实际实现时根据不同查询上下文替换 `resolved_staff_id_card`。
|
||||
|
||||
### 7.2 员工风险基础 SQL
|
||||
|
||||
修改 `resolvedEmployeeRiskBaseSql`:
|
||||
|
||||
1. 先解析 `staff_id_card`。
|
||||
2. 对 `STATEMENT` 和 `OBJECT` 分别过滤。
|
||||
3. 被排除命中不进入员工风险聚合。
|
||||
|
||||
### 7.3 风险模型卡片
|
||||
|
||||
修改模型统计查询:
|
||||
|
||||
1. `warning_count` 按未排除命中统计。
|
||||
2. `people_count` 按未排除命中涉及人员去重。
|
||||
|
||||
### 7.4 风险模型命中人员
|
||||
|
||||
修改命中人员查询:
|
||||
|
||||
1. `hitTagList` 不包含已排除规则。
|
||||
2. `modelNames` 按剩余命中规则组装。
|
||||
3. 过滤后无有效规则的人员不返回。
|
||||
|
||||
### 7.5 人员项目分析详情
|
||||
|
||||
修改详情组装:
|
||||
|
||||
1. `BANK_STATEMENT` 记录的 `hitTags` 排除已排除标签。
|
||||
2. 如果一条流水没有剩余 `hitTags`,不进入流水异常明细。
|
||||
3. `OBJECT` 记录排除已排除对象规则。
|
||||
|
||||
### 7.6 涉疑交易明细与导出
|
||||
|
||||
修改涉疑交易查询:
|
||||
|
||||
1. 已排除流水标签不进入 `hitTags`。
|
||||
2. 过滤后无有效标签的流水不作为可疑流水展示。
|
||||
3. Excel 导出和 PDF 报告复用同一过滤口径。
|
||||
|
||||
### 7.7 外部人员预警
|
||||
|
||||
外部人员使用证件号作为对象键:
|
||||
|
||||
1. `OBJECT` 类型按 `staff_id_card = cert_no` 过滤。
|
||||
2. `STATEMENT` 类型按 `bank_statement_id` 过滤。
|
||||
3. 外部人员列表、模型卡片、详情、报告口径一致。
|
||||
|
||||
## 8. 快照重算
|
||||
|
||||
如果当前已有员工结果快照重算逻辑,排除成功后复用该逻辑。
|
||||
|
||||
若没有可复用入口,新增内部方法:
|
||||
|
||||
```java
|
||||
refreshOverviewEmployeeResult(Long projectId)
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
1. 使用过滤排除后的有效命中重算。
|
||||
2. 删除或更新当前项目 `ccdi_project_overview_employee_result`。
|
||||
3. 确保列表、模型统计和详情口径一致。
|
||||
|
||||
## 9. 测试计划
|
||||
|
||||
### 9.1 后端编译
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-project -am compile -DskipTests
|
||||
```
|
||||
|
||||
### 9.2 单接口验证
|
||||
|
||||
验证接口:
|
||||
|
||||
```text
|
||||
POST /ccdi/project/overview/risk-exclusions
|
||||
```
|
||||
|
||||
场景:
|
||||
|
||||
1. 缺少原因返回错误。
|
||||
2. `STATEMENT` 缺少 `bankStatementId` 返回错误。
|
||||
3. `OBJECT` 缺少 `staffIdCard` 返回错误。
|
||||
4. 重复排除幂等成功。
|
||||
5. 归档项目或只读项目不允许排除。
|
||||
|
||||
### 9.3 数据口径验证
|
||||
|
||||
场景:
|
||||
|
||||
1. 一笔流水多个标签,排除一个后另一个仍存在。
|
||||
2. 人员多个对象型预警,排除一个后人员仍存在。
|
||||
3. 人员唯一预警被排除后,风险人员列表不再展示该人员。
|
||||
4. 模型预警次数和命中人数减少。
|
||||
5. PDF/Excel 不输出已排除预警。
|
||||
|
||||
## 10. 风险控制
|
||||
|
||||
1. 排除表只影响展示和统计,不影响原始命中结果。
|
||||
2. 所有新增 SQL 使用 `utf8mb4_general_ci`。
|
||||
3. `rule_code` 保持全大写。
|
||||
4. 不引入恢复接口,避免扩大范围。
|
||||
|
||||
@@ -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,39 @@
|
||||
# 专项排查资产收入匹配口径后端实施计划
|
||||
|
||||
## 保存路径确认
|
||||
|
||||
本文档保存于 `docs/plans/backend/`,用于记录专项排查后端口径调整计划。
|
||||
|
||||
## 背景
|
||||
|
||||
专项排查家庭资产负债原逻辑将无负债记录判为缺少信息,且使用收入、负债、资产之间的旧比较方式,无法满足“无负债仍需输出资产收入匹配判断”的业务口径。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- 调整 `CcdiProjectSpecialCheckMapper.xml` 中家庭资产负债列表与详情查询。
|
||||
- 保留家庭总年收入、家庭总资产、家庭总负债等原字段。
|
||||
- 新增本人年收入、配偶年收入、本人入职年限、可解释收入、资产收入倍数等返回字段。
|
||||
- 风险等级统一为 `高关注`、`关注`、`正常`、`缺少信息`。
|
||||
|
||||
## 计算口径
|
||||
|
||||
- 本人年收入取 `ccdi_base_staff.annual_income`。
|
||||
- 本人入职年限由 `ccdi_base_staff.hire_date` 按当前日期折算,不足 1 年按 1 年。
|
||||
- 配偶年收入取配偶员工年收入或亲属关系年收入。
|
||||
- 家庭总资产取本人及配偶 `ccdi_asset_info.current_value` 合计。
|
||||
- 家庭总负债取本人及配偶 `ccdi_debts_info.principal_balance` 合计,无记录按 0。
|
||||
- 可解释收入 = 本人年收入 × 本人入职年限 + 配偶年收入 + 家庭总负债。
|
||||
- 资产收入倍数 = 家庭总资产 / 可解释收入。
|
||||
|
||||
## 风险规则
|
||||
|
||||
- 缺少信息:本人年收入为空或小于等于 0、入职时间为空、家庭总资产为空或小于等于 0。
|
||||
- 正常:资产收入倍数小于等于 1.5。
|
||||
- 关注:资产收入倍数大于 1.5 且小于等于 3。
|
||||
- 高关注:资产收入倍数大于 3。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 执行后端编译或针对 `ccdi-project` 模块的编译检查。
|
||||
- 检查 MyBatis XML 中新增字段映射与 SQL 别名一致。
|
||||
- 对典型数据进行公式复核:年收入 20 万、入职 10 年、资产 586 万、负债 0 时应为关注。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 涉疑交易明细筛选一致性后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
- 完成员工模型与外部人员模型隔离。
|
||||
- 列表接口批量返回当前筛选范围内的标签和展示字段。
|
||||
- 详情与风险明细导出的涉疑交易标签复用相同筛选口径。
|
||||
- 保持分页查询性能,不引入逐行 SQL。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 在涉疑交易查询标准化逻辑中识别员工模型与 `EXTERNAL_*` 外部模型,并按 `suspiciousType` 限制模型范围。
|
||||
2. 修改 `suspiciousTransactionModelHitSql`,使 `MODEL_RULE` 只读取员工模型、`EXTERNAL_PERSON` 只读取外部模型。
|
||||
3. 优化外部人员分支开关:仅 `ALL + 全部/外部模型` 或 `EXTERNAL_PERSON` 查询检查外部人员主体。
|
||||
4. 扩展涉疑交易行 VO 和分页 SQL,直接返回账户等列表展示字段。
|
||||
5. 扩展流水标签批量查询,支持 `modelCode` 和 `suspiciousType` 范围,并返回 `modelCode`。
|
||||
6. 涉疑交易分页完成后,对当前页流水 ID 执行一次标签批量查询并装配。
|
||||
7. 流水详情接口增加可选 `modelCode`、`suspiciousType` 参数,按相同标签范围查询。
|
||||
8. 风险明细导出接收涉疑交易筛选参数,涉疑交易工作表按当前筛选生成;其他工作表保持原逻辑。
|
||||
9. 修改涉疑交易导出标签聚合,使其按当前模型范围过滤。
|
||||
|
||||
## 测试
|
||||
|
||||
- Service:员工/外部模型范围、外部分支开关、当前页批量标签装配。
|
||||
- Mapper SQL:模型范围条件、账户字段、标签批量查询和导出标签条件。
|
||||
- Controller:详情与风险明细导出参数透传。
|
||||
- 性能结构:分页 Service 每次最多调用一次标签批量查询,不按行调用。
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 涉疑交易无外部人员短路后端实施计划
|
||||
|
||||
## 背景
|
||||
|
||||
生产结果总览首屏中,`/ccdi/project/overview/suspicious-transactions` 在无外部人员项目上可能超过前端 10 秒超时。该接口的汇总 SQL 无条件拼接外部人员预警分支,导致只上传员工数据的项目仍会执行外部人员复杂关联。
|
||||
|
||||
## 目标
|
||||
|
||||
- 保持涉疑交易原有口径不变。
|
||||
- 当项目不存在外部人员主体时,跳过涉疑交易中的外部人员预警分支。
|
||||
- 当查询类型为 `EXTERNAL_PERSON` 且项目不存在外部人员主体时,直接返回空结果。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 在 `CcdiProjectSuspiciousTransactionQueryDTO` 增加内部开关 `includeExternalPerson`。
|
||||
2. 在 `CcdiProjectOverviewMapper` 增加轻量外部人员主体存在性判断。
|
||||
3. 在 `CcdiProjectOverviewMapper.xml` 中将 `externalSuspiciousTransactionSql` 的 `union all` 改为按 `includeExternalPerson` 条件拼接。
|
||||
4. 在 `CcdiProjectOverviewServiceImpl` 中按 `suspiciousType` 设置外部人员分支开关;`EXTERNAL_PERSON` 无主体时直接返回空分页或空导出列表。
|
||||
5. 补充 Mapper SQL 结构测试和 Service 行为测试。
|
||||
|
||||
## 验证
|
||||
|
||||
- 运行涉疑交易相关单测。
|
||||
- 运行结果总览 Mapper SQL 结构测试。
|
||||
- 验证无外部人员项目不再执行涉疑交易外部人员分支。
|
||||
@@ -0,0 +1,24 @@
|
||||
# 结果总览异常账户口径调整后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
修正“休眠账户大额启用”在项目流水不完整时的误报,并在结果总览异常账户数据中透出命中原因和涉及金额。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- 调整 `DORMANT_ACCOUNT_LARGE_ACTIVATION` SQL:项目流水最早日期必须覆盖启用日前 6 个月观察窗口。
|
||||
- 调整 `SUDDEN_ACCOUNT_CLOSURE` SQL:销户前 30 天窗口内需存在大额资金流动,避免普通销户前交易被判定为异常账户线索。
|
||||
- 异常账户分页和导出查询增加 `reasonDetail`、`involvedAmount`。
|
||||
- 风险明细工作簿和 PDF 报告的异常账户表增加“命中原因”“涉及金额”。
|
||||
- 新增规则元数据增量 SQL,同步“突然销户”业务口径。
|
||||
- 更新后端单元测试断言。
|
||||
|
||||
## 不包含
|
||||
|
||||
- 不开发非实控账户规则。
|
||||
- 不新增“关联员工”列。
|
||||
- 不新增“销户后交易”校验类场景。
|
||||
|
||||
## 验证
|
||||
|
||||
- 执行异常账户规则、结果总览 Mapper、导出器相关测试。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 账户库账户号码筛选后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在账户库管理列表和导出查询中支持按账户号码筛选,方便业务按账号直接定位账户主档。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- `CcdiAccountInfoQueryDTO` 增加 `accountNo` 查询字段。
|
||||
- `CcdiAccountInfoMapper.xml` 的账户库查询条件增加 `ai.account_no LIKE` 过滤。
|
||||
- 页面列表和导出共用同一查询条件,保持筛选口径一致。
|
||||
- 补充 Mapper SQL 渲染测试,确认账号条件被拼入列表 SQL。
|
||||
|
||||
## 不包含
|
||||
|
||||
- 不调整账户主档字段结构。
|
||||
- 不修改账户导入模板。
|
||||
- 不混入项目风险结果或异常账户预警规则。
|
||||
|
||||
## 验证
|
||||
|
||||
- 执行账户库 Mapper 测试,确认 SQL 渲染包含账户号码过滤条件。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 流水明细导出项目可读权限后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
普通用户进入本人可读项目后,可以导出该项目的流水明细;不可读项目仍禁止导出。
|
||||
|
||||
## 实施范围
|
||||
|
||||
1. 调整 `CcdiBankStatementController.export`,移除 `ccdi:project:export` 菜单权限前置校验。
|
||||
2. 保留并依赖 `projectAccessService.assertCanRead(projectId)`,导出权限与流水列表、筛选项、详情的项目可读口径保持一致。
|
||||
3. 不调整项目列表导出、结果总览导出、专项核查导出等其他链路;本次仅修正流水明细导出遗留的旧权限标识。
|
||||
|
||||
## 验证
|
||||
|
||||
执行 `mvn -pl ccdi-project -am -Dtest=CcdiBankStatementControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`,确认流水明细 Controller 单测通过。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 上传流水身份证号本地回填后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
修复上传行外流水文件后,流水平台返回明细中 `cretNo` 为空导致 `ccdi_bank_statement.cret_no` 为空、结果总览和打标无法按身份证号关联的问题。
|
||||
|
||||
## 范围
|
||||
|
||||
- 仅调整后端批量上传流水文件链路。
|
||||
- 不调整拉取本行信息链路。
|
||||
- 不调整前端页面、数据库表结构、结果总览查询和打标 SQL。
|
||||
|
||||
## 实施方案
|
||||
|
||||
1. 复用现有文件名空白清理与身份证号识别规则,从归一化后的上传记录文件名中提取 18 位身份证号。
|
||||
2. 保持转传流水平台的文件名不变,继续使用当前上传记录文件名。
|
||||
3. 保存平台返回的流水明细时,若 `CcdiBankStatement.cretNo` 为空,则使用当前上传记录文件名中提取到的身份证号回填。
|
||||
4. 补充单元测试覆盖平台 `cretNo` 为空时本地回填。
|
||||
|
||||
## 验证
|
||||
|
||||
- 运行 `mvn -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`。
|
||||
- 必要时运行 `mvn -pl ccdi-project -am -DskipTests compile`。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 拉取行内/金综流水后端实施计划
|
||||
|
||||
## 背景
|
||||
|
||||
项目详情页“上传数据”中的拉取流水能力需要支持两种来源:行内流水 `ZJRCU` 与金综流水 `JZL`。流水平台仍使用原 `/getJZFileOrZjrcuFile` 接口,一次请求只传一种 `dataChannelCode`。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- `CcdiPullBankInfoSubmitDTO` 增加 `dataChannelCode`。
|
||||
- `CcdiFileUploadController#pullBankInfo` 校验流水来源,仅允许 `ZJRCU`、`JZL`。
|
||||
- `ICcdiFileUploadService#submitPullBankInfo` 与实现类透传来源。
|
||||
- `CcdiFileUploadServiceImpl#processPullBankInfoAsync` 按来源组装流水平台请求:
|
||||
- `ZJRCU`:使用页面传入的开始、结束日期。
|
||||
- `JZL`:`dataStartDateId=0`、`dataEndDateId=0`。
|
||||
- `LsfxConstants` 增加 `DATA_CHANNEL_JZL` 常量。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 运行 `CcdiFileUploadControllerTest`,验证 Controller 透传来源与金综不要求日期。
|
||||
- 运行 `CcdiFileUploadServiceImplTest`,验证金综请求传 `JZL` 与 `0/0` 日期参数。
|
||||
@@ -0,0 +1,293 @@
|
||||
# 项目结果总览排除可疑前端实施计划
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在项目分析详情的异常明细中提供轻量“排除可疑”操作。用户确认排除后,页面重新请求后端数据,展示过滤后的风险人员、模型预警次数、命中人数与异常明细。
|
||||
|
||||
本阶段不做恢复入口,不做排除记录列表。
|
||||
|
||||
## 2. 涉及范围
|
||||
|
||||
前端模块:
|
||||
|
||||
```text
|
||||
ruoyi-ui
|
||||
```
|
||||
|
||||
重点文件:
|
||||
|
||||
1. `ruoyi-ui/src/api/ccdi/projectOverview.js`
|
||||
2. `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue`
|
||||
3. `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
|
||||
4. `ruoyi-ui/src/views/ccdiProject/components/detail/ExternalPersonDetailDialog.vue`
|
||||
5. `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
|
||||
6. `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue`
|
||||
7. `ruoyi-ui/src/views/ccdiProject/components/detail/RiskModelSection.vue`
|
||||
8. `ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`
|
||||
|
||||
## 3. API 封装
|
||||
|
||||
在 `projectOverview.js` 新增:
|
||||
|
||||
```js
|
||||
export function excludeOverviewRisk(data) {
|
||||
return request({
|
||||
url: '/ccdi/project/overview/risk-exclusions',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
请求字段:
|
||||
|
||||
```js
|
||||
{
|
||||
projectId,
|
||||
exclusionType,
|
||||
ruleCode,
|
||||
bankStatementId,
|
||||
staffIdCard,
|
||||
excludeReason
|
||||
}
|
||||
```
|
||||
|
||||
## 4. UI 设计
|
||||
|
||||
### 4.1 流水异常明细
|
||||
|
||||
位置:
|
||||
|
||||
```text
|
||||
ProjectAnalysisAbnormalTab.vue > BANK_STATEMENT 表格 > 异常标签列
|
||||
```
|
||||
|
||||
展示方式:
|
||||
|
||||
1. 每个异常标签保持 `el-tag`。
|
||||
2. 标签右侧增加小号文字按钮“排除可疑”。
|
||||
3. 按钮与标签在同一行内,不增加大操作列。
|
||||
|
||||
交互:
|
||||
|
||||
1. 点击“排除可疑”。
|
||||
2. 打开确认弹窗。
|
||||
3. 用户填写排除原因。
|
||||
4. 确认后调用后端接口。
|
||||
|
||||
提交参数:
|
||||
|
||||
```js
|
||||
{
|
||||
projectId,
|
||||
exclusionType: 'STATEMENT',
|
||||
ruleCode: tag.ruleCode,
|
||||
bankStatementId: row.bankStatementId,
|
||||
staffIdCard: resolvePersonIdCard(),
|
||||
excludeReason
|
||||
}
|
||||
```
|
||||
|
||||
其中 `staffIdCard` 仅作为上下文传递,后端流水型以 `bankStatementId` 为主。
|
||||
|
||||
### 4.2 对象异常明细
|
||||
|
||||
位置:
|
||||
|
||||
```text
|
||||
ProjectAnalysisAbnormalTab.vue > OBJECT 卡片右上角
|
||||
```
|
||||
|
||||
展示方式:
|
||||
|
||||
1. 卡片右上角增加小号按钮“排除可疑”。
|
||||
2. 样式仿照现有“加入证据库”按钮。
|
||||
3. 不新增恢复按钮。
|
||||
|
||||
提交参数:
|
||||
|
||||
```js
|
||||
{
|
||||
projectId,
|
||||
exclusionType: 'OBJECT',
|
||||
ruleCode: item.ruleCode || item.modelCode,
|
||||
staffIdCard: resolvePersonIdCard(),
|
||||
excludeReason
|
||||
}
|
||||
```
|
||||
|
||||
实施时需要确保对象异常记录保留真实 `ruleCode`。如果当前对象卡片只带 `modelCode`,前后端需补齐 `ruleCode` 字段,不能用 `modelCode` 替代规则编码。
|
||||
|
||||
## 5. 确认弹窗
|
||||
|
||||
使用 Element UI 对话框或 `$prompt`。
|
||||
|
||||
推荐文案:
|
||||
|
||||
流水型:
|
||||
|
||||
```text
|
||||
确认将当前流水的“{规则名称}”标记为排除可疑吗?
|
||||
该操作只影响当前流水上的这一个规则标签。
|
||||
```
|
||||
|
||||
对象型:
|
||||
|
||||
```text
|
||||
确认将“{人员姓名}”的“{规则名称}”标记为排除可疑吗?
|
||||
该操作只影响当前人员的这一个规则预警。
|
||||
```
|
||||
|
||||
输入框:
|
||||
|
||||
```text
|
||||
请输入排除原因
|
||||
```
|
||||
|
||||
校验:
|
||||
|
||||
1. 原因不能为空。
|
||||
2. 原因长度不超过 1000。
|
||||
|
||||
## 6. 刷新策略
|
||||
|
||||
排除成功后不做前端本地假删除,统一通知父组件刷新。
|
||||
|
||||
事件链路:
|
||||
|
||||
1. `ProjectAnalysisAbnormalTab` 调用接口成功。
|
||||
2. 向上 emit `risk-excluded`。
|
||||
3. `ProjectAnalysisDialog` 接收后重新加载当前人员详情,并继续向上 emit。
|
||||
4. `PreliminaryCheck` 接收后重新加载结果总览数据。
|
||||
5. 子组件因 props 更新重新加载风险人员、模型卡片、命中人员与涉疑交易。
|
||||
|
||||
外部人员详情:
|
||||
|
||||
1. `ExternalPersonDetailDialog` 接收 `risk-excluded` 后重新加载外部人员流水。
|
||||
2. 同时通知 `PreliminaryCheck` 刷新总览数据。
|
||||
|
||||
需要刷新的数据:
|
||||
|
||||
1. 风险人员列表。
|
||||
2. 外部人员预警列表。
|
||||
3. 风险模型卡片。
|
||||
4. 风险模型命中人员。
|
||||
5. 涉疑交易明细。
|
||||
6. 当前弹窗异常明细。
|
||||
|
||||
## 7. 页面状态
|
||||
|
||||
### 7.1 操作中
|
||||
|
||||
点击确认后按钮进入 loading 或禁用状态,避免重复提交。
|
||||
|
||||
### 7.2 成功
|
||||
|
||||
提示:
|
||||
|
||||
```text
|
||||
排除成功
|
||||
```
|
||||
|
||||
随后刷新页面数据。
|
||||
|
||||
### 7.3 失败
|
||||
|
||||
提示后端错误信息:
|
||||
|
||||
```text
|
||||
排除失败,请稍后重试
|
||||
```
|
||||
|
||||
不修改当前页面数据。
|
||||
|
||||
### 7.4 只读项目
|
||||
|
||||
如果 `canOperate=false`,不展示“排除可疑”按钮,或展示禁用态并提示当前项目仅可查看。
|
||||
|
||||
## 8. 数据要求
|
||||
|
||||
前端需要从后端获取或透传以下字段:
|
||||
|
||||
流水标签:
|
||||
|
||||
1. `ruleCode`
|
||||
2. `ruleName`
|
||||
3. `bankStatementId`
|
||||
|
||||
对象卡片:
|
||||
|
||||
1. `ruleCode`
|
||||
2. `ruleName`
|
||||
3. `modelCode`
|
||||
4. `modelName`
|
||||
5. `reasonDetail`
|
||||
|
||||
人员上下文:
|
||||
|
||||
1. `projectId`
|
||||
2. `idNo` 或 `staffIdCard`
|
||||
3. `name` 或 `staffName`
|
||||
|
||||
如果对象异常卡片缺少 `ruleCode`,应先补齐数据映射,再展示排除按钮。
|
||||
|
||||
## 9. 测试计划
|
||||
|
||||
### 9.1 Node 环境
|
||||
|
||||
前端命令执行前按仓库规则:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
nvm use
|
||||
node -v
|
||||
npm -v
|
||||
where node
|
||||
where npm
|
||||
```
|
||||
|
||||
如果 Node 14.21.3 缺少 `npm.cmd`,切换:
|
||||
|
||||
```bash
|
||||
nvm use 22.22.3
|
||||
```
|
||||
|
||||
并在实施记录中说明实际版本。
|
||||
|
||||
### 9.2 构建验证
|
||||
|
||||
```bash
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
### 9.3 浏览器验证
|
||||
|
||||
完成页面开发后,使用 `browser-use` 打开真实业务页面验证,不打开 prototype。
|
||||
|
||||
验证路径:
|
||||
|
||||
1. 登录系统。
|
||||
2. 进入项目详情。
|
||||
3. 打开结果总览。
|
||||
4. 打开某个风险人员项目分析详情。
|
||||
5. 对流水标签执行“排除可疑”。
|
||||
6. 确认弹窗填写原因。
|
||||
7. 验证异常明细、模型统计、风险人员数据刷新。
|
||||
8. 刷新浏览器,验证排除仍生效。
|
||||
9. 对对象型“年流水交易额超限”执行同样验证。
|
||||
|
||||
### 9.4 关键用例
|
||||
|
||||
1. 单笔流水多标签,只排除其中一个标签。
|
||||
2. 对象型单预警人员,排除后人员从风险列表消失。
|
||||
3. 对象型多预警人员,排除后人员保留但标签和次数减少。
|
||||
4. 只读项目不显示或禁用按钮。
|
||||
5. 排除原因为空时不能提交。
|
||||
|
||||
## 10. 不做事项
|
||||
|
||||
1. 不做恢复入口。
|
||||
2. 不做排除记录管理页。
|
||||
3. 不使用浏览器本地存储。
|
||||
4. 不新增批量排除。
|
||||
|
||||
@@ -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,42 @@
|
||||
# 专项排查资产收入匹配口径前端实施计划
|
||||
|
||||
## 保存路径确认
|
||||
|
||||
本文档保存于 `docs/plans/frontend/`,用于记录专项排查前端展示调整计划。
|
||||
|
||||
## 背景
|
||||
|
||||
专项排查家庭资产负债详情需要展示新的资产收入匹配结论,并将家庭总年收入改为万元展示,便于业务人员阅读。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- 调整 `FamilyAssetLiabilitySection.vue` 列表中的家庭总年收入展示为万元。
|
||||
- 调整 `FamilyAssetLiabilityDetail.vue` 总收入、本人收入、配偶收入展示为万元。
|
||||
- 将详查结果结论改为分行展示。
|
||||
- 结论显示风险等级、家庭总资产、可解释收入公式和资产收入倍数。
|
||||
|
||||
## 展示口径
|
||||
|
||||
详查结果结论示例:
|
||||
|
||||
```text
|
||||
关注
|
||||
|
||||
家庭总资产:586.00 万元
|
||||
可解释收入:本人年收入 20.00 万元 × 入职年限 10 年 + 配偶年收入 0.00 万元 + 家庭总负债 0.00 万元 = 200.00 万元
|
||||
资产收入倍数:2.93
|
||||
```
|
||||
|
||||
缺少信息时仅显示:
|
||||
|
||||
```text
|
||||
缺少资产,暂无法判断。
|
||||
```
|
||||
|
||||
缺少项会按实际情况展示为收入、资产或入职时间;多项同时缺失时展示“关键信息不完整,暂无法判断。”。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 执行前端构建或静态编译检查。
|
||||
- 页面验证专项排查列表家庭总年收入为万元。
|
||||
- 页面验证详查结果按多行展示,且长公式不溢出容器。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 涉疑交易明细默认分页调整前端实施计划
|
||||
|
||||
## 修改范围
|
||||
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
|
||||
- `ruoyi-ui/tests/unit/risk-detail-suspicious-transaction-layout.test.js`
|
||||
- `ruoyi-ui/tests/unit/preliminary-check-suspicious-transaction-load.test.js`
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 将共用默认分页常量拆分为涉疑交易默认值和其他风险列表默认值。
|
||||
2. 结果总览首次加载涉疑交易时请求 10 条。
|
||||
3. 涉疑交易初始化及项目切换时使用 10 条默认值。
|
||||
4. 员工征信负面和异常账户关联人员继续使用 5 条默认值。
|
||||
5. 更新定向测试,验证分页默认值和影响范围。
|
||||
6. 执行前端生产构建和真实页面验证。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 涉疑交易明细首次加载请求携带 `pageSize=10`。
|
||||
- 其他两个风险列表默认分页行为不变。
|
||||
- 分页选项保持 `5 / 10 / 20 / 50`。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user