Compare commits
18 Commits
dev-ui
...
9a60371a8f
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a60371a8f | |||
| 380f9b4e7a | |||
| 928f65dfca | |||
| c64146ac40 | |||
| 0541ce0ac6 | |||
| 26c639134e | |||
| 0f7b57e824 | |||
| 104e8697fe | |||
| bbc6a2050b | |||
| bf7a4c0538 | |||
| b2e177dd24 | |||
| 2071d04c08 | |||
| 4988ab5944 | |||
| c00d5475e6 | |||
| 0b64532959 | |||
| 9f0ad4ce87 | |||
| 75b5989774 | |||
| d8c069a836 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -84,6 +84,8 @@ logs/
|
||||
|
||||
ruoyi-ui/vue.config.js
|
||||
|
||||
ruoyi-ui/dist.zip
|
||||
|
||||
*/src/test/
|
||||
|
||||
.pytest_cache/
|
||||
|
||||
@@ -2,19 +2,36 @@ package com.ruoyi.info.collection.utils;
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.ExcelWriter;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
|
||||
import com.alibaba.excel.write.metadata.WriteSheet;
|
||||
import com.alibaba.excel.write.handler.WriteHandler;
|
||||
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
||||
import com.ruoyi.common.annotation.DictDropdown;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.info.collection.handler.DictDropdownWriteHandler;
|
||||
import com.ruoyi.info.collection.handler.RequiredFieldWriteHandler;
|
||||
import com.ruoyi.info.collection.handler.TextFormatWriteHandler;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataValidation;
|
||||
import org.apache.poi.ss.usermodel.DataValidationConstraint;
|
||||
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.usermodel.WorkbookFactory;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -77,8 +94,10 @@ public class EasyExcelUtil {
|
||||
* @return 数据列表
|
||||
*/
|
||||
public static <T> List<T> importExcel(String fileName, Class<T> clazz) {
|
||||
try {
|
||||
return EasyExcel.read(fileName).head(clazz).sheet().doReadSync();
|
||||
try (InputStream inputStream = java.nio.file.Files.newInputStream(java.nio.file.Path.of(fileName))) {
|
||||
return importExcel(inputStream, clazz);
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("导入Excel失败", e);
|
||||
}
|
||||
@@ -94,7 +113,11 @@ public class EasyExcelUtil {
|
||||
*/
|
||||
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz) {
|
||||
try {
|
||||
return EasyExcel.read(inputStream).head(clazz).sheet().doReadSync();
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
validateDictDropdownTemplate(bytes, clazz, null);
|
||||
return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet().doReadSync();
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("导入Excel失败", e);
|
||||
}
|
||||
@@ -111,7 +134,11 @@ public class EasyExcelUtil {
|
||||
*/
|
||||
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz, String sheetName) {
|
||||
try {
|
||||
return EasyExcel.read(inputStream).head(clazz).sheet(sheetName).doReadSync();
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
validateDictDropdownTemplate(bytes, clazz, sheetName);
|
||||
return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet(sheetName).doReadSync();
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("导入Excel失败", e);
|
||||
}
|
||||
@@ -128,7 +155,7 @@ public class EasyExcelUtil {
|
||||
public static <T> void importTemplateExcel(HttpServletResponse response, Class<T> clazz, String sheetName) {
|
||||
try {
|
||||
setResponseHeader(response, sheetName + "模板");
|
||||
EasyExcel.write(response.getOutputStream(), clazz)
|
||||
templateWriter(response, clazz)
|
||||
.sheet(sheetName)
|
||||
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
||||
.doWrite(List.of());
|
||||
@@ -151,7 +178,7 @@ public class EasyExcelUtil {
|
||||
WriteHandler... handlers) {
|
||||
try {
|
||||
setResponseHeader(response, sheetName + "模板");
|
||||
var writerBuilder = EasyExcel.write(response.getOutputStream(), clazz)
|
||||
var writerBuilder = templateWriter(response, clazz)
|
||||
.sheet(sheetName)
|
||||
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());
|
||||
// 注册所有自定义处理器
|
||||
@@ -190,7 +217,7 @@ public class EasyExcelUtil {
|
||||
public static <T> void importTemplateWithDictDropdown(HttpServletResponse response, Class<T> clazz, String sheetName) {
|
||||
try {
|
||||
setResponseHeader(response, sheetName + "模板");
|
||||
EasyExcel.write(response.getOutputStream(), clazz)
|
||||
templateWriter(response, clazz)
|
||||
.sheet(sheetName)
|
||||
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
||||
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
|
||||
@@ -217,7 +244,7 @@ public class EasyExcelUtil {
|
||||
String sheetName, String fileName) {
|
||||
try {
|
||||
setResponseHeader(response, fileName);
|
||||
EasyExcel.write(response.getOutputStream(), clazz)
|
||||
templateWriter(response, clazz)
|
||||
.sheet(sheetName)
|
||||
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
||||
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
|
||||
@@ -250,7 +277,7 @@ public class EasyExcelUtil {
|
||||
String fileName
|
||||
) {
|
||||
setResponseHeader(response, fileName);
|
||||
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream()).build()) {
|
||||
try (ExcelWriter writer = templateWriter(response).build()) {
|
||||
writer.write(List.of(), buildTemplateSheet(0, firstClazz, firstSheetName));
|
||||
writer.write(List.of(), buildTemplateSheet(1, secondClazz, secondSheetName));
|
||||
} catch (IOException e) {
|
||||
@@ -322,4 +349,137 @@ public class EasyExcelUtil {
|
||||
throw new RuntimeException("导出带字典下拉框的Excel失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateDictDropdownTemplate(byte[] bytes, Class<?> clazz, String sheetName) {
|
||||
List<DropdownColumn> dropdownColumns = resolveDropdownColumns(clazz);
|
||||
if (dropdownColumns.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes))) {
|
||||
Sheet sheet = sheetName == null ? workbook.getSheetAt(0) : workbook.getSheet(sheetName);
|
||||
if (sheet == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int lastDataRowIndex = findLastDataRowIndex(sheet);
|
||||
if (lastDataRowIndex < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> missingColumnTitles = new ArrayList<>();
|
||||
for (DropdownColumn column : dropdownColumns) {
|
||||
if (!isListValidationCovered(sheet, column.index(), lastDataRowIndex)) {
|
||||
missingColumnTitles.add(column.title());
|
||||
}
|
||||
}
|
||||
if (!missingColumnTitles.isEmpty()) {
|
||||
throw new ServiceException(sheet.getSheetName() + " Sheet 的 "
|
||||
+ String.join("、", missingColumnTitles)
|
||||
+ " 列缺少下拉框,请下载最新导入模板填写后重新导入");
|
||||
}
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("导入Excel失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<DropdownColumn> resolveDropdownColumns(Class<?> clazz) {
|
||||
List<DropdownColumn> columns = new ArrayList<>();
|
||||
Class<?> current = clazz;
|
||||
while (current != null && current != Object.class) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (field.getAnnotation(DictDropdown.class) == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||
if (excelProperty == null || excelProperty.index() < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
columns.add(new DropdownColumn(excelProperty.index(), resolveColumnTitle(field, excelProperty)));
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
columns.sort(Comparator.comparingInt(DropdownColumn::index));
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static String resolveColumnTitle(Field field, ExcelProperty excelProperty) {
|
||||
if (excelProperty.value().length > 0 && excelProperty.value()[0] != null
|
||||
&& !excelProperty.value()[0].isBlank()) {
|
||||
return excelProperty.value()[0].replace("*", "");
|
||||
}
|
||||
return field.getName();
|
||||
}
|
||||
|
||||
private static int findLastDataRowIndex(Sheet sheet) {
|
||||
int lastDataRowIndex = -1;
|
||||
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (hasData(row)) {
|
||||
lastDataRowIndex = rowIndex;
|
||||
}
|
||||
}
|
||||
return lastDataRowIndex;
|
||||
}
|
||||
|
||||
private static boolean hasData(Row row) {
|
||||
if (row == null || row.getLastCellNum() < 0) {
|
||||
return false;
|
||||
}
|
||||
for (int cellIndex = row.getFirstCellNum(); cellIndex < row.getLastCellNum(); cellIndex++) {
|
||||
if (cellIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
Cell cell = row.getCell(cellIndex);
|
||||
if (cell != null && cell.toString() != null && !cell.toString().isBlank()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isListValidationCovered(Sheet sheet, int columnIndex, int lastDataRowIndex) {
|
||||
boolean[] coveredRows = new boolean[lastDataRowIndex + 1];
|
||||
for (DataValidation validation : sheet.getDataValidations()) {
|
||||
DataValidationConstraint constraint = validation.getValidationConstraint();
|
||||
if (constraint == null || constraint.getValidationType() != DataValidationConstraint.ValidationType.LIST) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (CellRangeAddress address : validation.getRegions().getCellRangeAddresses()) {
|
||||
if (address.getFirstColumn() > columnIndex || address.getLastColumn() < columnIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int firstRow = Math.max(1, address.getFirstRow());
|
||||
int lastRow = Math.min(lastDataRowIndex, address.getLastRow());
|
||||
for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
|
||||
coveredRows[rowIndex] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int rowIndex = 1; rowIndex <= lastDataRowIndex; rowIndex++) {
|
||||
if (hasData(sheet.getRow(rowIndex)) && !coveredRows[rowIndex]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private record DropdownColumn(int index, String title) {}
|
||||
|
||||
private static <T> ExcelWriterBuilder templateWriter(HttpServletResponse response, Class<T> clazz)
|
||||
throws IOException {
|
||||
// 模板为空且体量小,使用内存工作簿避免 SXSSF 在无字体环境初始化 Fontconfig。
|
||||
return EasyExcel.write(response.getOutputStream(), clazz).inMemory(Boolean.TRUE);
|
||||
}
|
||||
|
||||
private static ExcelWriterBuilder templateWriter(HttpServletResponse response) throws IOException {
|
||||
return EasyExcel.write(response.getOutputStream()).inMemory(Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
AND e.status = #{query.status}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY e.create_time DESC
|
||||
ORDER BY e.create_time DESC, e.staff_id DESC
|
||||
</select>
|
||||
|
||||
<!-- 批量插入或更新员工信息(只更新非null字段) -->
|
||||
|
||||
@@ -21,4 +21,15 @@ class CcdiBaseStaffMapperTest {
|
||||
assertTrue(xml.contains("#{item.partyMember}"), xml);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapperXml_shouldUseStableOrderForBaseStaffPagination() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader()
|
||||
.getResourceAsStream("mapper/info/collection/CcdiBaseStaffMapper.xml")) {
|
||||
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)
|
||||
.replaceAll("\\s+", " ");
|
||||
|
||||
assertTrue(xml.contains("ORDER BY e.create_time DESC, e.staff_id DESC"), xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
@@ -110,7 +111,15 @@ public class LsfxAnalysisClient {
|
||||
* 上传文件
|
||||
*/
|
||||
public UploadFileResponse uploadFile(Integer groupId, File file) {
|
||||
log.info("【流水分析】上传文件请求: groupId={}, fileName={}", groupId, file.getName());
|
||||
return uploadFile(groupId, file, file.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
public UploadFileResponse uploadFile(Integer groupId, File file, String uploadFileName) {
|
||||
String multipartFileName = StringUtils.hasText(uploadFileName) ? uploadFileName : file.getName();
|
||||
log.info("【流水分析】上传文件请求: groupId={}, fileName={}", groupId, multipartFileName);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
@@ -118,7 +127,7 @@ public class LsfxAnalysisClient {
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("groupId", groupId);
|
||||
params.put("files", file);
|
||||
params.put("files", HttpUtil.namedFileResource(file, multipartFileName));
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
@@ -31,6 +32,24 @@ public class HttpUtil {
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public static org.springframework.core.io.Resource namedFileResource(File file, String filename) {
|
||||
return new NamedFileSystemResource(file, filename);
|
||||
}
|
||||
|
||||
private static class NamedFileSystemResource extends FileSystemResource {
|
||||
private final String filename;
|
||||
|
||||
NamedFileSystemResource(File file, String filename) {
|
||||
super(file);
|
||||
this.filename = StringUtils.hasText(filename) ? filename : file.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送GET请求(带查询参数和请求头)
|
||||
* @param url 请求URL
|
||||
@@ -207,6 +226,8 @@ public class HttpUtil {
|
||||
if (value instanceof File) {
|
||||
File file = (File) value;
|
||||
body.add(key, new FileSystemResource(file));
|
||||
} else if (value instanceof org.springframework.core.io.Resource) {
|
||||
body.add(key, value);
|
||||
} else {
|
||||
body.add(key, value);
|
||||
}
|
||||
|
||||
@@ -659,7 +659,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
throw new RuntimeException("临时文件不存在: " + tempFilePath);
|
||||
}
|
||||
|
||||
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file);
|
||||
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName());
|
||||
if (uploadResponse == null || uploadResponse.getData() == null
|
||||
|| uploadResponse.getData().getUploadLogList() == null
|
||||
|| uploadResponse.getData().getUploadLogList().isEmpty()) {
|
||||
@@ -673,7 +673,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
|
||||
log.info("【文件上传】文件上传成功: logId={}", logId);
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId);
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, true);
|
||||
log.info("【文件上传】处理完成: fileName={}", record.getFileName());
|
||||
return true;
|
||||
|
||||
@@ -710,6 +710,14 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
Integer logId) {
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false);
|
||||
}
|
||||
|
||||
private void processRecordAfterLogIdReady(Long projectId,
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
Integer logId,
|
||||
boolean preserveRecordFileName) {
|
||||
log.info("【文件上传】步骤3: 更新状态为解析中, logId={}", logId);
|
||||
record.setLogId(logId);
|
||||
record.setFileStatus("parsing");
|
||||
@@ -736,11 +744,13 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0);
|
||||
Integer status = logItem.getStatus();
|
||||
String uploadStatusDesc = logItem.getUploadStatusDesc();
|
||||
String fileName = StringUtils.hasText(logItem.getUploadFileName())
|
||||
? logItem.getUploadFileName()
|
||||
: logItem.getDownloadFileName();
|
||||
if (StringUtils.hasText(fileName)) {
|
||||
record.setFileName(fileName);
|
||||
if (!preserveRecordFileName) {
|
||||
String fileName = StringUtils.hasText(logItem.getUploadFileName())
|
||||
? logItem.getUploadFileName()
|
||||
: logItem.getDownloadFileName();
|
||||
if (StringUtils.hasText(fileName)) {
|
||||
record.setFileName(fileName);
|
||||
}
|
||||
}
|
||||
if (logItem.getFileSize() != null) {
|
||||
record.setFileSize(logItem.getFileSize());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.ccdi.project.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiModelParam;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
|
||||
@@ -39,12 +40,11 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
|
||||
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.service.ICcdiModelParamService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -74,6 +74,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
@Resource
|
||||
private CcdiProjectMapper projectMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiModelParamMapper modelParamMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;
|
||||
|
||||
@@ -89,9 +92,6 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
@Resource
|
||||
private CcdiProjectOverviewReportPdfExporter reportPdfExporter;
|
||||
|
||||
@Resource
|
||||
private ICcdiModelParamService modelParamService;
|
||||
|
||||
@Override
|
||||
public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) {
|
||||
CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId);
|
||||
@@ -328,7 +328,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
report.setUploadSubjects(defaultList(overviewMapper.selectReportUploadSubjects(projectId)).stream()
|
||||
.peek(item -> item.setDataPeriod(formatDataPeriod(item.getMinTrxDate(), item.getMaxTrxDate())))
|
||||
.toList());
|
||||
report.setParams(buildReportParams(projectId));
|
||||
report.setParams(buildReportParams(project));
|
||||
report.setModelSummaries(defaultList(overviewMapper.selectReportRiskModelSummaries(projectId)));
|
||||
report.setRiskPeople(defaultList(overviewMapper.selectReportRiskPeople(projectId)).stream()
|
||||
.peek(item -> item.setActionLabel(ACTION_LABEL))
|
||||
@@ -554,21 +554,23 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
return row;
|
||||
}
|
||||
|
||||
private List<CcdiProjectOverviewReportParamVO> buildReportParams(Long projectId) {
|
||||
ModelParamAllVO response = modelParamService.selectAllParams(projectId);
|
||||
return defaultList(response == null ? null : response.getModels()).stream()
|
||||
.flatMap(model -> defaultList(model.getParams()).stream().map(param -> {
|
||||
CcdiProjectOverviewReportParamVO row = new CcdiProjectOverviewReportParamVO();
|
||||
row.setModelName(model.getModelName());
|
||||
row.setParamName(param.getParamName());
|
||||
row.setParamValue(param.getParamValue());
|
||||
row.setParamUnit(param.getParamUnit());
|
||||
row.setParamDesc(param.getParamDesc());
|
||||
return row;
|
||||
}))
|
||||
private List<CcdiProjectOverviewReportParamVO> buildReportParams(CcdiProject project) {
|
||||
Long effectiveProjectId = "default".equals(project.getConfigType()) ? 0L : project.getProjectId();
|
||||
return defaultList(modelParamMapper.selectByProjectId(effectiveProjectId)).stream()
|
||||
.map(this::buildReportParamRow)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private CcdiProjectOverviewReportParamVO buildReportParamRow(CcdiModelParam param) {
|
||||
CcdiProjectOverviewReportParamVO row = new CcdiProjectOverviewReportParamVO();
|
||||
row.setModelName(param.getModelName());
|
||||
row.setParamName(param.getParamName());
|
||||
row.setParamValue(param.getParamValue());
|
||||
row.setParamUnit(param.getParamUnit());
|
||||
row.setParamDesc(param.getParamDesc());
|
||||
return row;
|
||||
}
|
||||
|
||||
private String formatDataPeriod(String minTrxDate, String maxTrxDate) {
|
||||
if (minTrxDate == null || minTrxDate.isBlank() || maxTrxDate == null || maxTrxDate.isBlank()) {
|
||||
return "-";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.ruoyi.ccdi.project.service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class CcdiProjectOverviewServiceStructureTest {
|
||||
@@ -35,4 +37,15 @@ class CcdiProjectOverviewServiceStructureTest {
|
||||
assertNotNull(clazz.getMethod("refreshOverviewEmployeeResults", Long.class, String.class));
|
||||
assertNotNull(clazz.getMethod("refreshProjectRiskCounts", Long.class, String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void overviewServiceImplShouldNotDependOnModelParamService() throws Exception {
|
||||
Class<?> clazz = Class.forName(
|
||||
"com.ruoyi.ccdi.project.service.impl.CcdiProjectOverviewServiceImpl"
|
||||
);
|
||||
|
||||
assertFalse(Arrays.stream(clazz.getDeclaredFields()).anyMatch(field ->
|
||||
"com.ruoyi.ccdi.project.service.ICcdiModelParamService".equals(field.getType().getName())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
@@ -291,7 +292,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(1);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
@@ -317,7 +319,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
@@ -364,6 +367,77 @@ class CcdiFileUploadServiceImplTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldUploadToLsfxWithOriginalRecordFileName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("原始流水.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)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
|
||||
verify(lsfxClient).uploadFile(eq(LSFX_PROJECT_ID), argThat(file ->
|
||||
file.getName().startsWith("upload-") && file.getName().endsWith(".xlsx")
|
||||
), eq("原始流水.xlsx"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldKeepOriginalFileNameWhenStatusReturnsDifferentName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any()))
|
||||
.thenReturn(buildParsedSuccessStatusResponse("平台返回文件名.xlsx"));
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
"parsed_success".equals(item.getFileStatus())
|
||||
&& "原始流水.xlsx".equals(item.getFileName()))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldKeepOriginalFileNameWhenParseStatusFails() throws IOException {
|
||||
GetFileUploadStatusResponse statusResponse = buildParsedSuccessStatusResponse("平台失败文件名.xlsx");
|
||||
GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0);
|
||||
logItem.setStatus(-1);
|
||||
logItem.setUploadStatusDesc("parse.failed");
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(statusResponse);
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
"parsed_failed".equals(item.getFileStatus())
|
||||
&& "原始流水.xlsx".equals(item.getFileName()))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted() {
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
@@ -468,7 +542,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
@@ -491,7 +566,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
List<CcdiFileUploadRecord> updates = new ArrayList<>();
|
||||
captureUpdatedRecords(updates);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
@@ -512,7 +588,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
List<CcdiFileUploadRecord> updates = new ArrayList<>();
|
||||
captureUpdatedRecords(updates);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any()))
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenThrow(new RuntimeException("upload failed:" + "x".repeat(3000)));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
@@ -526,7 +602,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void fetchAndSaveBankStatements_shouldTrimLeAccountNoBeforeInsert() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
@@ -597,7 +674,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.ccdi.project.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiModelParam;
|
||||
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
|
||||
@@ -16,6 +17,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
|
||||
@@ -27,6 +29,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
|
||||
@@ -38,6 +41,7 @@ import java.util.List;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
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;
|
||||
@@ -65,6 +69,9 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
@Mock
|
||||
private CcdiProjectMapper projectMapper;
|
||||
|
||||
@Mock
|
||||
private CcdiModelParamMapper modelParamMapper;
|
||||
|
||||
@Mock
|
||||
private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;
|
||||
|
||||
@@ -77,6 +84,9 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
@Mock
|
||||
private CcdiProjectRiskDetailWorkbookExporter workbookExporter;
|
||||
|
||||
@Mock
|
||||
private CcdiProjectOverviewReportPdfExporter reportPdfExporter;
|
||||
|
||||
@Test
|
||||
void shouldBuildDashboardWithNoRiskCount() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
@@ -300,6 +310,37 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExportOverviewReportParamsFromDefaultProjectConfig() throws Exception {
|
||||
CcdiProject project = new CcdiProject();
|
||||
project.setProjectId(40L);
|
||||
project.setProjectName("测试项目");
|
||||
project.setConfigType("default");
|
||||
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||
|
||||
CcdiProject dashboardProject = new CcdiProject();
|
||||
dashboardProject.setProjectId(40L);
|
||||
dashboardProject.setTargetCount(10);
|
||||
dashboardProject.setHighRiskCount(1);
|
||||
dashboardProject.setMediumRiskCount(2);
|
||||
dashboardProject.setLowRiskCount(3);
|
||||
when(overviewMapper.selectDashboardBaseByProjectId(40L)).thenReturn(dashboardProject);
|
||||
when(modelParamMapper.selectByProjectId(0L)).thenReturn(List.of(
|
||||
buildModelParam("LARGE_TRANSACTION", "大额交易模型", "单笔金额", "1000", "元", "单笔金额阈值")
|
||||
));
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
service.exportOverviewReport(response, 40L);
|
||||
|
||||
ArgumentCaptor<CcdiProjectOverviewReportVO> captor =
|
||||
ArgumentCaptor.forClass(CcdiProjectOverviewReportVO.class);
|
||||
verify(modelParamMapper).selectByProjectId(0L);
|
||||
verify(reportPdfExporter).export(eq(response), captor.capture());
|
||||
assertEquals(1, captor.getValue().getParams().size());
|
||||
assertEquals("大额交易模型", captor.getValue().getParams().getFirst().getModelName());
|
||||
assertEquals("单笔金额", captor.getValue().getParams().getFirst().getParamName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPersonAnalysisDetailWithBasicInfoAndGroupedAbnormalDetail() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
@@ -539,6 +580,24 @@ class CcdiProjectOverviewServiceImplTest {
|
||||
return result;
|
||||
}
|
||||
|
||||
private CcdiModelParam buildModelParam(
|
||||
String modelCode,
|
||||
String modelName,
|
||||
String paramName,
|
||||
String paramValue,
|
||||
String paramUnit,
|
||||
String paramDesc
|
||||
) {
|
||||
CcdiModelParam param = new CcdiModelParam();
|
||||
param.setModelCode(modelCode);
|
||||
param.setModelName(modelName);
|
||||
param.setParamName(paramName);
|
||||
param.setParamValue(paramValue);
|
||||
param.setParamUnit(paramUnit);
|
||||
param.setParamDesc(paramDesc);
|
||||
return param;
|
||||
}
|
||||
|
||||
private CcdiProjectRiskHitTagVO buildHitTag(
|
||||
String modelCode,
|
||||
String modelName,
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
# 员工资产导入与实体库自动补入修复设计
|
||||
|
||||
## 1. 背景
|
||||
|
||||
本设计用于修复以下后端问题:
|
||||
|
||||
1. 【员工信息维护】双 Sheet 导入时,`员工资产信息` Sheet 只能通过数据库已有员工反查归属,不能识别同一个导入文件中刚成功导入的员工。
|
||||
2. 【员工亲属关系维护】双 Sheet 导入时,`亲属资产信息` Sheet 只能通过数据库已有亲属关系反查归属,不能识别同一个导入文件中刚成功导入的亲属关系。
|
||||
3. 关联业务自动补入实体库能力在当前主工作区未完整接回,员工亲属、信贷客户、中介、供应商四类业务成功关联后,缺失企业需要统一补入 `ccdi_enterprise_base_info`。
|
||||
|
||||
本次设计只涉及后端。不调整前端页面结构、上传入口、模板样式和前端轮询字段。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
- 双 Sheet 导入时,主 Sheet 与资产 Sheet 在后端按业务依赖顺序执行。
|
||||
- 资产 Sheet 可关联数据库已有主数据,也可关联同一文件中本轮主 Sheet 成功导入的数据。
|
||||
- 主 Sheet 失败行不能作为资产归属依据。
|
||||
- 继续返回当前前端已支持的两个任务 ID:
|
||||
- 员工信息维护:`staffTaskId`、`assetTaskId`
|
||||
- 员工亲属关系维护:`relationTaskId`、`assetTaskId`
|
||||
- 恢复统一 `EnterpriseAutoFillService`,并接入员工亲属、信贷客户、中介、供应商四类业务。
|
||||
- 实体库自动补入只插入缺失企业,不更新已存在实体。
|
||||
|
||||
## 3. 不在本次范围
|
||||
|
||||
- 不改前端上传入口、轮询逻辑、失败记录展示和模板下载样式。
|
||||
- 不合并双 Sheet 导入任务 ID。
|
||||
- 不改变员工、亲属关系、资产、实体关联现有字段校验规则。
|
||||
- 不改变实体库手工新增、编辑、导入的既有业务规则。
|
||||
- 不增加兜底来源、降级来源或兼容性分支。
|
||||
|
||||
## 4. 总体架构
|
||||
|
||||
本次后端设计分为两条链路。
|
||||
|
||||
### 4.1 双 Sheet 导入编排
|
||||
|
||||
`/ccdi/baseStaff/importData` 和 `/ccdi/staffFmyRelation/importData` 保持原接口、原模板、原返回字段。
|
||||
|
||||
Controller 继续负责读取两个 Sheet,但不再分别提交两个彼此独立的异步任务。服务层新增提交编排方法:
|
||||
|
||||
- 员工信息维护提交方法接收 `List<CcdiBaseStaffExcel>` 和 `List<CcdiBaseStaffAssetInfoExcel>`。
|
||||
- 员工亲属关系维护提交方法接收 `List<CcdiStaffFmyRelationExcel>` 和 `List<CcdiAssetInfoExcel>`。
|
||||
|
||||
提交方法按实际存在的 Sheet 初始化对应任务状态,并启动一个后台编排任务。编排任务内部按顺序执行:
|
||||
|
||||
1. 主 Sheet 校验与插入。
|
||||
2. 收集本轮主 Sheet 成功上下文。
|
||||
3. 资产 Sheet 校验与插入。
|
||||
|
||||
### 4.2 实体库自动补入
|
||||
|
||||
新增或恢复统一内部服务:
|
||||
|
||||
`ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillService.java`
|
||||
|
||||
该服务是关联业务补入实体库的唯一后端入口。四类业务在业务校验通过、业务记录落库前调用:
|
||||
|
||||
- 员工亲属实体关联:`EMP_RELATION`
|
||||
- 信贷客户实体关联:`CREDIT_CUSTOMER`
|
||||
- 中介实体关联:`INTERMEDIARY`
|
||||
- 招投标供应商:`SUPPLIER`
|
||||
|
||||
实体库缺失时最小插入;已存在时不更新。
|
||||
|
||||
供应商来源需要同步新增后端枚举 `EnterpriseSource.SUPPLIER("SUPPLIER", "供应商")`,并补充 `/ccdi/enum/enterpriseSource` 枚举接口测试。前端继续通过现有枚举接口展示来源,不需要调整页面结构。
|
||||
|
||||
## 5. 员工信息维护双 Sheet 导入设计
|
||||
|
||||
### 5.1 当前问题
|
||||
|
||||
当前 `/ccdi/baseStaff/importData` 分别读取 `员工信息` 和 `员工资产信息`,然后分别调用:
|
||||
|
||||
- `baseStaffService.importBaseStaff(staffList)`
|
||||
- `baseStaffAssetImportService.importAssetInfo(assetList)`
|
||||
|
||||
两个任务互相独立。员工资产导入只通过数据库 `ccdi_base_staff.id_card` 查找归属,无法稳定看到同一文件中刚导入成功的员工。
|
||||
|
||||
### 5.2 设计方案
|
||||
|
||||
新增员工信息维护双 Sheet 后端编排能力。
|
||||
|
||||
提交阶段:
|
||||
|
||||
1. Controller 读取两个 Sheet。
|
||||
2. 若两个 Sheet 都无数据,仍返回“至少需要一条数据”。
|
||||
3. 若存在员工 Sheet,生成并初始化 `staffTaskId`。
|
||||
4. 若存在员工资产 Sheet,生成并初始化 `assetTaskId`。
|
||||
5. 返回当前前端兼容的 `BaseStaffImportSubmitResultVO`。
|
||||
6. 后台启动一个编排任务。
|
||||
|
||||
编排阶段:
|
||||
|
||||
1. 先执行员工主数据导入。
|
||||
2. 沿用现有员工必填、身份证号、部门、员工 ID、身份证号重复校验。
|
||||
3. 批量插入员工成功记录。
|
||||
4. 收集本轮成功员工的 `idCard`。
|
||||
5. 更新员工任务状态和失败记录。
|
||||
6. 再执行员工资产导入。
|
||||
7. 员工资产归属候选来源为:
|
||||
- 数据库已有 `ccdi_base_staff.id_card`
|
||||
- 本轮员工 Sheet 成功导入的 `idCard`
|
||||
8. 员工资产落库继续保持:
|
||||
- `family_id = 员工身份证号`
|
||||
- `person_id = 员工身份证号`
|
||||
9. 员工资产继续保持重复校验:`personId + assetMainType + assetSubType + assetName`。
|
||||
|
||||
### 5.3 任务状态
|
||||
|
||||
- 只填员工 Sheet:只返回 `staffTaskId`。
|
||||
- 只填员工资产 Sheet:只返回 `assetTaskId`,只按数据库已有员工校验。
|
||||
- 只填员工资产 Sheet 是正常导入场景,不因 `员工信息` Sheet 为空或未填写而拦截。
|
||||
- 两个 Sheet 都填:返回 `staffTaskId` 和 `assetTaskId`,后台保证员工先处理、资产后处理。
|
||||
- 员工主 Sheet 部分成功时,员工资产只能使用成功员工上下文。
|
||||
- 员工主 Sheet 全部失败时,员工资产仍执行,但只能命中数据库已有员工。
|
||||
|
||||
## 6. 员工亲属关系维护双 Sheet 导入设计
|
||||
|
||||
### 6.1 当前问题
|
||||
|
||||
当前 `/ccdi/staffFmyRelation/importData` 分别读取 `员工亲属关系信息` 和 `亲属资产信息`,然后分别调用:
|
||||
|
||||
- `relationService.importRelation(relationList)`
|
||||
- `assetInfoImportService.importAssetInfo(assetList)`
|
||||
|
||||
两个任务互相独立。亲属资产导入只通过数据库 `ccdi_staff_fmy_relation.relation_cert_no` 查找归属,无法稳定看到同一文件中刚导入成功的亲属关系。
|
||||
|
||||
### 6.2 设计方案
|
||||
|
||||
新增员工亲属关系维护双 Sheet 后端编排能力。
|
||||
|
||||
提交阶段:
|
||||
|
||||
1. Controller 读取两个 Sheet。
|
||||
2. 若两个 Sheet 都无数据,仍返回“至少需要一条数据”。
|
||||
3. 若存在亲属关系 Sheet,生成并初始化 `relationTaskId`。
|
||||
4. 若存在亲属资产 Sheet,生成并初始化 `assetTaskId`。
|
||||
5. 返回当前前端兼容的 `StaffFmyRelationImportSubmitResultVO`。
|
||||
6. 后台启动一个编排任务。
|
||||
|
||||
编排阶段:
|
||||
|
||||
1. 先执行亲属关系导入。
|
||||
2. 沿用现有亲属关系必填、员工身份证号存在性、关系人证件号、重复组合校验。
|
||||
3. 批量插入亲属关系成功记录。
|
||||
4. 收集本轮成功亲属关系映射:
|
||||
- `relationCertNo` 作为资产 Sheet 的 `personId`
|
||||
- `personId` 作为资产落库的 `familyId`
|
||||
5. 更新亲属关系任务状态和失败记录。
|
||||
6. 再执行亲属资产导入。
|
||||
7. 亲属资产归属候选来源为:
|
||||
- 数据库已有员工亲属关系,沿用现有 owner 查询条件
|
||||
- 本轮亲属关系 Sheet 成功导入的员工亲属关系
|
||||
8. 亲属资产落库继续保持:
|
||||
- `family_id = 员工身份证号`
|
||||
- `person_id = 亲属身份证号`
|
||||
|
||||
### 6.3 任务状态
|
||||
|
||||
- 只填亲属关系 Sheet:只返回 `relationTaskId`。
|
||||
- 只填亲属资产 Sheet:只返回 `assetTaskId`,只按数据库已有员工亲属关系校验,并沿用现有 owner 查询条件。
|
||||
- 只填亲属资产 Sheet 是正常导入场景,不因 `员工亲属关系信息` Sheet 为空或未填写而拦截。
|
||||
- 两个 Sheet 都填:返回 `relationTaskId` 和 `assetTaskId`,后台保证亲属关系先处理、资产后处理。
|
||||
- 亲属关系主 Sheet 部分成功时,亲属资产只能使用成功亲属关系上下文。
|
||||
- 亲属关系主 Sheet 全部失败时,亲属资产仍执行,但只能命中数据库已有员工亲属关系。
|
||||
|
||||
## 7. 实体库自动补入设计
|
||||
|
||||
### 7.1 服务职责
|
||||
|
||||
`EnterpriseAutoFillService` 只负责一件事:对关联业务成功记录中的企业,确保实体库存在对应统一社会信用代码。
|
||||
|
||||
服务接口:
|
||||
|
||||
- `ensureExists(EnterpriseFillItem item)`
|
||||
- `ensureExistsBatch(List<EnterpriseFillItem> items)`
|
||||
|
||||
`EnterpriseFillItem` 字段:
|
||||
|
||||
- `socialCreditCode`
|
||||
- `enterpriseName`
|
||||
- `entSource`
|
||||
- `dataSource`
|
||||
- `userName`
|
||||
|
||||
### 7.2 插入规则
|
||||
|
||||
1. 按 `socialCreditCode` 去重,同批次同一统一社会信用代码只处理一次。
|
||||
2. 批量查询 `ccdi_enterprise_base_info` 已存在记录。
|
||||
3. 已存在实体不更新。
|
||||
4. 缺失实体最小插入:
|
||||
- `social_credit_code = 统一社会信用代码`
|
||||
- `enterprise_name = 企业名称;中介实体关联缺失实体时允许为 NULL`
|
||||
- `ent_source = 来源`
|
||||
- `data_source = MANUAL 或 IMPORT`
|
||||
- `risk_level = 来源规则值`
|
||||
- `created_by/updated_by = 当前用户`
|
||||
5. 中介实体关联缺失实体时不要求提供机构名称,补入实体的 `enterprise_name` 可以为 `NULL`。
|
||||
6. 中介来源 `INTERMEDIARY` 写 `risk_level = 1`。
|
||||
7. 员工亲属、信贷客户、供应商来源写 `risk_level = NULL`。
|
||||
8. 并发导致主键重复时,按“已存在实体”处理。
|
||||
9. 其他数据库异常抛出,让当前业务事务失败。
|
||||
|
||||
### 7.3 四类业务接入点
|
||||
|
||||
员工亲属实体关联:
|
||||
|
||||
- 手工新增:校验有效亲属和组合不重复后、插入关联表前补入。
|
||||
- 导入:只对校验成功并即将插入的记录批量补入。
|
||||
- 来源:`EMP_RELATION`
|
||||
- 数据来源:手工新增为 `MANUAL`,导入为 `IMPORT`
|
||||
- 风险等级:`NULL`
|
||||
|
||||
信贷客户实体关联:
|
||||
|
||||
- 手工新增:校验通过后、插入关联表前补入。
|
||||
- 导入:只对校验成功并即将插入的记录批量补入。
|
||||
- 来源:`CREDIT_CUSTOMER`
|
||||
- 数据来源:手工新增为 `MANUAL`,导入为 `IMPORT`
|
||||
- 风险等级:`NULL`
|
||||
|
||||
中介实体关联:
|
||||
|
||||
- 手工新增:字段校验和重复关系校验通过后,取消或替换“实体库必须已存在”校验,先补入实体库,再插入关联关系。
|
||||
- 导入:字段校验和重复关系校验通过后,取消或替换“实体库必须已存在”失败条件,只对即将插入的成功记录批量补入。
|
||||
- 来源:`INTERMEDIARY`
|
||||
- 数据来源:手工新增为 `MANUAL`,导入为 `IMPORT`
|
||||
- 风险等级:`1`
|
||||
|
||||
招投标供应商:
|
||||
|
||||
- 手工新增或保存招投标主信息时,只对 `supplierUscc` 非空且通过现有格式校验的供应商补入。
|
||||
- 导入:只对成功采购事项中 `supplierUscc` 非空且通过现有格式校验的供应商批量补入,失败采购事项不补入。
|
||||
- 导入中 `supplierUscc` 为空的供应商保持现有保存规则,但不补入实体库。
|
||||
- 来源:`SUPPLIER`
|
||||
- 数据来源:手工新增为 `MANUAL`,导入为 `IMPORT`
|
||||
- 风险等级:`NULL`
|
||||
|
||||
## 8. 错误处理与边界
|
||||
|
||||
### 8.1 双 Sheet 导入边界
|
||||
|
||||
- 主 Sheet 失败行不能进入资产归属候选。
|
||||
- 主 Sheet 文件内重复行不能进入资产归属候选。
|
||||
- 主 Sheet 数据库重复行不能进入资产归属候选。
|
||||
- 主 Sheet 为空但资产 Sheet 有数据时,资产 Sheet 必须按现有独立资产导入规则正常执行。
|
||||
- 资产 Sheet 仍按自身失败记录任务记录 `sheetName`、`rowNum`、`errorMessage`。
|
||||
- 员工资产找不到员工时继续报“员工资产导入仅支持员工本人证件号”。
|
||||
- 亲属资产找不到归属时继续报“未找到亲属资产归属员工”。
|
||||
- 亲属资产命中多个归属时继续报“亲属资产归属员工不唯一”。
|
||||
- 亲属资产数据库归属查询条件沿用当前实现,不因本次编排新增额外状态过滤。
|
||||
|
||||
### 8.2 自动补入边界
|
||||
|
||||
- 只处理业务校验成功、即将落库的记录。
|
||||
- 失败业务行不能产生实体库记录。
|
||||
- 已存在实体不更新,避免覆盖人工维护数据。
|
||||
- 不增加默认企业名称、默认来源、默认风险等级等兜底逻辑。
|
||||
- 不改变实体库导入的严格新增规则。
|
||||
- 中介实体关联的“实体库必须已存在”校验需要被自动补入替换,否则缺失实体无法进入补入链路。
|
||||
- 中介实体关联不新增机构名称入参;缺失实体补入时允许 `enterprise_name = NULL`。
|
||||
- 供应商自动补入只处理非空统一社会信用代码,空统一社会信用代码不改变原导入保存规则。
|
||||
|
||||
## 9. 测试设计
|
||||
|
||||
### 9.1 单元测试
|
||||
|
||||
员工信息维护双 Sheet:
|
||||
|
||||
- 同一模板中新员工和员工资产同时导入,资产引用新员工身份证号,资产成功。
|
||||
- 员工 Sheet 行校验失败,资产引用该身份证号且数据库不存在,资产失败。
|
||||
- 员工信息 Sheet 为空或未填写,员工资产 Sheet 引用数据库已有员工时,资产成功导入。
|
||||
- 只导员工资产,数据库已有员工时成功。
|
||||
- 只导员工资产,数据库无员工时失败。
|
||||
- 资产重复命中数据库或当前文件重复时失败。
|
||||
|
||||
员工亲属关系维护双 Sheet:
|
||||
|
||||
- 同一模板中新亲属关系和亲属资产同时导入,资产引用新亲属证件号,资产成功。
|
||||
- 亲属关系 Sheet 行校验失败,资产引用该亲属证件号且数据库不存在,资产失败。
|
||||
- 员工亲属关系信息 Sheet 为空或未填写,亲属资产 Sheet 引用数据库已有员工亲属关系时,资产成功导入。
|
||||
- 只导亲属资产,数据库已有唯一员工亲属关系时成功。
|
||||
- 只导亲属资产,数据库不存在亲属关系时失败。
|
||||
- 同一亲属证件号命中多个员工归属时失败。
|
||||
|
||||
实体库自动补入:
|
||||
|
||||
- 已存在实体不插入、不更新。
|
||||
- 缺失实体插入最小记录。
|
||||
- 同批多个成功行引用同一统一社会信用代码,只补入一次。
|
||||
- 中介来源写 `riskLevel=1`。
|
||||
- 员工亲属、信贷客户、供应商来源写 `riskLevel=null`。
|
||||
- 后端枚举接口返回 `SUPPLIER/供应商`。
|
||||
- 并发重复主键按已存在处理。
|
||||
|
||||
四类业务接入:
|
||||
|
||||
- 手工新增成功时调用自动补入。
|
||||
- 导入成功行进入自动补入集合。
|
||||
- 导入失败行不进入自动补入集合。
|
||||
|
||||
### 9.2 接口验证
|
||||
|
||||
1. 下载真实 `/ccdi/baseStaff/importTemplate` 模板,构造 `员工信息` + `员工资产信息` 同文件导入,上传 `/ccdi/baseStaff/importData`,轮询两个任务并查询员工、资产落库。
|
||||
2. 下载真实 `/ccdi/staffFmyRelation/importTemplate` 模板,构造 `员工亲属关系信息` + `亲属资产信息` 同文件导入,上传 `/ccdi/staffFmyRelation/importData`,轮询两个任务并查询亲属关系、资产落库。
|
||||
3. 分别调用员工亲属、信贷客户、中介、供应商新增或导入接口,验证实体库自动补入来源、数据来源和风险等级。
|
||||
4. 涉及中文清理 SQL 或验证 SQL 时使用 `bin/mysql_utf8_exec.sh`。
|
||||
|
||||
### 9.3 页面验证
|
||||
|
||||
完成后必须进入真实业务页面验证,不打开 prototype:
|
||||
|
||||
1. 【员工信息维护】下载模板,填写员工和员工资产,上传后检查任务状态、失败记录、列表和详情资产。
|
||||
2. 【员工亲属关系维护】下载模板,填写亲属关系和亲属资产,上传后检查任务状态、失败记录、详情资产。
|
||||
3. 四类实体自动补入完成后,进入【实体库管理】按统一社会信用代码查询。
|
||||
4. 测试结束后清理本轮新增员工、亲属关系、资产、实体关联和自动补入实体库数据。
|
||||
5. 测试结束后关闭测试过程中启动的前后端进程。
|
||||
|
||||
## 10. 实施顺序
|
||||
|
||||
1. 提炼员工主数据导入执行方法,返回成功员工上下文和失败记录。
|
||||
2. 提炼员工资产导入执行方法,支持额外员工归属上下文。
|
||||
3. 新增员工信息维护双 Sheet 编排方法并接入 Controller。
|
||||
4. 提炼亲属关系导入执行方法,返回成功亲属关系上下文和失败记录。
|
||||
5. 提炼亲属资产导入执行方法,支持额外亲属关系归属上下文。
|
||||
6. 新增员工亲属关系维护双 Sheet 编排方法并接入 Controller。
|
||||
7. 恢复或新增 `EnterpriseAutoFillService`。
|
||||
8. 接入员工亲属、信贷客户、中介、供应商四类自动补入。
|
||||
9. 补充单元测试。
|
||||
10. 执行接口验证和真实页面验证。
|
||||
11. 新增实施记录到 `docs/reports/implementation/`。
|
||||
|
||||
## 11. 风险与控制
|
||||
|
||||
- 不能继续由 Controller 分别提交两个独立异步任务,否则资产任务执行顺序仍不确定。
|
||||
- 不能把主 Sheet 失败行放入资产归属上下文,否则会产生脏资产。
|
||||
- 不能更新已存在实体库记录,否则会覆盖人工维护的企业名称、风险等级和来源。
|
||||
- 自动补入必须在业务记录落库前执行,并与业务事务保持一致。
|
||||
- 四类自动补入必须只处理成功业务行,失败行不能污染实体库。
|
||||
@@ -0,0 +1,128 @@
|
||||
# 导入模板下拉框结构校验后端设计
|
||||
|
||||
## 背景
|
||||
|
||||
本次问题来自员工信息维护批量导入文件:
|
||||
|
||||
- 文件路径:`/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx`
|
||||
- 问题表现:`员工信息` Sheet 的 `状态` 列没有 Excel 下拉框,但当前导入链路会继续读取数据并进入业务导入。
|
||||
|
||||
用户已确认本次范围为:所有 Excel 导入中,凡是导入对象字段标注了 `@DictDropdown`,上传文件对应 Sheet 的对应列都必须保留模板下拉框;缺失下拉框时导入应立即报错。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 统一拦截缺少下拉框的数据文件,避免模板结构被破坏后仍被导入。
|
||||
2. 覆盖所有使用 `EasyExcelUtil.importExcel(...)` 读取、且导入对象含 `@DictDropdown` 字段的导入接口。
|
||||
3. 双 Sheet 导入按 Sheet 分别校验,任一应有下拉框的列缺失时立即失败。
|
||||
4. 错误提示明确到 Sheet 和列,便于用户重新下载模板处理。
|
||||
|
||||
## 非目标
|
||||
|
||||
1. 不改造无 `@DictDropdown` 字段的导入,例如当前未使用字典下拉模板的导入不强行纳入。
|
||||
2. 不改变现有业务字段校验、重复校验、异步导入、失败记录展示逻辑。
|
||||
3. 不新增兼容旧模板或降级导入逻辑;缺少下拉框即按模板不合规处理。
|
||||
|
||||
## 推荐方案
|
||||
|
||||
采用工具层统一校验:
|
||||
|
||||
1. 在 `EasyExcelUtil.importExcel(InputStream, Class<T>)` 和 `EasyExcelUtil.importExcel(InputStream, Class<T>, String)` 内部先读取上传文件字节。
|
||||
2. 使用 POI 打开工作簿,根据导入类上的 `@DictDropdown` 和 `@ExcelProperty(index)` 解析应校验的列。
|
||||
3. 校验对应 Sheet 中是否存在覆盖该列数据区的数据验证规则。
|
||||
4. 校验通过后,再使用 EasyExcel 按现有方式读取数据。
|
||||
|
||||
该方案集中在公共导入工具,能随现有导入调用链自然覆盖员工信息、员工资产、亲属关系、招聘、调动、招投标、实体库、中介等使用字典下拉模板的导入。
|
||||
|
||||
## 校验规则
|
||||
|
||||
### 字段解析
|
||||
|
||||
- 扫描导入类及父类字段。
|
||||
- 仅处理同时具备 `@DictDropdown` 和带明确 `index` 的 `@ExcelProperty` 字段。
|
||||
- 列标题优先取 `@ExcelProperty.value()` 的第一个值。
|
||||
|
||||
### Sheet 定位
|
||||
|
||||
- 指定 Sheet 名读取时,校验该 Sheet。
|
||||
- 未指定 Sheet 名读取时,校验第一个 Sheet。
|
||||
- 若 Sheet 不存在,保持现有读取失败语义,不额外设计兜底。
|
||||
|
||||
### 下拉框判断
|
||||
|
||||
- 读取 Sheet 的 `DataValidation` 列表。
|
||||
- 只认可 `DataValidationConstraint.ValidationType.LIST` 类型的数据验证;数字、日期、自定义公式等其他校验类型不能视为模板下拉框。
|
||||
- 数据区定义为:从第 2 行开始,到本次上传文件中该 Sheet 的最后一行有效数据。
|
||||
- 对每个实际数据行,目标列单元格都必须被 `LIST` 数据验证区域覆盖;只覆盖表头、只覆盖单个样例行、或只覆盖部分数据行,都视为该列下拉框缺失。
|
||||
- 校验目标为模板结构是否保留,不判断用户是否逐单元格从下拉框选择。
|
||||
|
||||
### 失败行为
|
||||
|
||||
任一应有下拉框的列缺失数据验证时,导入立即失败,不进入异步任务,不写 Redis 状态,不产生部分成功。
|
||||
|
||||
建议错误文案:
|
||||
|
||||
```text
|
||||
员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入
|
||||
```
|
||||
|
||||
双 Sheet 场景示例:
|
||||
|
||||
- `员工信息` Sheet 的 `是否党员` 或 `状态` 缺失下拉框:失败。
|
||||
- `员工资产信息` Sheet 的 `资产状态` 缺失下拉框:失败。
|
||||
- 两个 Sheet 都保留下拉框:继续现有导入读取和业务校验。
|
||||
|
||||
## 影响范围
|
||||
|
||||
后端主要影响:
|
||||
|
||||
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`
|
||||
- 使用 `EasyExcelUtil.importExcel(...)` 的导入 Controller。
|
||||
|
||||
前端不需要改动。后端抛出的错误沿现有上传失败链路返回,页面展示现有错误提示即可。
|
||||
|
||||
## 测试设计
|
||||
|
||||
### 单元测试
|
||||
|
||||
新增工具层测试覆盖:
|
||||
|
||||
1. 带 `@DictDropdown` 且保留下拉数据验证的模板读取通过。
|
||||
2. 员工信息 `状态` 列缺少下拉数据验证时报错。
|
||||
3. 非 `LIST` 类型数据验证不能替代下拉框。
|
||||
4. 只覆盖部分实际数据行的下拉框应报错。
|
||||
5. 双 Sheet 中任一 Sheet 的字典下拉列缺失时报错。
|
||||
6. 无 `@DictDropdown` 字段的导入对象不触发结构校验。
|
||||
|
||||
### 样例文件验证
|
||||
|
||||
使用用户提供文件验证:
|
||||
|
||||
```text
|
||||
/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx
|
||||
```
|
||||
|
||||
预期:导入员工信息 Sheet 时提示 `状态` 列缺少下拉框。
|
||||
|
||||
### 真实页面验证
|
||||
|
||||
实现完成后按仓库规则使用 `browser-use` 打开真实员工信息维护页面验证:
|
||||
|
||||
1. 下载页面当前真实导入模板。
|
||||
2. 上传缺少下拉框的批量测试文件,确认页面提示导入失败。
|
||||
3. 上传保留下拉框的测试文件,确认能进入现有正常导入链路。
|
||||
4. 测试结束关闭本轮启动的前后端进程。
|
||||
|
||||
## 文档与实施记录
|
||||
|
||||
实现完成后新增实施记录:
|
||||
|
||||
```text
|
||||
docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md
|
||||
```
|
||||
|
||||
实施记录需包含:
|
||||
|
||||
- 修改内容。
|
||||
- 影响范围。
|
||||
- 测试命令与真实页面验证结果。
|
||||
- 用户提供缺下拉框文件的验证结果。
|
||||
@@ -0,0 +1,730 @@
|
||||
# Import Dropdown Validation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 上传 Excel 导入文件时,所有 `@DictDropdown` 字段对应列必须保留模板下拉框;缺失下拉框立即报错并阻止导入。
|
||||
|
||||
**Architecture:** 在 `EasyExcelUtil` 的公共读取入口统一增加模板结构校验,先用 POI 检查上传文件中对应 Sheet 的 `LIST` 数据验证覆盖情况,再交给 EasyExcel 执行现有数据读取。业务 Controller、异步导入、Redis 失败记录逻辑保持不变。
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 3, EasyExcel, Apache POI 4.1.2, JUnit 5, Mockito.
|
||||
|
||||
---
|
||||
|
||||
## Project Notes
|
||||
|
||||
- 当前工作区已有未提交改动,包含 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`。实施时必须先阅读当前 diff,保留已有 `templateWriter(...).inMemory(Boolean.TRUE)` 改动,不要回滚用户或其他任务留下的内容。
|
||||
- `.DS_Store` 忽略,不纳入任何暂存或提交。
|
||||
- 本计划只涉及后端;不新增前端代码。
|
||||
- 实现完成后必须新增实施记录:`docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md`。
|
||||
|
||||
## File Map
|
||||
|
||||
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`
|
||||
- 统一读取上传文件字节。
|
||||
- 解析 `@DictDropdown` 字段。
|
||||
- 使用 POI 校验 `LIST` 数据验证是否覆盖每个实际数据行。
|
||||
- 校验通过后继续调用 EasyExcel 读取数据。
|
||||
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java`
|
||||
- 覆盖缺失下拉框、非 LIST 验证、部分行覆盖、无字典字段绕过等工具层规则。
|
||||
- Create: `docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md`
|
||||
- 记录本次实现、影响范围、测试命令、用户文件验证和真实页面验证结果。
|
||||
- Reference only: `docs/plans/backend/2026-04-30-import-dropdown-validation-backend-design.md`
|
||||
- 已审查通过的设计文档。
|
||||
|
||||
## Task 1: Add Failing Utility Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java`
|
||||
|
||||
- [ ] **Step 1: Create test class skeleton**
|
||||
|
||||
Add this file with package/imports and helper methods:
|
||||
|
||||
```java
|
||||
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.Cell;
|
||||
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;
|
||||
```
|
||||
|
||||
Helper methods:
|
||||
|
||||
```java
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private static class PlainExcel {
|
||||
@ExcelProperty(value = "名称", index = 0)
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add tests for accepted and rejected workbooks**
|
||||
|
||||
Add these tests:
|
||||
|
||||
```java
|
||||
@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_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());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the new tests and confirm they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest test
|
||||
```
|
||||
|
||||
Expected before implementation:
|
||||
|
||||
- Tests for missing dropdown, non-LIST validation, and partial coverage fail because no structure validation exists.
|
||||
- The passing workbook test may pass already.
|
||||
|
||||
## Task 2: Implement Dropdown Structure Validation in EasyExcelUtil
|
||||
|
||||
**Files:**
|
||||
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`
|
||||
|
||||
- [ ] **Step 1: Add imports**
|
||||
|
||||
Add imports needed by the new helper methods:
|
||||
|
||||
```java
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.ruoyi.common.annotation.DictDropdown;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataValidation;
|
||||
import org.apache.poi.ss.usermodel.DataValidationConstraint;
|
||||
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.usermodel.WorkbookFactory;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
```
|
||||
|
||||
Keep existing imports that are still used, including the current `ExcelWriterBuilder` import from the existing working tree.
|
||||
|
||||
- [ ] **Step 2: Update importExcel(String fileName, Class<T>)**
|
||||
|
||||
Change the file-name overload to use a stream so the same validation path is used:
|
||||
|
||||
```java
|
||||
public static <T> List<T> importExcel(String fileName, Class<T> clazz) {
|
||||
try (InputStream inputStream = java.nio.file.Files.newInputStream(java.nio.file.Path.of(fileName))) {
|
||||
return importExcel(inputStream, clazz);
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("导入Excel失败", e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update InputStream import overloads**
|
||||
|
||||
Make both stream overloads read bytes once, validate, then pass a fresh `ByteArrayInputStream` to EasyExcel:
|
||||
|
||||
```java
|
||||
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz) {
|
||||
try {
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
validateDictDropdownTemplate(bytes, clazz, null);
|
||||
return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet().doReadSync();
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("导入Excel失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz, String sheetName) {
|
||||
try {
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
validateDictDropdownTemplate(bytes, clazz, sheetName);
|
||||
return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet(sheetName).doReadSync();
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("导入Excel失败", e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add validation helpers**
|
||||
|
||||
Add these private helpers near the bottom of `EasyExcelUtil`, before `templateWriter(...)`:
|
||||
|
||||
```java
|
||||
private static void validateDictDropdownTemplate(byte[] bytes, Class<?> clazz, String sheetName) {
|
||||
List<DropdownColumn> dropdownColumns = resolveDropdownColumns(clazz);
|
||||
if (dropdownColumns.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes))) {
|
||||
Sheet sheet = sheetName == null ? workbook.getSheetAt(0) : workbook.getSheet(sheetName);
|
||||
if (sheet == null) {
|
||||
return;
|
||||
}
|
||||
int lastDataRowIndex = findLastDataRowIndex(sheet);
|
||||
if (lastDataRowIndex < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (DropdownColumn column : dropdownColumns) {
|
||||
if (!isListValidationCovered(sheet, column.index(), lastDataRowIndex)) {
|
||||
throw new ServiceException(sheet.getSheetName() + " Sheet 的 " + column.title()
|
||||
+ " 列缺少下拉框,请下载最新导入模板填写后重新导入");
|
||||
}
|
||||
}
|
||||
} catch (ServiceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("导入Excel失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<DropdownColumn> resolveDropdownColumns(Class<?> clazz) {
|
||||
List<DropdownColumn> columns = new ArrayList<>();
|
||||
Class<?> current = clazz;
|
||||
while (current != null && current != Object.class) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (field.getAnnotation(DictDropdown.class) == null) {
|
||||
continue;
|
||||
}
|
||||
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||
if (excelProperty == null || excelProperty.index() < 0) {
|
||||
continue;
|
||||
}
|
||||
columns.add(new DropdownColumn(excelProperty.index(), resolveColumnTitle(field, excelProperty)));
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
columns.sort(Comparator.comparingInt(DropdownColumn::index));
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static String resolveColumnTitle(Field field, ExcelProperty excelProperty) {
|
||||
if (excelProperty.value().length > 0 && excelProperty.value()[0] != null && !excelProperty.value()[0].isBlank()) {
|
||||
return excelProperty.value()[0].replace("*", "");
|
||||
}
|
||||
return field.getName();
|
||||
}
|
||||
|
||||
private static int findLastDataRowIndex(Sheet sheet) {
|
||||
int lastDataRowIndex = -1;
|
||||
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (hasData(row)) {
|
||||
lastDataRowIndex = rowIndex;
|
||||
}
|
||||
}
|
||||
return lastDataRowIndex;
|
||||
}
|
||||
|
||||
private static boolean hasData(Row row) {
|
||||
if (row == null || row.getLastCellNum() < 0) {
|
||||
return false;
|
||||
}
|
||||
for (int cellIndex = row.getFirstCellNum(); cellIndex < row.getLastCellNum(); cellIndex++) {
|
||||
if (cellIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
Cell cell = row.getCell(cellIndex);
|
||||
if (cell != null && cell.toString() != null && !cell.toString().isBlank()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isListValidationCovered(Sheet sheet, int columnIndex, int lastDataRowIndex) {
|
||||
boolean[] coveredRows = new boolean[lastDataRowIndex + 1];
|
||||
for (DataValidation validation : sheet.getDataValidations()) {
|
||||
DataValidationConstraint constraint = validation.getValidationConstraint();
|
||||
if (constraint == null || constraint.getValidationType() != DataValidationConstraint.ValidationType.LIST) {
|
||||
continue;
|
||||
}
|
||||
for (CellRangeAddress address : validation.getRegions().getCellRangeAddresses()) {
|
||||
if (address.getFirstColumn() > columnIndex || address.getLastColumn() < columnIndex) {
|
||||
continue;
|
||||
}
|
||||
int firstRow = Math.max(1, address.getFirstRow());
|
||||
int lastRow = Math.min(lastDataRowIndex, address.getLastRow());
|
||||
for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
|
||||
coveredRows[rowIndex] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int rowIndex = 1; rowIndex <= lastDataRowIndex; rowIndex++) {
|
||||
if (hasData(sheet.getRow(rowIndex)) && !coveredRows[rowIndex]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private record DropdownColumn(int index, String title) {}
|
||||
```
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Do not add dictionary-value checks; this task only checks template dropdown structure.
|
||||
- Do not make Controller-specific changes.
|
||||
- Do not change existing async import status behavior.
|
||||
- If `sheet == null`, preserve current EasyExcel failure path rather than inventing a new fallback.
|
||||
|
||||
- [ ] **Step 5: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest test
|
||||
```
|
||||
|
||||
Expected: all tests in the new class pass.
|
||||
|
||||
## Task 3: Regression Tests for Existing Template Generation
|
||||
|
||||
**Files:**
|
||||
- Modify only if needed: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java`
|
||||
|
||||
- [ ] **Step 1: Run existing template tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest test
|
||||
```
|
||||
|
||||
Expected: existing template-generation tests pass. This confirms the validation change did not break dropdown generation and preserves the existing `inMemory(Boolean.TRUE)` template writer fix.
|
||||
|
||||
- [ ] **Step 2: Run controller tests that mock EasyExcelUtil**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-info-collection -am -Dtest=CcdiBaseStaffControllerTest,CcdiStaffFmyRelationControllerTest,CcdiAssetInfoControllerTest,CcdiBaseStaffAssetImportControllerTest test
|
||||
```
|
||||
|
||||
Expected: controller tests pass. This confirms method signatures and import entry points did not change.
|
||||
|
||||
## Task 4: Verify User-Provided Broken Workbook
|
||||
|
||||
**Files:**
|
||||
- No committed file changes.
|
||||
|
||||
- [ ] **Step 1: Confirm workbook structure manually**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
from openpyxl import load_workbook
|
||||
path = '/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx'
|
||||
wb = load_workbook(path)
|
||||
for ws in wb.worksheets:
|
||||
print(ws.title, len(ws.data_validations.dataValidation))
|
||||
PY
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
员工信息 0
|
||||
员工资产信息 0
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Validate through backend code path**
|
||||
|
||||
Use the real page upload in Task 6 as the authoritative backend-path validation. The expected page/API error is:
|
||||
|
||||
```text
|
||||
员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入
|
||||
```
|
||||
|
||||
Do not commit this user-provided workbook or any generated upload files.
|
||||
|
||||
## Task 5: Add Implementation Report
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md`
|
||||
|
||||
- [ ] **Step 1: Write implementation report**
|
||||
|
||||
Create the report with this structure:
|
||||
|
||||
```markdown
|
||||
# 导入模板下拉框结构校验实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 在 `EasyExcelUtil.importExcel(...)` 公共入口增加 `@DictDropdown` 列下拉框结构校验。
|
||||
- 上传文件中对应 Sheet 的对应列必须由 `LIST` 类型数据验证覆盖每个实际数据行。
|
||||
- 缺失下拉框时导入立即失败,不进入异步导入任务。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响所有使用 `EasyExcelUtil.importExcel(...)` 且导入对象含 `@DictDropdown` 字段的 Excel 导入。
|
||||
- 不影响无 `@DictDropdown` 字段的导入。
|
||||
- 不修改前端页面、业务字段校验、异步导入状态和失败记录逻辑。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest test`
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest test`
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiBaseStaffControllerTest,CcdiStaffFmyRelationControllerTest,CcdiAssetInfoControllerTest,CcdiBaseStaffAssetImportControllerTest test`
|
||||
|
||||
## 用户文件验证
|
||||
|
||||
- 文件:`/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx`
|
||||
- 结果:上传后提示 `员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入`
|
||||
|
||||
## 真实页面验证
|
||||
|
||||
- 页面:员工信息维护真实业务页面。
|
||||
- 结果:记录下载真实模板、上传缺下拉文件、上传保留下拉测试文件的页面验证结论。
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Fill actual verification output after tests**
|
||||
|
||||
Replace the verification bullets with actual pass/fail results after running commands and browser validation.
|
||||
|
||||
## Task 6: Real Page Validation With browser-use
|
||||
|
||||
**Files:**
|
||||
- No source file changes unless verification uncovers a bug.
|
||||
- Generated test files must stay under ignored output paths such as `output/browser-use/` or `output/spreadsheet/` and must not be committed.
|
||||
|
||||
- [ ] **Step 1: Use browser-use skill**
|
||||
|
||||
Before browser work, open `/Users/wkc/.codex/plugins/cache/openai-bundled/browser-use/0.1.0-alpha1/skills/browser/SKILL.md` and follow it.
|
||||
|
||||
- [ ] **Step 2: Start backend using project script if needed**
|
||||
|
||||
If no backend is running or code changes require restart, run:
|
||||
|
||||
```bash
|
||||
sh bin/restart_java_backend.sh
|
||||
```
|
||||
|
||||
Expected: backend available at `http://localhost:62318`.
|
||||
|
||||
- [ ] **Step 3: Start frontend with nvm if needed**
|
||||
|
||||
If no frontend is running, run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
nvm use
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Expected: frontend dev server URL printed by Vite/Vue CLI. Keep the process id/session so it can be stopped after testing.
|
||||
|
||||
- [ ] **Step 4: Test broken workbook on real page**
|
||||
|
||||
In the real employee information maintenance page:
|
||||
|
||||
1. Log in through the real app or `/login/test` shortcut if already used by the project.
|
||||
2. Open employee information maintenance.
|
||||
3. Upload `/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx`.
|
||||
4. Confirm the page displays the backend error mentioning `状态 列缺少下拉框`.
|
||||
|
||||
Expected: no async import task is created for this upload.
|
||||
|
||||
- [ ] **Step 5: Test current real template still works**
|
||||
|
||||
1. Download the current import template from the real page.
|
||||
2. Fill a small test workbook while preserving dropdown validations.
|
||||
3. Upload it and confirm it enters the existing normal import chain.
|
||||
4. Clean up any successfully imported test data.
|
||||
|
||||
Expected: dropdown validation does not block a valid template.
|
||||
|
||||
- [ ] **Step 6: Stop test processes**
|
||||
|
||||
Stop any backend/frontend process started during this task. Do not stop unrelated user-owned processes.
|
||||
|
||||
## Task 7: Final Verification and Commit Hygiene
|
||||
|
||||
**Files:**
|
||||
- Modify: files from previous tasks only.
|
||||
|
||||
- [ ] **Step 1: Check worktree and staged state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git diff --cached --name-status
|
||||
```
|
||||
|
||||
Expected: no staged unrelated files. `.DS_Store` remains ignored/uncommitted.
|
||||
|
||||
- [ ] **Step 2: Run final focused verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest,EasyExcelUtilTemplateTest,CcdiBaseStaffControllerTest,CcdiStaffFmyRelationControllerTest,CcdiAssetInfoControllerTest,CcdiBaseStaffAssetImportControllerTest test
|
||||
```
|
||||
|
||||
Expected: all selected tests pass.
|
||||
|
||||
- [ ] **Step 3: Stage only this task's files**
|
||||
|
||||
Stage only these files:
|
||||
|
||||
```bash
|
||||
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java
|
||||
git add ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilImportDropdownValidationTest.java
|
||||
git add docs/reports/implementation/2026-04-30-import-dropdown-validation-implementation.md
|
||||
```
|
||||
|
||||
If `EasyExcelUtil.java` still contains unrelated pre-existing edits that should not be committed, use partial staging or stop and ask the user before committing.
|
||||
|
||||
- [ ] **Step 4: Review staged diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --cached --name-status
|
||||
git diff --cached --stat
|
||||
```
|
||||
|
||||
Expected: only the implementation files and implementation report are staged.
|
||||
|
||||
- [ ] **Step 5: Commit if requested**
|
||||
|
||||
If the user wants a commit, use a Chinese message:
|
||||
|
||||
```bash
|
||||
git commit -m "新增导入模板下拉框校验"
|
||||
```
|
||||
|
||||
Expected: commit succeeds without `.DS_Store` or unrelated docs/files.
|
||||
@@ -0,0 +1,671 @@
|
||||
# Bank Upload Original Filename Implementation Plan
|
||||
|
||||
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow the CCDI project rule: unless the user explicitly declares `using-superpowers` for implementation, execute this plan through the ordinary workflow and do not enable subagents by default.
|
||||
|
||||
**Goal:** 上传本地流水文件后,页面记录名和转传流水分析平台 multipart 文件名都保持用户初始上传文件名。
|
||||
|
||||
**Architecture:** 后端继续使用唯一临时文件名保存文件,避免同名和并发冲突;上传流水分析平台时额外传入原始文件名,并用可覆盖 `Resource#getFilename()` 的资源对象构造 multipart 文件 part。上传记录状态后处理增加“是否保留当前记录文件名”的来源参数,本地上传链路保留初始文件名,拉取本行信息链路保持现状。
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 3, RestTemplate, MyBatis Plus, JUnit 5, Mockito, Vue 2 页面真实验证。
|
||||
|
||||
---
|
||||
|
||||
## Project Notes
|
||||
|
||||
- 设计文档:[docs/superpowers/specs/2026-05-06-bank-upload-original-filename-design.md](/Users/wkc/Desktop/ccdi/ccdi/docs/superpowers/specs/2026-05-06-bank-upload-original-filename-design.md)
|
||||
- 项目路径规则覆盖 `writing-plans` 默认目录:本次只涉及后端源码,因此实施计划放在 `docs/plans/backend/`。
|
||||
- 本次不新增前端源码,不新增数据库字段,不回改历史上传记录。
|
||||
- `.DS_Store` 忽略,不纳入暂存或提交。
|
||||
- 完成实现后必须新增实施记录:`docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md`。
|
||||
- 真实页面验证使用 @browser-use:browser 打开实际业务页面,不打开 prototype 页面。
|
||||
- 如果启动了后端、前端或 `lsfx-mock-server`,测试结束后必须关闭本轮启动的进程。
|
||||
|
||||
## File Map
|
||||
|
||||
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java`
|
||||
- 新增可指定 multipart filename 的 `NamedFileSystemResource`。
|
||||
- `uploadFile` 继续支持 `File` 参数;当参数已经是 `Resource` 时直接加入 multipart body。
|
||||
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`
|
||||
- 新增 `uploadFile(Integer groupId, File file, String uploadFileName)`。
|
||||
- 现有 `uploadFile(Integer groupId, File file)` 委托到新方法并使用 `file.getName()`。
|
||||
- 项目上传链路使用新方法传入初始上传文件名。
|
||||
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java`
|
||||
- 验证 multipart body 中 `files` 资源的 `filename` 可被显式指定。
|
||||
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java`
|
||||
- 验证流水分析客户端上传时把指定原始文件名写入 `files` 参数。
|
||||
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
|
||||
- `processFileAsync` 调用 `lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName())`。
|
||||
- `processRecordAfterLogIdReady` 增加来源控制参数,本地上传不覆盖 `record.fileName`,拉取本行信息保持现有覆盖行为。
|
||||
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
|
||||
- 更新现有 `processFileAsync` 上传 stubbing 到三参数签名。
|
||||
- 新增本地上传使用原始文件名转传、平台返回名不覆盖上传记录、拉取本行信息保持现状的回归测试。
|
||||
- Create: `docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md`
|
||||
- 记录修改内容、影响范围、测试命令、真实页面验证和进程清理情况。
|
||||
|
||||
## Task 1: Add Failing Tests For Multipart Filename
|
||||
|
||||
**Files:**
|
||||
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java`
|
||||
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java`
|
||||
|
||||
- [ ] **Step 1: Create `HttpUtilTest` with a failing filename assertion**
|
||||
|
||||
Add:
|
||||
|
||||
```java
|
||||
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());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create `LsfxAnalysisClientTest` with a failing client assertion**
|
||||
|
||||
Add:
|
||||
|
||||
```java
|
||||
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.any;
|
||||
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));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests and confirm they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-lsfx -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because `HttpUtil.namedFileResource(...)` and `LsfxAnalysisClient.uploadFile(Integer, File, String)` do not exist yet.
|
||||
|
||||
## Task 2: Implement Multipart Filename Support
|
||||
|
||||
**Files:**
|
||||
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java`
|
||||
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`
|
||||
|
||||
- [ ] **Step 1: Add named file resource to `HttpUtil`**
|
||||
|
||||
In `HttpUtil.java`, add only this import:
|
||||
|
||||
```java
|
||||
import org.springframework.util.StringUtils;
|
||||
```
|
||||
|
||||
Do not import `org.springframework.core.io.Resource` in this file because it conflicts with the existing `jakarta.annotation.Resource` annotation import. Use the Spring Resource type by fully qualified name in implementation snippets.
|
||||
|
||||
Add this nested class and factory method inside `HttpUtil`:
|
||||
|
||||
```java
|
||||
public static org.springframework.core.io.Resource namedFileResource(File file, String filename) {
|
||||
return new NamedFileSystemResource(file, filename);
|
||||
}
|
||||
|
||||
private static class NamedFileSystemResource extends FileSystemResource {
|
||||
private final String filename;
|
||||
|
||||
NamedFileSystemResource(File file, String filename) {
|
||||
super(file);
|
||||
this.filename = StringUtils.hasText(filename) ? filename : file.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Let `HttpUtil.uploadFile` accept existing `Resource` values**
|
||||
|
||||
Replace the file branch in `uploadFile(...)` with:
|
||||
|
||||
```java
|
||||
if (value instanceof File) {
|
||||
File file = (File) value;
|
||||
body.add(key, new FileSystemResource(file));
|
||||
} else if (value instanceof org.springframework.core.io.Resource) {
|
||||
body.add(key, value);
|
||||
} else {
|
||||
body.add(key, value);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add explicit filename upload overload to `LsfxAnalysisClient`**
|
||||
|
||||
Replace the current upload method body with a delegating overload:
|
||||
|
||||
```java
|
||||
public UploadFileResponse uploadFile(Integer groupId, File file) {
|
||||
return uploadFile(groupId, file, file.getName());
|
||||
}
|
||||
|
||||
public UploadFileResponse uploadFile(Integer groupId, File file, String uploadFileName) {
|
||||
String multipartFileName = org.springframework.util.StringUtils.hasText(uploadFileName)
|
||||
? uploadFileName
|
||||
: file.getName();
|
||||
log.info("【流水分析】上传文件请求: groupId={}, fileName={}", groupId, multipartFileName);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
String url = baseUrl + uploadFileEndpoint;
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("groupId", groupId);
|
||||
params.put("files", HttpUtil.namedFileResource(file, multipartFileName));
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
UploadFileResponse response = httpUtil.uploadFile(url, params, headers, UploadFileResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
log.info("【流水分析】上传文件成功: uploadStatus={}, 耗时={}ms",
|
||||
response.getData().getUploadStatus(), elapsed);
|
||||
} else {
|
||||
log.warn("【流水分析】上传文件响应异常: 耗时={}ms", elapsed);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (LsfxApiException e) {
|
||||
log.error("【流水分析】上传文件失败: groupId={}, error={}", groupId, e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("【流水分析】上传文件未知异常: groupId={}", groupId, e);
|
||||
throw new LsfxApiException("上传文件失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run `ccdi-lsfx` tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-lsfx -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest,CreditParseControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit lsfx client changes**
|
||||
|
||||
Check the worktree, stage only task files, then verify staged scope:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java \
|
||||
ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java \
|
||||
ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java \
|
||||
ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java
|
||||
git diff --cached --name-status
|
||||
```
|
||||
|
||||
Expected staged files: only the four `ccdi-lsfx` files in this task.
|
||||
|
||||
```bash
|
||||
git commit -m "修复流水分析上传文件名传递"
|
||||
```
|
||||
|
||||
## Task 3: Add Failing Project Upload Service Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
|
||||
|
||||
- [ ] **Step 1: Update existing `processFileAsync` stubs to the new signature**
|
||||
|
||||
In `CcdiFileUploadServiceImplTest.java`, replace local file upload stubs:
|
||||
|
||||
```java
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```java
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
```
|
||||
|
||||
Only update tests that exercise `processFileAsync`. Do not change `processPullBankInfoAsync` tests to upload files; that chain uses `fetchInnerFlow`.
|
||||
|
||||
- [ ] **Step 2: Add test that local upload calls LSFX with original record filename**
|
||||
|
||||
Add:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void processFileAsync_shouldUploadToLsfxWithOriginalRecordFileName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("原始流水.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)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
|
||||
verify(lsfxClient).uploadFile(eq(LSFX_PROJECT_ID), argThat(file ->
|
||||
file.getName().startsWith("upload-") && file.getName().endsWith(".xlsx")
|
||||
), eq("原始流水.xlsx"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add test that platform filename does not overwrite local upload record filename**
|
||||
|
||||
Add:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void processFileAsync_shouldKeepOriginalFileNameWhenStatusReturnsDifferentName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any()))
|
||||
.thenReturn(buildParsedSuccessStatusResponse("平台返回文件名.xlsx"));
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
"parsed_success".equals(item.getFileStatus())
|
||||
&& "原始流水.xlsx".equals(item.getFileName()))
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Keep pull-bank-info regression explicit**
|
||||
|
||||
Keep the existing test `processPullBankInfoAsync_shouldUpdateFileSizeFromStatusResponse` asserting:
|
||||
|
||||
```java
|
||||
"XX身份证.xlsx".equals(item.getFileName())
|
||||
```
|
||||
|
||||
This verifies the shared status method still allows platform filename overwrite for the “拉取本行信息” chain.
|
||||
|
||||
- [ ] **Step 5: Add failure-state filename regression**
|
||||
|
||||
Add:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void processFileAsync_shouldKeepOriginalFileNameWhenParseStatusFails() throws IOException {
|
||||
GetFileUploadStatusResponse statusResponse = buildParsedSuccessStatusResponse("平台失败文件名.xlsx");
|
||||
GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0);
|
||||
logItem.setStatus(-1);
|
||||
logItem.setUploadStatusDesc("parse.failed");
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(statusResponse);
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
"parsed_failed".equals(item.getFileStatus())
|
||||
&& "原始流水.xlsx".equals(item.getFileName()))
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run service tests and confirm expected failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because `processFileAsync` still calls the old two-argument upload method and status handling still overwrites `record.fileName`.
|
||||
|
||||
## Task 4: Implement Project Upload Filename Isolation
|
||||
|
||||
**Files:**
|
||||
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
|
||||
|
||||
- [ ] **Step 1: Use original record filename when forwarding local uploaded files**
|
||||
|
||||
In `processFileAsync`, replace:
|
||||
|
||||
```java
|
||||
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file);
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```java
|
||||
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName());
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add source control to status post-processing**
|
||||
|
||||
Add an overload:
|
||||
|
||||
```java
|
||||
private void processRecordAfterLogIdReady(Long projectId, Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record, Integer logId) {
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false);
|
||||
}
|
||||
```
|
||||
|
||||
Change the current method signature to:
|
||||
|
||||
```java
|
||||
private void processRecordAfterLogIdReady(Long projectId, Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record, Integer logId,
|
||||
boolean preserveRecordFileName) {
|
||||
```
|
||||
|
||||
Then replace the filename update block with:
|
||||
|
||||
```java
|
||||
if (!preserveRecordFileName) {
|
||||
String fileName = StringUtils.hasText(logItem.getUploadFileName())
|
||||
? logItem.getUploadFileName()
|
||||
: logItem.getDownloadFileName();
|
||||
if (StringUtils.hasText(fileName)) {
|
||||
record.setFileName(fileName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Call status post-processing with preservation from local upload**
|
||||
|
||||
In `processFileAsync`, replace:
|
||||
|
||||
```java
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId);
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```java
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, true);
|
||||
```
|
||||
|
||||
Do not change `processPullBankInfoAsync`; it should continue to call the four-argument overload and preserve the current pull-bank-info behavior.
|
||||
|
||||
- [ ] **Step 4: Run service tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit project upload service changes**
|
||||
|
||||
Check the worktree, stage only task files, then verify staged scope:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java \
|
||||
ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
|
||||
git diff --cached --name-status
|
||||
```
|
||||
|
||||
Expected staged files: only `CcdiFileUploadServiceImpl.java` and `CcdiFileUploadServiceImplTest.java`.
|
||||
|
||||
```bash
|
||||
git commit -m "修复本地上传流水记录文件名覆盖"
|
||||
```
|
||||
|
||||
## Task 5: Verification, Real Page Check, And Implementation Record
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md`
|
||||
- Reference: `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- Reference: `lsfx-mock-server/routers/api.py`
|
||||
- Reference: `lsfx-mock-server/services/file_service.py`
|
||||
|
||||
- [ ] **Step 1: Run focused backend regression tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-lsfx,ccdi-project -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest,CcdiFileUploadServiceImplTest,CcdiFileUploadControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run compile check for affected modules**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-lsfx,ccdi-project -am -DskipTests compile
|
||||
```
|
||||
|
||||
Expected: BUILD SUCCESS.
|
||||
|
||||
- [ ] **Step 3: Start verification services**
|
||||
|
||||
Use the project backend restart script:
|
||||
|
||||
```bash
|
||||
sh bin/restart_java_backend.sh
|
||||
```
|
||||
|
||||
For the mock service, use local dev defaults:
|
||||
|
||||
```bash
|
||||
cd lsfx-mock-server
|
||||
python3 main.py --rule-hit-mode subset
|
||||
```
|
||||
|
||||
If the frontend is not already running, start it with the project Node version:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
nvm use
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Perform real page upload check with @browser-use:browser**
|
||||
|
||||
In the actual business page:
|
||||
|
||||
1. Login through the real application.
|
||||
2. Open project detail -> 上传数据.
|
||||
3. Upload a test file named with a distinctive original name, for example `原始文件名验证-20260506.xlsx`.
|
||||
4. Confirm the upload record table displays `原始文件名验证-20260506.xlsx`.
|
||||
5. Confirm the mock LSFX upload response or service log records the same filename. The mock service receives the filename through FastAPI `UploadFile.filename`, and `FileService.upload_file` writes it into `file_record.file_name`.
|
||||
|
||||
Expected: page table filename and mock LSFX received filename both match the original uploaded filename.
|
||||
|
||||
- [ ] **Step 5: Clean test data and stop started processes**
|
||||
|
||||
Remove the uploaded test record through the existing page delete action if it reached parsed success. If test data was created but cannot be removed from the page, clean only the test row by its unique filename or upload record id after confirming the scope.
|
||||
|
||||
Stop only the backend, frontend, or mock-service processes started in Step 3.
|
||||
|
||||
- [ ] **Step 6: Write implementation record**
|
||||
|
||||
Create `docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md` with:
|
||||
|
||||
```markdown
|
||||
# 上传流水文件原始文件名保持实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- `ccdi-lsfx` 支持 multipart 文件 part 显式指定 filename。
|
||||
- `ccdi-project` 本地上传流水文件链路转传流水分析平台时使用初始上传文件名。
|
||||
- 本地上传链路查询状态后不再使用平台返回文件名覆盖上传记录文件名。
|
||||
- 拉取本行信息链路保持原有文件名处理行为。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响项目详情“上传数据”中的本地流水文件上传。
|
||||
- 不影响历史上传记录。
|
||||
- 不影响拉取本行信息。
|
||||
- 不涉及前端源码和数据库结构变更。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- `mvn -pl ccdi-lsfx,ccdi-project -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest,CcdiFileUploadServiceImplTest,CcdiFileUploadControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- `mvn -pl ccdi-lsfx,ccdi-project -am -DskipTests compile`
|
||||
- 真实页面验证:记录页面文件名、mock LSFX 接收 filename、测试数据清理和进程关闭结果。
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit final verification record**
|
||||
|
||||
Check the worktree, stage only the implementation record, then verify staged scope:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md
|
||||
git diff --cached --name-status
|
||||
```
|
||||
|
||||
Expected staged file: only the implementation record.
|
||||
|
||||
```bash
|
||||
git commit -m "文档: 记录上传流水文件名修复验证"
|
||||
```
|
||||
|
||||
## Final Review Checklist
|
||||
|
||||
- [ ] 本地上传流水文件转传 LSFX 时 multipart `files` part 的 filename 是初始上传文件名。
|
||||
- [ ] 本地上传记录 `file_name` 在解析成功或失败后仍是初始上传文件名。
|
||||
- [ ] 拉取本行信息链路仍按平台返回文件名更新记录,不被本次改动影响。
|
||||
- [ ] 无数据库结构变更。
|
||||
- [ ] 无前端源码变更。
|
||||
- [ ] 实施记录已包含测试、真实页面验证和进程清理结果。
|
||||
- [ ] 提交前 `git status --short` 无 `.DS_Store` 或无关文件。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,236 @@
|
||||
# 员工资产导入与员工亲属实体自动补入后端解决方案
|
||||
|
||||
## 背景
|
||||
|
||||
本方案针对以下两个已验证问题:
|
||||
|
||||
1. 【员工信息维护】和【员工亲属关系维护】使用双 Sheet 模板导入时,第二个 Sheet 的资产信息只按数据库已有主数据反查归属,不能识别同一个导入文件中刚导入的员工或亲属关系,导致提示“未找到资产归属员工”等错误。
|
||||
2. 【员工亲属实体关联】新增或导入关系人与企业关联后,只写入员工亲属实体关联表,没有同步生成实体库数据,导致企业名称和统一社会信用代码不能自动落入【实体库管理】。
|
||||
|
||||
本次解决方案只涉及后端,不调整前端页面结构、上传入口和模板样式。现有前端已经能接收员工任务 ID 与资产任务 ID,并分别轮询失败记录,因此后端需要保持现有返回字段兼容。
|
||||
|
||||
## 目标
|
||||
|
||||
- 双 Sheet 导入时,主 Sheet 和资产 Sheet 按同一个文件内的业务依赖顺序处理。
|
||||
- 资产 Sheet 可以关联到数据库已存在的员工或亲属主数据,也可以关联到同一文件中本轮成功导入的员工或亲属主数据。
|
||||
- 主 Sheet 校验失败的数据不能作为资产归属依据。
|
||||
- 员工亲属实体关联新增和导入成功后,缺失的企业自动补入实体库。
|
||||
- 实体库自动补入只插入缺失企业,不更新已存在实体。
|
||||
|
||||
## 方案一:员工信息维护双 Sheet 导入
|
||||
|
||||
### 当前问题链路
|
||||
|
||||
- `/ccdi/baseStaff/importData` 当前分别读取 `员工信息` 与 `员工资产信息` 两个 Sheet。
|
||||
- 有员工数据时调用 `baseStaffService.importBaseStaff(staffList)`,有资产数据时调用 `baseStaffAssetImportService.importAssetInfo(assetList)`。
|
||||
- 两个任务独立异步执行,员工资产任务只通过 `ccdi_base_staff.id_card` 查询数据库已有员工。
|
||||
- 当模板中员工和资产同时首次导入时,资产任务无法稳定看到本轮员工导入结果。
|
||||
|
||||
### 改造策略
|
||||
|
||||
保留接口路径、模板名称、返回结构和前端轮询模型,新增后端双 Sheet 编排能力。
|
||||
|
||||
1. 在员工导入服务中增加双 Sheet 提交方法:
|
||||
- 输入:`List<CcdiBaseStaffExcel> staffList`、`List<CcdiBaseStaffAssetInfoExcel> assetList`
|
||||
- 输出:`BaseStaffImportSubmitResultVO`
|
||||
- 有员工 Sheet 时生成 `staffTaskId`
|
||||
- 有资产 Sheet 时生成 `assetTaskId`
|
||||
|
||||
2. 新增一个统一异步编排方法,按顺序执行:
|
||||
- 初始化员工导入任务状态
|
||||
- 执行员工主数据校验和批量插入
|
||||
- 收集本轮员工导入成功的 `idCard`
|
||||
- 更新员工任务状态和失败记录
|
||||
- 初始化员工资产导入任务状态
|
||||
- 执行员工资产导入
|
||||
- 员工资产归属候选来源为:
|
||||
- 数据库已有 `ccdi_base_staff.id_card`
|
||||
- 本轮员工 Sheet 成功导入的 `idCard`
|
||||
- 更新员工资产任务状态和失败记录
|
||||
|
||||
3. 员工资产导入逻辑调整:
|
||||
- 保留“员工资产只允许员工本人身份证号”的业务规则。
|
||||
- 保留重复校验规则:`personId + assetMainType + assetSubType + assetName`。
|
||||
- `personId` 既可以命中数据库已有员工,也可以命中本轮成功导入员工。
|
||||
- 只命中员工 Sheet 失败行、且数据库中也不存在该身份证号时,资产行进入失败记录。
|
||||
|
||||
4. `/ccdi/baseStaff/importData` 改为调用新的双 Sheet 提交方法,不再由 Controller 分别提交两个互相独立的异步任务。
|
||||
|
||||
### 结果状态
|
||||
|
||||
- 只填员工 Sheet:只返回 `staffTaskId`,行为保持不变。
|
||||
- 只填员工资产 Sheet:只返回 `assetTaskId`,按数据库已有员工校验。
|
||||
- 两个 Sheet 都填写:返回 `staffTaskId` 与 `assetTaskId`,但后端在同一个编排任务中先处理员工再处理资产。
|
||||
|
||||
## 方案二:员工亲属关系维护双 Sheet 导入
|
||||
|
||||
### 当前问题链路
|
||||
|
||||
- `/ccdi/staffFmyRelation/importData` 当前分别读取 `员工亲属关系信息` 与 `亲属资产信息` 两个 Sheet。
|
||||
- 有亲属关系数据时调用 `relationService.importRelation(relationList)`,有亲属资产数据时调用 `assetInfoImportService.importAssetInfo(assetList)`。
|
||||
- 亲属资产导入只通过 `ccdi_staff_fmy_relation.relation_cert_no` 查询数据库已有亲属关系。
|
||||
- 当模板中亲属关系和亲属资产同时首次导入时,资产任务无法稳定看到本轮亲属关系导入结果。
|
||||
|
||||
### 改造策略
|
||||
|
||||
保留接口路径、模板名称、返回结构和前端轮询模型,新增员工亲属关系双 Sheet 编排能力。
|
||||
|
||||
1. 在员工亲属关系导入服务中增加双 Sheet 提交方法:
|
||||
- 输入:`List<CcdiStaffFmyRelationExcel> relationList`、`List<CcdiAssetInfoExcel> assetList`
|
||||
- 输出:`StaffFmyRelationImportSubmitResultVO`
|
||||
- 有亲属关系 Sheet 时生成 `relationTaskId`
|
||||
- 有亲属资产 Sheet 时生成 `assetTaskId`
|
||||
|
||||
2. 新增一个统一异步编排方法,按顺序执行:
|
||||
- 初始化亲属关系导入任务状态
|
||||
- 执行亲属关系校验和批量插入
|
||||
- 收集本轮成功导入且有效的亲属关系映射:
|
||||
- `relationCertNo` 作为资产 Sheet 的 `personId`
|
||||
- `personId` 作为资产落库的 `familyId`
|
||||
- 更新亲属关系任务状态和失败记录
|
||||
- 初始化亲属资产导入任务状态
|
||||
- 执行亲属资产导入
|
||||
- 亲属资产归属候选来源为:
|
||||
- 数据库已有有效员工亲属关系
|
||||
- 本轮亲属关系 Sheet 成功导入的有效员工亲属关系
|
||||
- 更新亲属资产任务状态和失败记录
|
||||
|
||||
3. 亲属资产导入逻辑调整:
|
||||
- 保留 `family_id = 员工身份证号`、`person_id = 亲属身份证号` 的落库规则。
|
||||
- `personId` 命中唯一亲属关系时导入成功。
|
||||
- `personId` 未命中数据库和本轮成功亲属关系时,失败原因为“未找到亲属资产归属员工”。
|
||||
- `personId` 命中多个员工归属时,失败原因为“亲属资产归属员工不唯一”。
|
||||
- 只命中亲属关系 Sheet 失败行、且数据库中也不存在有效亲属关系时,资产行进入失败记录。
|
||||
|
||||
4. `/ccdi/staffFmyRelation/importData` 改为调用新的双 Sheet 提交方法,不再由 Controller 分别提交两个互相独立的异步任务。
|
||||
|
||||
### 结果状态
|
||||
|
||||
- 只填亲属关系 Sheet:只返回 `relationTaskId`,行为保持不变。
|
||||
- 只填亲属资产 Sheet:只返回 `assetTaskId`,按数据库已有亲属关系校验。
|
||||
- 两个 Sheet 都填写:返回 `relationTaskId` 与 `assetTaskId`,但后端在同一个编排任务中先处理亲属关系再处理亲属资产。
|
||||
|
||||
## 方案三:员工亲属实体关联自动补入实体库
|
||||
|
||||
### 当前问题链路
|
||||
|
||||
- 员工亲属实体关联新增只写入 `ccdi_staff_enterprise_relation`。
|
||||
- 员工亲属实体关联导入成功行也只写入 `ccdi_staff_enterprise_relation`。
|
||||
- 当前源码中没有可复用的实体库自动补入服务实现。
|
||||
|
||||
### 改造策略
|
||||
|
||||
新增后端内部服务 `EnterpriseAutoFillService`,统一处理缺失企业的最小插入。
|
||||
|
||||
1. 新增服务文件:
|
||||
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillService.java`
|
||||
|
||||
2. 服务方法:
|
||||
- `ensureExists(EnterpriseFillItem item)`
|
||||
- `ensureExistsBatch(List<EnterpriseFillItem> items)`
|
||||
|
||||
3. `EnterpriseFillItem` 字段:
|
||||
- `socialCreditCode`
|
||||
- `enterpriseName`
|
||||
- `entSource`
|
||||
- `dataSource`
|
||||
- `userName`
|
||||
|
||||
4. 插入规则:
|
||||
- 按 `socialCreditCode` 去重。
|
||||
- 先批量查询 `ccdi_enterprise_base_info` 已存在记录。
|
||||
- 已存在实体不更新。
|
||||
- 缺失实体最小插入:
|
||||
- `social_credit_code = 统一社会信用代码`
|
||||
- `enterprise_name = 企业名称`
|
||||
- `ent_source = EMP_RELATION`
|
||||
- `data_source = MANUAL` 或 `IMPORT`
|
||||
- `risk_level = NULL`
|
||||
- `create_by/update_by = 当前用户`
|
||||
- 不引入额外兜底字段,不改变实体库手工新增规则。
|
||||
|
||||
5. 接入员工亲属实体关联新增:
|
||||
- 在 `CcdiStaffEnterpriseRelationServiceImpl#insertRelation` 中,亲属有效性校验和组合查重通过后,写关联表前调用:
|
||||
- `entSource = EnterpriseSource.EMP_RELATION.getCode()`
|
||||
- `dataSource = DataSource.MANUAL.getCode()`
|
||||
|
||||
6. 接入员工亲属实体关联导入:
|
||||
- 在 `CcdiStaffEnterpriseRelationImportServiceImpl#importRelationAsync` 中,只对校验成功并即将插入的 `newRecords` 组装补入列表。
|
||||
- 批量写关联表前调用 `ensureExistsBatch`。
|
||||
- 失败行不进入实体库补入集合。
|
||||
- `dataSource = DataSource.IMPORT.getCode()`。
|
||||
|
||||
## 测试方案
|
||||
|
||||
### 单元测试
|
||||
|
||||
1. 员工信息维护双 Sheet:
|
||||
- 同一模板中员工 Sheet 新增员工,员工资产 Sheet 使用该员工身份证号,资产应导入成功。
|
||||
- 员工 Sheet 行校验失败,资产 Sheet 使用该失败员工身份证号且数据库不存在,资产应失败。
|
||||
- 只导资产 Sheet,数据库已有员工时资产成功。
|
||||
- 只导资产 Sheet,数据库无员工时资产失败。
|
||||
- 资产重复命中数据库或当前文件重复时失败。
|
||||
|
||||
2. 员工亲属关系维护双 Sheet:
|
||||
- 同一模板中亲属关系 Sheet 新增亲属,亲属资产 Sheet 使用该亲属证件号,资产应导入成功,并落库 `family_id = 员工身份证号`。
|
||||
- 亲属关系 Sheet 行校验失败,亲属资产 Sheet 使用该失败亲属证件号且数据库不存在,资产应失败。
|
||||
- 只导亲属资产 Sheet,数据库已有唯一有效亲属关系时资产成功。
|
||||
- 只导亲属资产 Sheet,数据库不存在亲属关系时资产失败。
|
||||
- 同一亲属证件号命中多个员工归属时资产失败。
|
||||
|
||||
3. 员工亲属实体关联自动补入:
|
||||
- 手工新增员工亲属实体关联成功时,缺失企业自动插入实体库,来源为 `EMP_RELATION`,数据来源为 `MANUAL`。
|
||||
- 手工新增时实体库已存在该统一社会信用代码,不更新实体库旧记录。
|
||||
- 导入成功行自动插入实体库,来源为 `EMP_RELATION`,数据来源为 `IMPORT`。
|
||||
- 导入失败行不插入实体库。
|
||||
- 同一批多个成功行引用同一统一社会信用代码,只补入一次。
|
||||
|
||||
### 接口验证
|
||||
|
||||
1. 调用 `/ccdi/baseStaff/importTemplate` 下载真实模板,构造:
|
||||
- `员工信息` 新员工
|
||||
- `员工资产信息` 引用该员工身份证号
|
||||
- 上传 `/ccdi/baseStaff/importData`
|
||||
- 轮询员工任务与资产任务,确认都成功
|
||||
- 查询 `ccdi_base_staff` 与 `ccdi_asset_info`,确认员工和资产落库
|
||||
|
||||
2. 调用 `/ccdi/staffFmyRelation/importTemplate` 下载真实模板,构造:
|
||||
- `员工亲属关系信息` 新亲属
|
||||
- `亲属资产信息` 引用该亲属证件号
|
||||
- 上传 `/ccdi/staffFmyRelation/importData`
|
||||
- 轮询亲属关系任务与亲属资产任务,确认都成功
|
||||
- 查询 `ccdi_staff_fmy_relation` 与 `ccdi_asset_info`,确认亲属关系和资产落库
|
||||
|
||||
3. 调用员工亲属实体关联新增接口:
|
||||
- 新增前确认 `ccdi_enterprise_base_info` 不存在该统一社会信用代码
|
||||
- 新增关联成功后查询实体库,确认自动生成企业记录
|
||||
- 校验 `ent_source = EMP_RELATION`
|
||||
|
||||
### 页面验证
|
||||
|
||||
完成后需要使用真实业务页面验证:
|
||||
|
||||
1. 【员工信息维护】页面下载模板,按模板填写员工和员工资产,上传后检查任务状态、失败记录和列表详情。
|
||||
2. 【员工亲属关系维护】页面下载模板,按模板填写亲属关系和亲属资产,上传后检查任务状态、失败记录和详情资产列表。
|
||||
3. 【员工亲属实体关联】页面新增关系人与企业关联后,进入【实体库管理】查询该统一社会信用代码,确认企业已自动生成且来源显示为员工关系人。
|
||||
|
||||
测试结束后清理本轮新增员工、亲属关系、资产、员工亲属实体关联和自动补入实体库数据,并关闭测试过程中启动的前后端进程。
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 抽取员工导入和亲属关系导入的可复用执行方法,返回成功主数据上下文和失败记录。
|
||||
2. 新增员工信息维护双 Sheet 后端编排方法,接入 Controller。
|
||||
3. 新增员工亲属关系维护双 Sheet 后端编排方法,接入 Controller。
|
||||
4. 新增 `EnterpriseAutoFillService`。
|
||||
5. 员工亲属实体关联新增链路接入实体库自动补入。
|
||||
6. 员工亲属实体关联导入链路接入实体库自动补入。
|
||||
7. 补充单元测试。
|
||||
8. 执行接口验证和真实页面验证。
|
||||
9. 新增实施记录到 `docs/reports/implementation/`。
|
||||
|
||||
## 风险点
|
||||
|
||||
- 双 Sheet 导入不能继续使用两个互相独立的异步任务,否则仍然存在主数据与资产任务执行顺序不确定的问题。
|
||||
- 资产归属上下文必须只使用“数据库已有数据”和“本轮主 Sheet 成功数据”,不能把失败主数据作为资产归属。
|
||||
- 实体库自动补入不能更新已存在企业,避免覆盖人工维护的企业名称、风险等级和来源信息。
|
||||
- 员工亲属实体关联自动补入必须只处理成功行,失败行不能产生实体库记录。
|
||||
@@ -1,34 +0,0 @@
|
||||
# 员工信息维护与招聘信息管理正式化外壳前端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
- 基于当前本地最新代码,为 `ccdiBaseStaff` 与 `ccdiStaffRecruitment` 试套正式化外壳样式。
|
||||
- 仅调整查询区、工具条、表格区、分页区与弹窗壳层视觉。
|
||||
- 不改字段顺序、不改按钮位置、不改功能块结构。
|
||||
|
||||
## 范围
|
||||
|
||||
- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
|
||||
- `ruoyi-ui/tests/unit/base-staff-formal-shell-layout.test.js`
|
||||
- `ruoyi-ui/tests/unit/staff-recruitment-formal-shell-layout.test.js`
|
||||
|
||||
## 方案
|
||||
|
||||
- 复用现有 `app-container`、`query-form`、`mb8`、弹窗 class,只补最少样式。
|
||||
- 给列表区新增最小表格外壳,保证分页和表格归一。
|
||||
- 通过边框、浅底、留白和表头背景统一正式化视觉。
|
||||
|
||||
## 验证
|
||||
|
||||
- `node tests/unit/base-staff-formal-shell-layout.test.js`
|
||||
- `node tests/unit/staff-recruitment-formal-shell-layout.test.js`
|
||||
- `node tests/unit/employee-asset-maintenance-layout.test.js`
|
||||
- `node tests/unit/staff-recruitment-import-toolbar.test.js`
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 两个页面外壳样式统一
|
||||
- 按钮顺序和功能入口保持不变
|
||||
- 单测通过
|
||||
- 浏览器实测通过
|
||||
@@ -1,65 +0,0 @@
|
||||
# 2026-04-29 批量正式化外壳样式实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
- 基于当前本地最新前端代码,批量推进信息维护相关页面的正式化外壳样式。
|
||||
- 严格保持“只改样式、不改内容和功能”的边界。
|
||||
- 复用已经在详情弹窗、员工信息维护页、招聘信息管理页验证过的正式化样式骨架。
|
||||
|
||||
## 范围
|
||||
|
||||
- `ruoyi-ui/src/views/ccdiAccountInfo/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCreditInfo/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCustEnterpriseRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCustFmyRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/SearchForm.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/DataTable.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/DetailDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/EditDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffTransfer/index.vue`
|
||||
|
||||
## 实施策略
|
||||
|
||||
### 1. 查询区统一正式化
|
||||
|
||||
- 保留原有查询字段、排布逻辑和按钮位置。
|
||||
- 为查询区补统一白色面板、边框、克制圆角和更稳重的标签文字。
|
||||
- 收紧表单项底部留白,统一输入框、下拉框、日期控件的边框和圆角。
|
||||
|
||||
### 2. 工具条统一正式化
|
||||
|
||||
- 保留搜索、重置、新增、导入、失败记录入口及其相对位置。
|
||||
- 统一工具条外层白色承载区。
|
||||
- 按钮仅调整圆角、边框与视觉重量,不改变语义和行为。
|
||||
|
||||
### 3. 表格承载区统一正式化
|
||||
|
||||
- 新增或复用 `formal-table-shell` 包裹列表表格与分页区。
|
||||
- 收紧表头和行高,提升单屏信息密度。
|
||||
- 主体文本尽量左对齐,保留选择列和操作列居中。
|
||||
|
||||
### 4. 弹窗与详情区统一正式化
|
||||
|
||||
- 统一弹窗圆角、头部下边线、正文浅底。
|
||||
- 详情区、导入弹窗、编辑弹窗使用更克制的信息面板样式。
|
||||
- 不重排现有字段,不新增删减交互块。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 复用现有样式契约单测,确保已完成页面没有回退。
|
||||
- 使用浏览器打开真实业务路由进行验证,禁止使用 prototype 页面替代。
|
||||
- 核对关键页面是否保持:
|
||||
- 查询区与工具条仍在原位置
|
||||
- 新增、导入、失败记录按钮仍按原顺序出现
|
||||
- 表格列和弹窗内容结构不变
|
||||
|
||||
## 风险控制
|
||||
|
||||
- 不使用旧 patch 中的结构改法,只借用可复用的正式化视觉参数。
|
||||
- 每个页面只处理最外层承载和控件外观,不触碰业务字段、接口、校验、按钮逻辑。
|
||||
- 若真实页面路由可访问,则以真实页面结果为准;若不可访问,保留源码级验证说明。
|
||||
@@ -1,56 +0,0 @@
|
||||
# 结果总览项目分析详情正式化外壳前端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
- 基于 `output/mockups/project-analysis-formal-soft-preview.html` 的静态预览稿,恢复“项目分析详情”弹窗的正式化、去卡片化外壳样式。
|
||||
- 本次仅调整详情弹窗整体框架、标题区、左侧人物档案区、右侧主承载区与页签外层视觉。
|
||||
- 不修改“异常明细”页签内部业务结构、分页、按钮、接口与数据逻辑。
|
||||
|
||||
## 范围
|
||||
|
||||
- 修改 `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
|
||||
- 修改 `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
|
||||
- 修改 `ruoyi-ui/tests/unit/project-analysis-dialog-layout.test.js`
|
||||
- 修改 `ruoyi-ui/tests/unit/project-analysis-dialog-sidebar.test.js`
|
||||
|
||||
## 实施方案
|
||||
|
||||
### 1. 弹窗外壳正式化
|
||||
|
||||
- 将当前偏渐变、大圆角的详情弹窗外壳改为更平直的正式化工作台样式。
|
||||
- 顶部保留“结果总览 / 项目分析详情”的信息层级,但改成浅边线、弱装饰、明确留白的标题区。
|
||||
- 调整整体布局间距,让左侧档案区和右侧主区以纵向分隔线形成清晰结构。
|
||||
|
||||
### 2. 左侧档案区映射静态稿
|
||||
|
||||
- 保留当前姓名、风险等级、工号、部门、所属项目、命中模型数、核心异常标签的数据字段。
|
||||
- 通过信息头、字段列表、摘要区三段式样式,映射静态稿的人物档案视觉。
|
||||
- 不新增额外字段、不新增辅助业务区块。
|
||||
|
||||
### 3. 右侧主区外层收口
|
||||
|
||||
- 保持 `el-tabs`、错误提示、加载逻辑、默认页签逻辑不变。
|
||||
- 只调整页签外层、内容承载区、主区边界与留白,不进入各 tab 内部重做内容样式。
|
||||
|
||||
## 验证计划
|
||||
|
||||
### 代码校验
|
||||
|
||||
- 在 `ruoyi-ui` 目录执行:
|
||||
- `node tests/unit/project-analysis-dialog-layout.test.js`
|
||||
- `node tests/unit/project-analysis-dialog-sidebar.test.js`
|
||||
- `node tests/unit/project-analysis-dialog-empty-field.test.js`
|
||||
|
||||
### 浏览器验证
|
||||
|
||||
- 先通过 `nvm use` 确认前端 Node 版本。
|
||||
- 启动真实前端页面后,使用 `browser-use` 在系统真实页面打开“项目分析详情”弹窗。
|
||||
- 重点核对:
|
||||
- 标题区是否为正式化平直样式
|
||||
- 左侧档案区是否按预览稿形成清晰三段层次
|
||||
- 右侧主区是否只改外层、不影响“异常明细”内部内容与交互
|
||||
|
||||
## 风险控制
|
||||
|
||||
- 不改接口、不改 mock 数据、不改异常明细内部组件,避免把外壳样式改动扩大成业务结构调整。
|
||||
- 单测只更新与外层视觉契约直接相关的断言,避免引入无关回归。
|
||||
@@ -1,40 +0,0 @@
|
||||
# 员工信息维护与招聘信息管理正式化外壳实施记录
|
||||
|
||||
## 变更日期
|
||||
|
||||
- 2026-04-29
|
||||
|
||||
## 变更范围
|
||||
|
||||
- 前端:`ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
|
||||
- 前端:`ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
|
||||
- 单测:`ruoyi-ui/tests/unit/base-staff-formal-shell-layout.test.js`
|
||||
- 单测:`ruoyi-ui/tests/unit/staff-recruitment-formal-shell-layout.test.js`
|
||||
|
||||
## 实施内容
|
||||
|
||||
### 1. 页面外壳调整
|
||||
|
||||
- 复用 `app-container`、`query-form`、`mb8` 等现有结构,只补最少样式。
|
||||
- 为 `ccdiBaseStaff` 与 `ccdiStaffRecruitment` 的查询区增加正式化外壳视觉,包括浅底、边框、留白和输入框边线统一。
|
||||
- 保持搜索、重置、新增、导入、失败记录等按钮原有顺序不变。
|
||||
- 为两个页面新增 `formal-table-shell`,将表格和分页收口到同一视觉区域内。
|
||||
|
||||
### 2. 弹窗外壳调整
|
||||
|
||||
- 复用员工页已有 `employee-edit-dialog`、`employee-detail-dialog` class,只调整弹窗圆角、标题分隔线和弹窗正文背景。
|
||||
- 复用招聘页现有弹窗结构,只补统一的弹窗标题区和正文背景样式。
|
||||
- 未改动员工资产、历史工作经历等内部功能块结构。
|
||||
|
||||
### 3. 验证情况
|
||||
|
||||
- 单测通过:
|
||||
- `node tests/unit/base-staff-formal-shell-layout.test.js`
|
||||
- `node tests/unit/staff-recruitment-formal-shell-layout.test.js`
|
||||
- `node tests/unit/staff-recruitment-import-toolbar.test.js`
|
||||
- 现有单测异常:
|
||||
- `node tests/unit/employee-asset-maintenance-layout.test.js`
|
||||
- 失败原因为当前仓库源码不满足既有字符串断言 `createEmptyAssetRow(defaultPersonId = "")`,与本次外壳样式改动无关。
|
||||
- 浏览器验证:
|
||||
- 已使用 `browser-use` 打开 `http://localhost/prototype/staff-recruitment`,确认招聘信息管理页查询区、工具条、表格区已切换为正式化外壳,按钮仍保持原位。
|
||||
- 尝试打开 `http://localhost/ccdiBaseStaff` 时,当前本地前端路由返回 404 页面,因此未能在浏览器内完成员工信息维护页真实页面验证。
|
||||
@@ -1,92 +0,0 @@
|
||||
# 2026-04-29 批量正式化外壳样式实施记录
|
||||
|
||||
## 本次实施内容
|
||||
|
||||
本轮基于当前本地最新代码,批量将信息维护相关页面收口为统一的正式化外壳样式,继续保持“只改样式、不改内容和功能”的边界。
|
||||
|
||||
### 覆盖页面
|
||||
|
||||
- 账户库管理:`ruoyi-ui/src/views/ccdiAccountInfo/index.vue`
|
||||
- 征信维护:`ruoyi-ui/src/views/ccdiCreditInfo/index.vue`
|
||||
- 信贷客户实体关联:`ruoyi-ui/src/views/ccdiCustEnterpriseRelation/index.vue`
|
||||
- 信贷客户家庭关系:`ruoyi-ui/src/views/ccdiCustFmyRelation/index.vue`
|
||||
- 中介库管理:
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/SearchForm.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/DataTable.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/DetailDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/EditDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||
- 招投标信息维护:`ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
|
||||
- 员工亲属实体关联:`ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue`
|
||||
- 员工亲属关系维护:`ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue`
|
||||
- 员工调动记录:`ruoyi-ui/src/views/ccdiStaffTransfer/index.vue`
|
||||
|
||||
## 具体调整
|
||||
|
||||
### 查询区
|
||||
|
||||
- 将筛选区统一收进白色边框面板。
|
||||
- 统一标签颜色、控件边框、控件圆角和表单项间距。
|
||||
- 保留全部原始筛选条件和原始布局顺序。
|
||||
|
||||
### 工具条
|
||||
|
||||
- 为工具条增加统一白色承载面板。
|
||||
- 按钮圆角统一收敛到约 4px。
|
||||
- 不调整搜索、重置、新增、导入、失败记录等按钮的位置和语义。
|
||||
|
||||
### 表格
|
||||
|
||||
- 为列表页统一增加 `formal-table-shell` 外层承载。
|
||||
- 收紧表头和表体留白,提升单屏显示密度。
|
||||
- 统一普通列左对齐,操作列和选择列保持居中。
|
||||
|
||||
### 弹窗
|
||||
|
||||
- 编辑、详情、导入弹窗统一使用更正式的边界和浅底信息面板风格。
|
||||
- 去掉原有偏演示感的悬浮和装饰感。
|
||||
- 不改变弹窗中的字段组织和业务交互。
|
||||
|
||||
## 修正项
|
||||
|
||||
- 批量调整过程中,`ccdiPurchaseTransaction/index.vue` 样式块曾出现一个多余的 `}`,导致前端编译报错。
|
||||
- 已在本轮内修正,重新通过真实页面检查。
|
||||
- 批量将 `.mb8` 统一为 `flex` 承载后,`right-toolbar` 的“显示/隐藏 / 刷新”按钮组一度被挤到左侧。
|
||||
- 已通过为各列表页补充 `.mb8 ::v-deep .top-right-btn { margin-left: auto; }` 恢复原有靠右位置。
|
||||
|
||||
## 验证结果
|
||||
|
||||
### 单测
|
||||
|
||||
- `node ruoyi-ui/tests/unit/base-staff-formal-shell-layout.test.js` 通过
|
||||
- `node ruoyi-ui/tests/unit/staff-recruitment-formal-shell-layout.test.js` 通过
|
||||
- `node ruoyi-ui/tests/unit/project-analysis-dialog-layout.test.js` 通过
|
||||
- `node ruoyi-ui/tests/unit/project-analysis-dialog-sidebar.test.js` 通过
|
||||
|
||||
### 真实页面浏览器验证
|
||||
|
||||
已通过真实业务路由验证以下页面可以打开且关键外壳区域仍保持原有功能结构:
|
||||
|
||||
- `http://localhost/maintain/accountInfo`
|
||||
- `http://localhost/maintain/creditInfo`
|
||||
- `http://localhost/maintain/intermediary`
|
||||
- `http://localhost/maintain/purchaseTransaction`
|
||||
- `http://localhost/maintain/staffTransfer`
|
||||
- `http://localhost/maintain/staffEnterpriseRelation`
|
||||
- `http://localhost/maintain/staffFmyRelation`
|
||||
- `http://localhost/maintain/custEnterpriseRelation`
|
||||
- `http://localhost/maintain/custFmyRelation`
|
||||
- `http://localhost/maintain/staffRecruitment`
|
||||
|
||||
验证点:
|
||||
|
||||
- 页面标题、搜索按钮、新增按钮、导入按钮仍可见
|
||||
- 查询区与工具条仍位于原位置
|
||||
- 未发生按钮左右换位
|
||||
- 表格区与分页区仍按原内容结构展示
|
||||
|
||||
## 现有环境问题
|
||||
|
||||
- `staffRecruitment` 页面当前仍存在后端返回的字符集排序规则冲突报错:`utf8mb4_0900_ai_ci` 与 `utf8mb4_general_ci` 混用。
|
||||
- 该问题来自现有后端/数据库环境,不是本次样式改动引入的问题。
|
||||
@@ -1,101 +0,0 @@
|
||||
# 2026-04-29 正式化样式调整总说明
|
||||
|
||||
## 目标边界
|
||||
|
||||
- 本轮所有改动都基于当前本地最新代码进行。
|
||||
- 仅调整页面与弹窗外壳样式,不改变原有内容、字段、按钮语义、交互流程和功能逻辑。
|
||||
- 不参考 `2026-04-29-dev-ui-style-mixed-stash.patch` 中的结构性和功能性变动。
|
||||
|
||||
## 本轮纳入的页面
|
||||
|
||||
### 1. 项目分析详情弹窗
|
||||
|
||||
- 文件:
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
|
||||
- 调整方向:
|
||||
- 详情页外壳正式化、去卡片化
|
||||
- 标题区更平直,人物档案区更规整
|
||||
- 页签和主区承载更克制
|
||||
- 不变内容:
|
||||
- 异常明细、资产分析、征信摘要等业务内容结构不变
|
||||
- 数据请求、分页、按钮逻辑不变
|
||||
|
||||
### 2. 员工信息维护页
|
||||
|
||||
- 文件:
|
||||
- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
|
||||
- 调整方向:
|
||||
- 筛选区收进统一白色区域
|
||||
- 工具条按钮外观更正式,圆角收小
|
||||
- 表格与分页统一收进正式信息面板
|
||||
- 表格更紧凑、阅读更集中
|
||||
- 编辑/详情弹窗外壳更像正式信息面板
|
||||
- 不变内容:
|
||||
- 查询字段、按钮顺序、导入入口、资产信息与党员信息功能不变
|
||||
|
||||
### 3. 招聘信息管理页
|
||||
|
||||
- 文件:
|
||||
- `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
|
||||
- 调整方向:
|
||||
- 筛选区、工具条、表格区统一正式化
|
||||
- 按钮、输入框、下拉框视觉更稳重
|
||||
- 表格行高与表头高度适当收紧
|
||||
- 弹窗外壳更克制
|
||||
- 不变内容:
|
||||
- 招聘类型、历史工作经历、导入入口、按钮位置和业务流程不变
|
||||
|
||||
### 4. 批量推进的信息维护页面
|
||||
|
||||
- 文件:
|
||||
- `ruoyi-ui/src/views/ccdiAccountInfo/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCreditInfo/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCustEnterpriseRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCustFmyRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/SearchForm.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/DataTable.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/DetailDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/EditDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffTransfer/index.vue`
|
||||
- 调整方向:
|
||||
- 查询区、工具条、列表区统一成正式化白色信息面板
|
||||
- 输入框、下拉框、日期控件边框与圆角统一收敛
|
||||
- 表格与分页通过 `formal-table-shell` 统一承载
|
||||
- 中介库的搜索、列表、详情、编辑、导入弹窗统一到相同视觉语言
|
||||
- 各列表维护页的弹窗边界与留白更克制
|
||||
- 不变内容:
|
||||
- 按钮顺序、字段结构、导入流程、失败记录入口、详情内容和业务逻辑不变
|
||||
|
||||
## 统一视觉原则
|
||||
|
||||
- 筛选区更规整:统一白色面板承载,结构清晰
|
||||
- 按钮更正式:圆角约 4px,弱化轻飘感
|
||||
- 表单控件更稳重:圆角更小,边框更统一
|
||||
- 表格更紧凑:降低表头和行内容留白,一屏展示更多信息
|
||||
- 列表阅读性更好:尽量左对齐,减少长字段换行和大片空白
|
||||
- 视觉装饰收敛:移除不必要的阴影、渐变、悬浮感
|
||||
- 卡片感减弱:边界、留白、圆角更克制,保留原有内容结构
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 项目分析详情弹窗已完成真实页面验证
|
||||
- 员工信息维护页已完成源码与单测级校验
|
||||
- 招聘信息管理页和批量推进页面已通过真实业务路由验证:
|
||||
- `http://localhost/maintain/staffRecruitment`
|
||||
- `http://localhost/maintain/accountInfo`
|
||||
- `http://localhost/maintain/creditInfo`
|
||||
- `http://localhost/maintain/intermediary`
|
||||
- `http://localhost/maintain/purchaseTransaction`
|
||||
- `http://localhost/maintain/staffTransfer`
|
||||
- `http://localhost/maintain/staffEnterpriseRelation`
|
||||
- `http://localhost/maintain/staffFmyRelation`
|
||||
- `http://localhost/maintain/custEnterpriseRelation`
|
||||
- `http://localhost/maintain/custFmyRelation`
|
||||
- 浏览器验证过程中发现并修复了 `ccdiPurchaseTransaction/index.vue` 的样式编译错误
|
||||
- `staffRecruitment` 页面仍存在现有数据库字符集排序规则冲突报错,该问题不属于本轮样式改动
|
||||
@@ -1,44 +0,0 @@
|
||||
# 结果总览项目分析详情正式化外壳实施记录
|
||||
|
||||
## 变更日期
|
||||
|
||||
- 2026-04-29
|
||||
|
||||
## 变更范围
|
||||
|
||||
- 前端:`ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
|
||||
- 前端:`ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
|
||||
- 单测:`ruoyi-ui/tests/unit/project-analysis-dialog-layout.test.js`
|
||||
- 单测:`ruoyi-ui/tests/unit/project-analysis-dialog-sidebar.test.js`
|
||||
|
||||
## 实施内容
|
||||
|
||||
### 1. 弹窗外壳样式正式化
|
||||
|
||||
- 调整 `ProjectAnalysisDialog.vue` 的外层壳样式,去掉旧版渐变大圆角卡片视觉。
|
||||
- 将弹窗主体改为浅灰外层背景 + 白色内容工作台,强化边线、留白和分栏结构。
|
||||
- 将标题区改成“结果总览 / 项目分析详情”的正式化层级,保留当前命中模型提示,但收口为弱装饰信息块。
|
||||
- 调整右侧主区与左侧档案区之间的分隔线和间距,只改外壳,不进入各 tab 内部内容结构。
|
||||
|
||||
### 2. 左侧人物档案区样式映射
|
||||
|
||||
- 调整 `ProjectAnalysisSidebar.vue`,按“人物档案 + 命中模型摘要”两段式结构重排视觉。
|
||||
- 姓名、风险等级、工号、部门、所属项目继续沿用现有数据字段,不新增业务字段。
|
||||
- 将风险等级改为细边框状态标识,字段列表改为规整的标签/值双列展示。
|
||||
- 核心异常标签保留为现有标签数据,仅更新标签外观,不改渲染逻辑。
|
||||
|
||||
### 3. 验证情况
|
||||
|
||||
- 单测通过:
|
||||
- `node tests/unit/project-analysis-dialog-layout.test.js`
|
||||
- `node tests/unit/project-analysis-dialog-sidebar.test.js`
|
||||
- `node tests/unit/project-analysis-dialog-empty-field.test.js`
|
||||
- 浏览器实测:
|
||||
- 使用 `browser-use` 打开本地真实系统 `http://localhost/`
|
||||
- 进入项目详情页 `http://localhost/ccdiProject/detail/90337?tab=overview`
|
||||
- 在“结果总览”页点击“查看详情”,确认“项目分析详情”弹窗已应用正式化外壳样式
|
||||
- 确认左侧人物档案区样式已按预览稿方向收口,右侧“异常明细”内部业务内容未被重做
|
||||
- 环境记录:
|
||||
- `ruoyi-ui/.nvmrc` 期望版本为 `14.21.3`
|
||||
- 当前终端执行 `nvm use` 失败,原因是 `nvm` 未安装到 PowerShell PATH
|
||||
- 本次前端校验在当前可用 Node `v22.22.0` 下完成,相关单测通过
|
||||
@@ -0,0 +1,89 @@
|
||||
# 员工信息维护导入功能真实页面测试记录
|
||||
|
||||
## 测试范围
|
||||
|
||||
- 页面:`http://localhost:8080/maintain/baseStaff`
|
||||
- 后端:`http://localhost:62318`
|
||||
- 模块:员工信息维护导入
|
||||
- 测试方式:`browser-use` 打开真实业务页面,确认导入入口、模板下载和页面列表/详情展示;使用真实模板生成测试工作簿后调用同一导入接口上传,轮询导入任务状态并查询失败记录。
|
||||
|
||||
## 测试文件
|
||||
|
||||
- 真实模板:`output/spreadsheet/base_staff_import_template_20260430.xlsx`
|
||||
- 空模板:`output/spreadsheet/base_staff_import_empty_20260430.xlsx`
|
||||
- 员工成功样本:`output/spreadsheet/base_staff_import_staff_success_20260430.xlsx`
|
||||
- 员工混合失败样本:`output/spreadsheet/base_staff_import_staff_mixed_20260430.xlsx`
|
||||
- 资产成功样本:`output/spreadsheet/base_staff_import_asset_success_20260430.xlsx`
|
||||
- 资产混合失败样本:`output/spreadsheet/base_staff_import_asset_mixed_20260430.xlsx`
|
||||
- 双 Sheet 成功样本:`output/spreadsheet/base_staff_import_dual_success_20260430.xlsx`
|
||||
|
||||
## 页面验证
|
||||
|
||||
1. 打开真实员工信息维护页面,确认页面标题、查询区、列表、单一“导入”按钮加载正常。
|
||||
2. 打开“员工信息维护导入”弹窗,确认存在“下载模板”入口。
|
||||
3. 点击“下载模板”,页面触发真实模板下载。
|
||||
4. 弹窗提示确认:
|
||||
- 模板包含“员工信息”和“员工资产信息”两个 Sheet。
|
||||
- 两个 Sheet 可单独填写,也可同时填写。
|
||||
- 员工信息命中现有员工直接报错。
|
||||
- 资产信息仅支持员工本人资产。
|
||||
5. 页面查询员工 `9843001`,确认成功导入员工 `Codex导入员工A` 可在真实列表展示。
|
||||
6. 打开员工 `9843001` 详情,确认资产 `Codex资产住宅A` 展示在资产信息区。
|
||||
7. 页面查询员工 `9843004`,确认双 Sheet 成功样本中的员工 `Codex导入员工D` 可在真实列表展示。
|
||||
|
||||
## 接口与异步任务验证
|
||||
|
||||
| 场景 | 文件 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 空模板 | `base_staff_import_empty_20260430.xlsx` | 返回 `code=500`,提示“至少需要一条数据” |
|
||||
| 员工 Sheet 成功 | `base_staff_import_staff_success_20260430.xlsx` | 员工任务 `8afb5d0d-009d-4460-b9a5-e66b35716506`,`SUCCESS`,总数 1,成功 1,失败 0 |
|
||||
| 员工 Sheet 混合失败 | `base_staff_import_staff_mixed_20260430.xlsx` | 员工任务 `c57fc240-c50f-42a7-8d77-1614c46789ec`,`PARTIAL_SUCCESS`,总数 7,成功 1,失败 6 |
|
||||
| 资产 Sheet 成功 | `base_staff_import_asset_success_20260430.xlsx` | 资产任务 `8f266611-10ef-471f-8cc7-92b5b65cac45`,`SUCCESS`,总数 1,成功 1,失败 0 |
|
||||
| 资产 Sheet 混合失败 | `base_staff_import_asset_mixed_20260430.xlsx` | 资产任务 `314e1ec1-cf3c-4145-a60f-96f2e8311949`,`PARTIAL_SUCCESS`,总数 5,成功 1,失败 4 |
|
||||
| 双 Sheet 同时成功 | `base_staff_import_dual_success_20260430.xlsx` | 员工任务 `cacb797b-f308-4766-8470-d32061f0a964` 成功 1;资产任务 `08d02f9d-266e-49a1-a52a-fff29426e033` 成功 1 |
|
||||
|
||||
## 失败记录核对
|
||||
|
||||
员工混合失败记录包含 `sheetName=员工信息`、准确 `rowNum` 和失败原因:
|
||||
|
||||
- 第 2 行:员工 ID 已存在。
|
||||
- 第 3 行:姓名不能为空。
|
||||
- 第 4 行:所属部门 ID 不存在或已停用/删除。
|
||||
- 第 5 行:身份证号长度必须为 18 位。
|
||||
- 第 6 行:年收入不能为负数。
|
||||
- 第 8 行:员工 ID 在导入文件中重复。
|
||||
|
||||
资产混合失败记录包含 `sheetName=员工资产信息`、准确 `rowNum` 和失败原因:
|
||||
|
||||
- 第 2 行:资产记录已存在。
|
||||
- 第 3 行:员工资产导入仅支持员工本人证件号。
|
||||
- 第 4 行:资产名称不能为空。
|
||||
- 第 6 行:资产记录在导入文件中重复。
|
||||
|
||||
## 数据回查
|
||||
|
||||
- 员工 `9843001`:列表接口返回 1 条,姓名 `Codex导入员工A`,部门 `研发部门`,状态 `在职`。
|
||||
- 员工 `9843003`:列表接口返回 1 条,姓名 `Codex导入员工C`,验证混合失败文件中的成功行已入库。
|
||||
- 员工 `9843004`:列表接口返回 1 条,姓名 `Codex导入员工D`,验证双 Sheet 员工任务成功入库。
|
||||
- 员工 `9843001` 详情接口返回 3 条本轮导入资产:
|
||||
- `Codex资产住宅A`
|
||||
- `Codex资产车辆重复`
|
||||
- `Codex资产商铺D`
|
||||
|
||||
## 发现的问题
|
||||
|
||||
- 员工 `9843001` 列表中 `deptName=研发部门`,但详情接口返回 `deptName=null`,页面详情“所属部门”显示为 `-`。该问题不阻断导入成功,但属于员工详情展示链路的现存缺陷。
|
||||
|
||||
## 清理范围
|
||||
|
||||
本轮成功写入的测试员工:
|
||||
|
||||
- `9843001`
|
||||
- `9843003`
|
||||
- `9843004`
|
||||
|
||||
删除员工时,当前后端会同步删除员工身份证号对应的本人资产数据。
|
||||
|
||||
## 结论
|
||||
|
||||
员工信息维护导入主链路通过:双 Sheet 单入口、空数据拦截、员工新增、员工重复/必填/部门/证件/金额校验、资产新增、资产重复/归属/必填校验、失败记录 Sheet 与行号定位、页面列表与详情资产展示均已验证。需要单独处理员工详情部门名称缺失问题。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 员工信息维护分页重复修复实施记录
|
||||
|
||||
## 问题
|
||||
|
||||
- 页面:`http://localhost:1025/maintain/baseStaff`
|
||||
- 接口:`GET /ccdi/baseStaff/list`
|
||||
- 现象:第一页和第二页存在相同员工。
|
||||
- 复现结果:接口第一页和第二页出现 `9020004`、`9020005`、`9020009`、`9020003`、`9020008` 等重复员工。
|
||||
|
||||
## 原因
|
||||
|
||||
员工列表 SQL 仅按 `create_time DESC` 排序。批量导入的员工会在同一批次写入相同 `create_time`,数据库在相同排序值之间没有稳定顺序,分页时可能导致同一员工跨页重复出现。
|
||||
|
||||
## 修改内容
|
||||
|
||||
- `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiBaseStaffMapper.xml`
|
||||
- 将员工分页列表排序从 `ORDER BY e.create_time DESC` 调整为 `ORDER BY e.create_time DESC, e.staff_id DESC`。
|
||||
- 使用唯一的 `staff_id` 作为二级排序字段,保证同一创建时间内分页顺序稳定。
|
||||
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiBaseStaffMapperTest.java`
|
||||
- 增加 Mapper XML 断言,防止后续误删稳定分页排序。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 仅影响员工信息维护列表分页顺序。
|
||||
- 不改变筛选条件、列表字段、总数统计、导入、新增、编辑、删除逻辑。
|
||||
|
||||
## 验证
|
||||
|
||||
- 修改前已通过后端接口复现第一页和第二页员工重复。
|
||||
- `node -e` 轻量 XML 断言通过,确认 Mapper 中包含 `ORDER BY e.create_time DESC, e.staff_id DESC`。
|
||||
- `mvn -pl ruoyi-admin -am clean package -DskipTests` 构建通过。
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiBaseStaffMapperTest -Dsurefire.failIfNoSpecifiedTests=false test` 通过,`CcdiBaseStaffMapperTest` 共 2 个用例全部通过。
|
||||
- 后端接口复验:
|
||||
- 第 1 页:`9020018` 至 `9020009`。
|
||||
- 第 2 页:`9020008` 至 `9020001`,并继续显示后续员工。
|
||||
- 第 1 页与第 2 页 `staffId` 交集为 `none`。
|
||||
- browser-use 打开真实页面 `http://localhost:1025/maintain/baseStaff` 验证:
|
||||
- 第 1 页首行显示 `许平 / 9020018`。
|
||||
- 点击分页第 2 页后,页面首行显示 `郑丽 / 9020008`,未再显示第一页首行员工。
|
||||
@@ -0,0 +1,35 @@
|
||||
# EasyExcel 导入模板 Fontconfig 异常修复实施记录
|
||||
|
||||
## 保存路径确认
|
||||
|
||||
- 文档类型:实施记录
|
||||
- 保存路径:`docs/reports/implementation/`
|
||||
|
||||
## 问题说明
|
||||
|
||||
- 请求地址:`/ccdi/baseStaff/importTemplate`
|
||||
- 异常信息:`java.lang.RuntimeException: Fontconfig head is null, check your fonts or fonts configuration`
|
||||
- 触发链路:员工信息维护导入模板下载使用 `EasyExcelUtil.importTemplateWithDictDropdown` 生成双 Sheet 模板,EasyExcel 默认创建 `SXSSFWorkbook`,在无可用字体配置的服务器环境中创建 Sheet 时会触发 POI/AWT 字体配置初始化异常。
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修改 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/utils/EasyExcelUtil.java`
|
||||
- 将导入模板下载相关写出入口统一改为 `inMemory(Boolean.TRUE)`:
|
||||
- 单 Sheet 普通模板
|
||||
- 单 Sheet 带字典下拉模板
|
||||
- 指定文件名的带字典下拉模板
|
||||
- 双 Sheet 带字典下拉模板
|
||||
- 普通数据导出入口保持流式写出,不改变大数据导出的内存特性。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 修复员工信息维护 `/ccdi/baseStaff/importTemplate` 模板下载。
|
||||
- 同步覆盖复用同一工具方法的其他导入模板下载接口。
|
||||
- 不改变导入解析逻辑、模板表头、字典下拉、必填标记和文本格式处理逻辑。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- 已执行:`mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 结果:失败,失败原因是本机 JDK 21 下 Mockito inline 自附加失败,错误为 `Could not self-attach to current VM`,未进入模板业务断言。
|
||||
- 已执行:`mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilTemplateTest -Dsurefire.failIfNoSpecifiedTests=false -DargLine=-javaagent:/Users/wkc/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.8/byte-buddy-agent-1.17.8.jar test`
|
||||
- 结果:通过,`Tests run: 5, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
@@ -0,0 +1,54 @@
|
||||
# 2026-05-06 流水标签启动循环依赖修复实施记录
|
||||
|
||||
## 保存路径
|
||||
|
||||
- `docs/reports/implementation/2026-05-06-bank-tag-circular-dependency-fix.md`
|
||||
|
||||
## 问题背景
|
||||
|
||||
后端启动时报出 Bean 循环依赖:
|
||||
|
||||
```text
|
||||
ccdiBankTagController
|
||||
-> ccdiBankTagServiceImpl
|
||||
-> ccdiProjectOverviewServiceImpl
|
||||
-> ccdiModelParamServiceImpl
|
||||
-> ccdiBankTagServiceImpl
|
||||
```
|
||||
|
||||
循环关系的业务原因是:流水标签服务执行完成后刷新项目总览;项目总览导出报告时读取模型参数;模型参数保存后触发流水标签自动重算。
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 调整 `CcdiProjectOverviewServiceImpl` 的结果总览报告参数读取方式:
|
||||
- 移除对 `ICcdiModelParamService` 的注入。
|
||||
- 改为通过 `CcdiModelParamMapper` 只读查询模型参数。
|
||||
- 保持原有项目配置规则:`configType=default` 读取 `project_id=0` 的默认参数,否则读取当前项目参数。
|
||||
- 补充 `CcdiProjectOverviewServiceImplTest`,覆盖默认配置项目导出报告时参数读取来源。
|
||||
- 补充 `CcdiProjectOverviewServiceStructureTest`,约束结果总览服务实现不再注入 `ICcdiModelParamService`,避免循环依赖回归。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 仅涉及后端服务依赖关系与结果总览报告参数读取链路。
|
||||
- 不涉及前端页面、数据库结构和菜单权限。
|
||||
- 参数保存后触发自动重算、流水标签执行后刷新总览的业务链路保持不变。
|
||||
|
||||
## 验证情况
|
||||
|
||||
已执行并通过:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-project -am -Dtest=CcdiProjectOverviewServiceStructureTest,CcdiProjectOverviewServiceImplTest,CcdiBankTagServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
结果:`Tests run: 37, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
|
||||
已执行并通过:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am -DskipTests compile
|
||||
```
|
||||
|
||||
结果:`BUILD SUCCESS`。
|
||||
|
||||
补充说明:包含 `CcdiModelParamServiceImplTest` 的扩展测试命令因既有 Mockito 静态 mock 环境问题失败,错误为 `SubclassByteBuddyMockMaker does not support the creation of static mocks`;本次相关的结果总览与流水标签测试均已通过。
|
||||
@@ -0,0 +1,111 @@
|
||||
# 流水上传原始文件名保持实施记录
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 实施时间:2026-05-06
|
||||
- 需求范围:上传流水文件后,页面展示文件名与调用流水分析平台上传接口传递的文件名必须保持为用户初始上传文件名。
|
||||
- 历史数据处理:不处理历史上传记录,不追加历史修复脚本。
|
||||
|
||||
## 修改内容
|
||||
|
||||
### 流水分析客户端
|
||||
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java`
|
||||
- 新增可指定 multipart 文件名的 `namedFileResource`。
|
||||
- `uploadFile` 支持直接传入 `Resource`,避免重新包装后丢失指定文件名。
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`
|
||||
- 新增 `uploadFile(Integer groupId, File file, String uploadFileName)` 重载。
|
||||
- 原两参方法保留并委托到三参方法,默认继续使用本地文件名。
|
||||
|
||||
### 项目流水上传
|
||||
|
||||
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
|
||||
- 用户上传流水文件时,调用流水分析平台上传接口传入 `record.getFileName()`,即初始上传文件名。
|
||||
- 用户上传链路查询平台解析状态后,不再用平台返回的 `uploadFileName/downloadFileName` 覆盖记录文件名。
|
||||
- 拉取本行信息链路继续允许使用平台返回文件名,避免改变该存量业务行为。
|
||||
|
||||
### 测试覆盖
|
||||
|
||||
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
|
||||
- 补充验证用户上传链路传给 `LsfxAnalysisClient` 的文件名为初始上传文件名。
|
||||
- 补充验证解析成功时,即使平台返回不同文件名,记录仍保持初始上传文件名。
|
||||
- 补充验证解析失败时,即使平台返回不同文件名,记录仍保持初始上传文件名。
|
||||
- 保留拉取本行信息链路使用平台返回文件名的既有断言。
|
||||
|
||||
## 验证情况
|
||||
|
||||
### 已通过
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-lsfx -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest,CreditParseControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
- 结果:BUILD SUCCESS
|
||||
- 覆盖:multipart 指定文件名、流水分析客户端三参上传、既有征信解析控制器回归。
|
||||
|
||||
```bash
|
||||
env MAVEN_OPTS=-Djdk.attach.allowAttachSelf=true mvn -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldUploadToLsfxWithOriginalRecordFileName+processFileAsync_shouldKeepOriginalFileNameWhenStatusReturnsDifferentName+processFileAsync_shouldKeepOriginalFileNameWhenParseStatusFails+processPullBankInfoAsync_shouldUpdateFileSizeFromStatusResponse -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
- 结果:BUILD SUCCESS
|
||||
- 覆盖:用户上传文件名传递、解析成功文件名保持、解析失败文件名保持、拉取本行信息链路不回归。
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-lsfx,ccdi-project -am -DskipTests compile
|
||||
```
|
||||
|
||||
- 结果:BUILD SUCCESS
|
||||
- 覆盖:涉及模块编译通过。
|
||||
|
||||
```bash
|
||||
sh bin/restart_java_backend.sh restart
|
||||
```
|
||||
|
||||
- 结果:后端重新打包成功,启动日志出现“若依启动成功”。
|
||||
- 说明:本轮真实页面验证结束后,已关闭验证期间启动的后端进程。
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
source "$HOME/.nvm/nvm.sh"
|
||||
nvm use
|
||||
npm run dev -- --port 9527
|
||||
```
|
||||
|
||||
- 结果:Node 已切换到 `v14.21.3`,前端开发服务启动到 `http://localhost:9527/`。
|
||||
- 说明:本轮真实页面验证结束后,已关闭验证期间启动的前端进程。
|
||||
|
||||
```bash
|
||||
browser-use:browser
|
||||
```
|
||||
|
||||
- 验证页面:`http://localhost:9527/ccdiProject/detail/90341`
|
||||
- 验证项目:`流水文件名验证-20260506-1515`
|
||||
- 上传样本:`output/spreadsheet/流水文件名保持-原始名.csv`
|
||||
- 验证方式:
|
||||
- 使用 `browser-use:browser` 打开真实项目详情页和“上传数据”页。
|
||||
- `browser-use` 当前可用接口未暴露本地文件选择能力,本轮文件提交使用页面同源后端接口 `/ccdi/file-upload/batch`,提交后再回到真实页面核对列表展示。
|
||||
- 页面列表、后端记录、流水分析平台转传日志均核对本轮记录。
|
||||
- 本轮记录:`id=189`
|
||||
- 结果:页面列表展示 `流水文件名保持-原始名.csv`,后端记录 `fileName=流水文件名保持-原始名.csv`,调用流水分析平台上传接口日志中的 `fileName=流水文件名保持-原始名.csv`,未出现本地临时文件名前缀。
|
||||
- 关键日志:
|
||||
- `logs/backend-console.log:58460` 保存本地临时文件时记录 `originalName=流水文件名保持-原始名.csv`。
|
||||
- `logs/backend-console.log:58469` 异步任务提交时记录 `fileName=流水文件名保持-原始名.csv`。
|
||||
- `logs/backend-console.log:58471` 处理本地临时文件路径,但业务记录文件名仍为原始文件名。
|
||||
- `logs/backend-console.log:58473` 调用流水分析平台上传接口时 `fileName=流水文件名保持-原始名.csv`。
|
||||
- `logs/backend-console.log:58496` 流水分析平台解析状态返回 `downloadFileName/uploadFileName=流水文件名保持-原始名.csv`。
|
||||
- `logs/backend-console.log:58522` 解析成功更新记录时 `fileName=流水文件名保持-原始名.csv`。
|
||||
- 清理:验证完成后已通过后端接口删除临时项目 `90341`,并关闭本轮启动的前端、后端测试进程。
|
||||
|
||||
### 额外观察
|
||||
|
||||
- 同一测试项目页面中还观察到一条非本轮接口提交的记录 `id=190`,展示文件名为 `770262d91fd54622abcd5865132a60d2_0_1778052202738_330106198412121113-陈晨_9020011_流水.csv`。
|
||||
- 当前后端日志未检索到该记录对应的插入、上传或解析处理日志,暂不能确认其来源。
|
||||
- 本轮结论仅确认用户上传链路中,当前代码实例处理的记录已保持原始文件名;上述异常记录需要单独追溯来源后再判断是否属于其他链路问题。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响用户手工上传流水文件链路。
|
||||
- 不改历史记录。
|
||||
- 不改数据库结构。
|
||||
- 不改前端展示字段。
|
||||
- 不改变拉取本行信息链路的文件名展示逻辑。
|
||||
@@ -0,0 +1,42 @@
|
||||
# 导入模板下拉框结构校验实施记录
|
||||
|
||||
## 修改背景
|
||||
|
||||
员工信息维护导入文件中存在字典列下拉框被删除的情况,例如“状态”列不是模板下拉框。此类文件不应进入业务导入流程,需要在导入解析阶段直接报错,且所有使用 `@DictDropdown` 的导入类都应统一校验。
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 在 `EasyExcelUtil.importExcel(...)` 入口增加导入模板结构校验。
|
||||
- 校验范围为导入实体类中带 `@DictDropdown` 且配置了 `@ExcelProperty(index = ...)` 的列。
|
||||
- 对有实际数据的行逐行检查对应列是否被 `LIST` 类型数据验证覆盖。
|
||||
- 同一个 Sheet 内存在多个缺失下拉框列时,合并列名一次性提示,例如:
|
||||
- `员工信息 Sheet 的 是否党员、状态 列缺少下拉框,请下载最新导入模板填写后重新导入`
|
||||
- 无 `@DictDropdown` 的普通导入类不执行该结构校验。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 统一影响 `EasyExcelUtil.importExcel(InputStream, Class<T>)`、`EasyExcelUtil.importExcel(InputStream, Class<T>, String)` 和文件路径导入入口。
|
||||
- 员工信息维护、员工资产、亲属关系、招聘信息、实体库等复用该工具类且存在 `@DictDropdown` 字段的导入功能都会具备下拉框结构校验。
|
||||
- 不改变模板下载逻辑,不改变业务字段枚举值校验和异步导入逻辑。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 已执行 Maven 回归:
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=EasyExcelUtilImportDropdownValidationTest,EasyExcelUtilTemplateTest,CcdiBaseStaffControllerTest,CcdiStaffFmyRelationControllerTest,CcdiAssetInfoControllerTest,CcdiBaseStaffAssetImportControllerTest -Dsurefire.failIfNoSpecifiedTests=false -DargLine="-javaagent:/Users/wkc/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.8/byte-buddy-agent-1.17.8.jar" test`
|
||||
- 结果:32 个测试全部通过。
|
||||
- 已检查用户提供的文件:
|
||||
- `/Users/wkc/Desktop/ccdi/ccdi_bulk_20260430/员工信息维护导入模板_批量测试数据.xlsx`
|
||||
- `员工信息` Sheet 数据验证数量为 0。
|
||||
- `员工资产信息` Sheet 数据验证数量为 0。
|
||||
- 已重启后端并调用真实导入接口上传该文件:
|
||||
- `POST http://localhost:62318/ccdi/baseStaff/importData`
|
||||
- 返回:`{"msg":"员工信息 Sheet 的 是否党员、状态 列缺少下拉框,请下载最新导入模板填写后重新导入","code":500}`
|
||||
- 已使用 browser-use 打开真实页面:
|
||||
- `http://localhost:1025/maintain/baseStaff`
|
||||
- 已进入“员工信息维护导入”弹窗,确认页面入口和真实业务页可访问。
|
||||
- 当前 browser-use 可用 API 不支持直接给隐藏文件选择框设置本地文件,上传动作改用真实后端接口验证。
|
||||
|
||||
## 清理说明
|
||||
|
||||
- 本轮异常导入在模板结构校验阶段被拦截,未提交异步导入任务,未写入员工或资产业务数据。
|
||||
- 测试过程中未生成需要提交到 Git 的测试文件。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 中介实体自动补入无需机构名称规则文档修订记录
|
||||
|
||||
## 保存路径确认
|
||||
|
||||
- 目标目录:`docs/reports/implementation/`
|
||||
- 文档用途:记录中介实体自动补入业务规则调整
|
||||
- 路径检查结果:符合仓库实施记录归档规范
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修订员工资产导入与实体库自动补入修复设计文档。
|
||||
- 修订对应后端实施计划。
|
||||
- 明确中介实体关联缺失实体时不要求提供机构名称。
|
||||
- 明确中介实体自动补入允许 `ccdi_enterprise_base_info.enterprise_name` 为 `NULL`。
|
||||
- 移除后端计划中新增中介 `enterpriseName` DTO 字段、Excel `机构名称` 列、失败记录名称字段的实施要求。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 文档范围:设计文档、后端实施计划、实施记录。
|
||||
- 代码范围:本次未修改源码。
|
||||
- 业务规则:中介实体关联按统一社会信用代码补入实体库,来源为 `INTERMEDIARY`,风险等级为 `1`,企业名称允许为空。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 文档关键字检查:已检查计划中不再要求中介缺实体时填写机构名称。
|
||||
- 代码验证:本次未修改源码,未执行编译或单元测试。
|
||||
@@ -0,0 +1,58 @@
|
||||
# 2026-05-06 NAS Docker 部署实施记录
|
||||
|
||||
## 保存路径确认
|
||||
|
||||
- 目标目录:`docs/reports/implementation/`
|
||||
- 文档用途:记录本次 NAS Docker 部署操作、影响范围与验证结果
|
||||
- 路径检查结果:符合仓库实施记录归档规范
|
||||
|
||||
## 本次操作
|
||||
|
||||
- 在本地仓库 `/Users/wkc/Desktop/ccdi/ccdi` 执行 NAS 部署。
|
||||
- 使用 `ruoyi-ui/.nvmrc` 指定的 Node `v14.21.3` 运行前端构建。
|
||||
- 执行后端打包:`mvn clean package -DskipTests`。
|
||||
- 执行前端打包:`npm run build:prod`。
|
||||
- 执行部署脚本:`deploy/deploy-to-nas.sh`。
|
||||
- 部署目标:
|
||||
- SSH:`116.62.17.81:9444`
|
||||
- 远端目录:`/volume1/webapp/ccdi`
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 远端部署目录 `/volume1/webapp/ccdi` 已刷新为本次构建产物。
|
||||
- Docker 服务已重建:
|
||||
- `ccdi-backend`
|
||||
- `ccdi-frontend`
|
||||
- `ccdi-lsfx-mock`
|
||||
- 本次操作未修改业务代码。
|
||||
|
||||
## 验证结果
|
||||
|
||||
### 构建验证
|
||||
|
||||
- Maven 聚合打包成功,`ruoyi-admin/target/ruoyi-admin.jar` 已生成。
|
||||
- Vue 生产构建成功,`ruoyi-ui/dist` 已生成。
|
||||
- 前端构建期间仅出现资源体积告警,无构建失败。
|
||||
|
||||
### 远端容器验证
|
||||
|
||||
- `docker compose ps` 结果:
|
||||
- `ccdi-backend`:`Up About a minute`
|
||||
- `ccdi-frontend`:`Up About a minute`
|
||||
- `ccdi-lsfx-mock`:`Up About a minute`
|
||||
- 端口映射结果:
|
||||
- `62318 -> backend:8080`
|
||||
- `62319 -> frontend:80`
|
||||
- `62320 -> mock:8000`
|
||||
|
||||
### 应用可用性验证
|
||||
|
||||
- 在 NAS 本机访问 `127.0.0.1` 返回正常:
|
||||
- `http://127.0.0.1:62319` 返回 `200`
|
||||
- `http://127.0.0.1:62318/swagger-ui/index.html` 返回 `200`
|
||||
- `http://127.0.0.1:62320/docs` 返回 `200`
|
||||
- 后端日志确认:
|
||||
- `nas` profile 已启用
|
||||
- TongWeb `8080` 已启动
|
||||
- `RuoYiApplication` 启动完成
|
||||
- 输出“若依启动成功”
|
||||
@@ -0,0 +1,53 @@
|
||||
# 项目分析弹窗圆角显示方案调整实施记录
|
||||
|
||||
## 变更日期
|
||||
|
||||
- 2026-05-06
|
||||
|
||||
## 保存路径确认
|
||||
|
||||
- 本次实施记录保存到 `docs/reports/implementation/`,符合项目实施记录目录规范。
|
||||
|
||||
## 变更范围
|
||||
|
||||
- 前端:`ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
|
||||
- 前端:`ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
|
||||
- 前端:`ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue`
|
||||
- 单测:`ruoyi-ui/tests/unit/project-analysis-dialog-layout.test.js`
|
||||
- 单测:`ruoyi-ui/tests/unit/project-analysis-dialog-sidebar.test.js`
|
||||
- 单测:`ruoyi-ui/tests/unit/project-analysis-dialog-abnormal-tab.test.js`
|
||||
|
||||
## 实施内容
|
||||
|
||||
### 1. 弹窗外壳统一
|
||||
|
||||
- 将项目分析弹窗重新收口为单一弹窗壳显示方案。
|
||||
- 外层 `el-dialog` 统一使用 `8px` 圆角,标题栏与内容区使用同一浅灰工作台背景。
|
||||
- 移除内部 `24px` 大圆角壳层,避免外层默认圆角、内部大圆角和直角内容区混杂。
|
||||
|
||||
### 2. 左侧人物档案面板统一
|
||||
|
||||
- 将左侧人物档案和命中模型摘要合并到同一个 `6px` 面板内。
|
||||
- 人物档案与命中模型摘要之间通过分隔线区分层次,不再使用两个独立大圆角白卡。
|
||||
- 风险等级和异常标签统一使用 `6px` 状态标识样式。
|
||||
|
||||
### 3. 右侧内容区统一
|
||||
|
||||
- 右侧主承载区统一为 `6px` 面板,tabs 顶部与内容区共享同一边界。
|
||||
- 异常明细卡片、流水表格、对象摘要卡片和异常原因快照统一使用 `6px` 圆角。
|
||||
- 不修改接口、字段、分页、证据库按钮或异常明细业务逻辑。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 已通过 Node 版本切换验证:`nvm use`,实际使用 `v14.21.3`。
|
||||
- 已执行并通过:
|
||||
- `node tests/unit/project-analysis-dialog-layout.test.js`
|
||||
- `node tests/unit/project-analysis-dialog-sidebar.test.js`
|
||||
- `node tests/unit/project-analysis-dialog-abnormal-tab.test.js`
|
||||
- `node tests/unit/project-analysis-dialog-empty-field.test.js`
|
||||
- 已执行 `git diff --check`,相关改动无空白错误。
|
||||
- 已使用 `browser-use` 打开真实页面 `http://localhost:1025/ccdiProject/detail/90338?tab=overview`,在结果总览页点击“查看详情”打开“项目分析”弹窗验证:
|
||||
- 页面标题与业务项目数据正常显示。
|
||||
- 弹窗打开后详情接口加载完成,人员工号、异常明细表格与异常对象摘要正常渲染。
|
||||
- 左侧面板不再出现大圆角白卡和直角灰底混用。
|
||||
- 当前浏览器视口下弹窗无框架错误覆盖,控制台无相关 error/warn。
|
||||
@@ -0,0 +1,24 @@
|
||||
# 新建项目后自动进入上传数据页前端实施记录
|
||||
|
||||
## 保存路径检查
|
||||
- 本次为页面交互改动,实施记录按项目规则保存到 `docs/reports/implementation/`
|
||||
|
||||
## 问题描述
|
||||
- 在【新建项目】弹窗填写项目名称并点击【确认/创建项目】后,页面停留在项目列表,仅刷新列表数据
|
||||
|
||||
## 本次改动
|
||||
- 调整初核项目列表页的新建项目成功回调
|
||||
- 创建接口返回 `projectId` 后,自动跳转到 `/ccdiProject/detail/{projectId}?tab=upload`
|
||||
- 跳转后由项目详情页按 `tab=upload` 激活【上传数据】页签
|
||||
|
||||
## 影响文件
|
||||
- `ruoyi-ui/src/views/ccdiProject/index.vue`
|
||||
|
||||
## 验证
|
||||
- `node ruoyi-ui/tests/unit/project-create-upload-jump.test.js`
|
||||
- `browser-use:browser` 初始化 Codex in-app browser 后端
|
||||
|
||||
## 验证结果
|
||||
- 源码契约测试通过,确认新建项目成功后使用接口返回的 `projectId` 跳转到项目详情上传数据页
|
||||
- 已确认前端服务 `http://localhost:8081/` 与后端服务 `http://localhost:62318/` 可用
|
||||
- `browser-use:browser` 未能连接到 `iab` 后端,报错为未发现 Codex in-app browser backend,因此真实页面浏览器验证未完成
|
||||
@@ -0,0 +1,49 @@
|
||||
# 撤回统一信息维护正式化外壳样式实施记录
|
||||
|
||||
## 背景
|
||||
|
||||
- 本次按要求撤回提交 `6f2ea599 统一信息维护正式化外壳样式` 中合并的前端外壳样式调整。
|
||||
- 保存路径已按项目规则确认:实施记录放在 `docs/reports/implementation/`。
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 逆向应用提交 `6f2ea599`,撤回信息维护页面、部分弹窗和项目分析弹窗中的正式化外壳样式调整。
|
||||
- 同步撤回该提交新增的正式化外壳样式前端实施计划和历史实施记录。
|
||||
- 保留当前工作区已有的其他未提交改动,不纳入本次撤回范围。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 前端页面:
|
||||
- `ruoyi-ui/src/views/ccdiAccountInfo/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCreditInfo/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCustEnterpriseRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiCustFmyRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiIntermediary/`
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/`
|
||||
- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffEnterpriseRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
|
||||
- `ruoyi-ui/src/views/ccdiStaffTransfer/index.vue`
|
||||
- 前端单测:
|
||||
- `ruoyi-ui/tests/unit/project-analysis-dialog-layout.test.js`
|
||||
- `ruoyi-ui/tests/unit/project-analysis-dialog-sidebar.test.js`
|
||||
|
||||
## 验证记录
|
||||
|
||||
- `git diff --cached --check`:通过,无空白错误。
|
||||
- `git diff --check -- $(git show --name-only --format= 6f2ea5994ade0060814b46a0144a5485fbab2d89)`:通过,无空白错误。
|
||||
- `git diff --name-status 95ac01d7dc9ab1d14cf31d2aa12c49c7d3128b29 -- $(git show --name-only --format= 6f2ea5994ade0060814b46a0144a5485fbab2d89)`:无输出,确认本次撤回范围已回到 `6f2ea599` 父提交状态。
|
||||
- `cd ruoyi-ui && nvm use && node -v && npx mocha tests/unit/project-analysis-dialog-layout.test.js tests/unit/project-analysis-dialog-sidebar.test.js`:
|
||||
- Node 已切换到 `v14.21.3`。
|
||||
- 命令退出码为 `0`;两个断言型脚本未抛出异常。
|
||||
- `cd ruoyi-ui && nvm use && node -v && npm run build:prod`:
|
||||
- Node 已切换到 `v14.21.3`。
|
||||
- 构建通过;仅保留既有资源体积类 warning。
|
||||
- `browser-use` 真实页面验证:
|
||||
- 复用本机已有 `http://localhost:1025` 前端服务,未额外启动测试进程。
|
||||
- 后端 `http://localhost:62318` 已在监听,使用真实登录页完成登录。
|
||||
- 打开 `/maintain/baseStaff`、`/maintain/staffRecruitment`、`/maintain/accountInfo`,页面查询区、列表和操作按钮正常渲染。
|
||||
- 打开项目详情页并点击“查看详情”,项目分析弹窗正常展示“项目分析 / 异常明细 / 资产分析 / 征信摘要 / 关系图谱 / 资金流向”。
|
||||
- 浏览器控制台未发现 error 日志。
|
||||
@@ -0,0 +1,88 @@
|
||||
# 上传流水文件原始文件名保持设计
|
||||
|
||||
## 背景
|
||||
|
||||
项目详情的“上传数据”页支持批量上传流水文件。当前前端提交文件时保留了浏览器选择文件的原始文件名,但后端为了异步处理,会先把文件保存为带有批次号、序号和时间戳的临时文件名。异步任务再把这个临时文件对象转传给流水分析平台,导致流水分析平台上传接口收到的 multipart 文件名不是用户初始上传的文件名。
|
||||
|
||||
同时,后端在查询流水分析平台文件状态后,会用平台返回的 `uploadFileName` 或 `downloadFileName` 覆盖本系统上传记录 `ccdi_file_upload_record.file_name`,页面列表读取该字段展示文件名,因此页面展示名也可能与初始上传文件名不一致。
|
||||
|
||||
## 目标
|
||||
|
||||
- 新上传流水文件时,页面展示的文件名必须保持用户初始上传文件名。
|
||||
- 调用流水分析平台上传文件接口时,multipart 文件 part 的 filename 必须保持用户初始上传文件名。
|
||||
- 临时文件仍需保持唯一命名,避免批量上传、同名文件或并发上传互相覆盖。
|
||||
- 本次只处理“上传本地流水文件”链路,不改变“拉取本行信息”链路的现有文件名展示与状态处理行为。
|
||||
- 历史已上传记录不做批量回改。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不修改历史上传记录的文件名。
|
||||
- 不调整上传入口、文件格式限制、文件大小限制、异步任务调度和解析轮询规则。
|
||||
- 不新增数据库字段。
|
||||
- 不改变流水分析平台接口地址、请求参数名和响应解析口径。
|
||||
- 不改变“拉取本行信息”生成的上传记录文件名规则。
|
||||
|
||||
## 现状链路
|
||||
|
||||
1. 前端 `UploadData.vue` 将 `selectedFiles.map((f) => f.raw)` 传给 `batchUploadFiles`。
|
||||
2. `ccdiProjectUpload.js` 使用 `FormData.append("files", file)` 上传,浏览器侧仍保留原始文件名。
|
||||
3. `CcdiFileUploadController.batchUpload` 通过 `MultipartFile.getOriginalFilename()` 校验格式。
|
||||
4. `CcdiFileUploadServiceImpl.batchUploadFiles` 保存记录时写入原始文件名,但临时文件路径使用 `batchId_index_timestamp_originalFilename`。
|
||||
5. `processFileAsync` 读取临时文件并调用 `lsfxClient.uploadFile(lsfxProjectId, file)`。
|
||||
6. `LsfxAnalysisClient.uploadFile` 和 `HttpUtil.uploadFile` 只接收 `File`,最终用 `FileSystemResource` 发送文件,multipart filename 来自临时文件名。
|
||||
7. 查询文件上传状态后,`processRecordAfterLogIdReady` 使用平台返回文件名覆盖 `record.fileName`。
|
||||
|
||||
## 设计方案
|
||||
|
||||
### 后端上传转发
|
||||
|
||||
保留当前临时文件唯一命名方式,避免同名文件覆盖。异步处理时以 `CcdiFileUploadRecord.fileName` 作为原始文件名来源,将“临时文件内容”和“原始文件名”一起传给流水分析客户端。
|
||||
|
||||
`LsfxAnalysisClient.uploadFile` 增加可指定上传文件名的能力。对项目上传流水链路,调用新签名并传入原始文件名;测试 Controller 或其他调用方如果不传原始文件名,可继续使用现有文件名语义。
|
||||
|
||||
`HttpUtil.uploadFile` 增加对“文件内容 + 指定 filename”的 multipart 资源包装。发送时仍使用参数名 `files`,但文件 part 的 filename 使用原始文件名,而不是临时文件名。
|
||||
|
||||
### 上传记录文件名
|
||||
|
||||
“上传本地流水文件”链路中的 `ccdi_file_upload_record.file_name` 只记录本系统初始上传文件名。查询流水分析平台状态后,该链路不再用 `uploadFileName` 或 `downloadFileName` 覆盖该字段。
|
||||
|
||||
平台返回的文件大小仍可继续更新到 `file_size`,解析状态、主体名称、账号、错误信息等字段保持现有处理方式。
|
||||
|
||||
当前状态后处理逻辑会被本地上传和“拉取本行信息”复用。实现时需要在调用或方法参数上区分来源:本地上传链路保留初始文件名;拉取本行信息链路保持现有行为,继续按当前规则处理平台返回文件名。
|
||||
|
||||
### 前端展示
|
||||
|
||||
前端无需改造。上传记录列表继续展示后端返回的 `fileName`。由于本地上传链路后端不再覆盖该字段,新上传记录从创建、处理中、成功或失败状态都会展示初始上传文件名。
|
||||
|
||||
## 数据流
|
||||
|
||||
```text
|
||||
用户选择文件: 银行流水A.xlsx
|
||||
-> 前端 FormData files: filename=银行流水A.xlsx
|
||||
-> 后端 MultipartFile originalFilename=银行流水A.xlsx
|
||||
-> 本地临时文件: batchId_0_timestamp_银行流水A.xlsx
|
||||
-> 上传记录 file_name=银行流水A.xlsx
|
||||
-> 流水分析平台 multipart files: filename=银行流水A.xlsx
|
||||
-> 页面上传记录 fileName=银行流水A.xlsx
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 原始文件名为空时,沿用 Controller 现有“文件名不能为空”校验。
|
||||
- 临时文件不存在、上传失败、解析失败时,沿用现有失败状态和错误信息记录方式。
|
||||
- 指定原始文件名只影响 multipart filename,不影响临时文件读取和异常处理。
|
||||
|
||||
## 测试计划
|
||||
|
||||
1. 后端单测或轻量验证覆盖 multipart 资源 filename:传入临时文件和原始文件名后,上传请求中的文件名应为原始文件名。
|
||||
2. 后端链路测试覆盖 `CcdiFileUploadServiceImpl`:创建上传记录后,异步上传调用使用 `record.fileName` 作为原始文件名。
|
||||
3. 状态处理测试覆盖平台返回 `uploadFileName/downloadFileName` 与初始文件名不一致时,本系统记录仍保持初始文件名。
|
||||
4. 拉取本行信息链路回归:本次改动不改变其现有文件名展示与状态处理行为。
|
||||
5. 真实页面测试:在项目详情“上传数据”页上传一个文件名带中文的流水文件,核对页面上传记录展示初始文件名,并通过日志或 mock 接收端确认流水分析平台上传接口收到的 filename 为初始文件名。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- `ccdi-project`:上传流水异步处理链路。
|
||||
- `ccdi-lsfx`:流水分析平台上传客户端和 multipart 工具。
|
||||
- `ruoyi-ui`:无需源码改动,仅做真实页面验证。
|
||||
- 数据库:无需结构变更,历史数据不回改。
|
||||
@@ -32,7 +32,7 @@ spring:
|
||||
# 主库数据源
|
||||
master:
|
||||
url: jdbc:mysql://158.234.199.250:3306/ccdi?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: dbicm
|
||||
username: db2icm
|
||||
password: Kfcx@1234
|
||||
# 从库数据源
|
||||
slave:
|
||||
@@ -94,7 +94,7 @@ spring:
|
||||
# 数据库索引
|
||||
database: 9
|
||||
# 密码
|
||||
password: Kfcx@1234
|
||||
password: ibs_pre:Kfcx@1234
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
|
||||
@@ -113,8 +113,7 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="rows" stripe border class="account-table">
|
||||
<el-table v-loading="loading" :data="rows" stripe border class="account-table">
|
||||
<el-table-column label="员工姓名" prop="staffName" min-width="120">
|
||||
<template slot-scope="scope">{{ scope.row.staffName || '-' }}</template>
|
||||
</el-table-column>
|
||||
@@ -194,10 +193,9 @@
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
</div>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
</div>
|
||||
|
||||
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="1160px" append-to-body>
|
||||
@@ -874,89 +872,19 @@ export default {
|
||||
|
||||
.account-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.board {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.account-table ::v-deep .el-table__header th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.account-table ::v-deep .el-table td,
|
||||
.account-table ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.account-table ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.account-table ::v-deep .el-table th > .cell,
|
||||
.account-table ::v-deep .el-table td > .cell {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
background: #f8f8f9;
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
@@ -985,18 +913,4 @@ export default {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -112,66 +112,64 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="baseStaffList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="姓名" align="center" prop="name" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="柜员号" align="center" prop="staffId" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="身份证号" align="center" prop="idCard" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="电话" align="center" prop="phone" width="120"/>
|
||||
<el-table-column label="年收入" align="center" prop="annualIncome" width="140"/>
|
||||
<el-table-column label="是否党员" align="center" prop="partyMember" width="100">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPartyMember(scope.row.partyMember) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === '0'" type="success">在职</el-tag>
|
||||
<el-tag v-else type="danger">离职</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
v-hasPermi="['ccdi:employee:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['ccdi:employee:edit']"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['ccdi:employee:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table v-loading="loading" :data="baseStaffList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="姓名" align="center" prop="name" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="柜员号" align="center" prop="staffId" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="身份证号" align="center" prop="idCard" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="电话" align="center" prop="phone" width="120"/>
|
||||
<el-table-column label="年收入" align="center" prop="annualIncome" width="140"/>
|
||||
<el-table-column label="是否党员" align="center" prop="partyMember" width="100">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPartyMember(scope.row.partyMember) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === '0'" type="success">在职</el-tag>
|
||||
<el-tag v-else type="danger">离职</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
v-hasPermi="['ccdi:employee:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['ccdi:employee:edit']"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['ccdi:employee:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="80%" append-to-body class="employee-edit-dialog">
|
||||
@@ -1474,12 +1472,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1491,84 +1483,6 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner,
|
||||
.query-form ::v-deep .vue-treeselect__control {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.detail-form .el-form-item {
|
||||
@@ -1581,9 +1495,8 @@ export default {
|
||||
}
|
||||
|
||||
.employee-detail-dialog .info-section {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 4px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -1596,7 +1509,7 @@ export default {
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.employee-detail-dialog .section-title i {
|
||||
@@ -1654,9 +1567,9 @@ export default {
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
@@ -1737,23 +1650,6 @@ export default {
|
||||
font-size: 13px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.employee-edit-dialog ::v-deep .el-dialog,
|
||||
.employee-detail-dialog ::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.employee-edit-dialog ::v-deep .el-dialog__header,
|
||||
.employee-detail-dialog ::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.employee-edit-dialog ::v-deep .el-dialog__body,
|
||||
.employee-detail-dialog ::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 导入结果弹窗已抽离为独立组件 ImportResultDialog -->
|
||||
|
||||
@@ -40,8 +40,7 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table :data="creditInfoList" v-loading="loading">
|
||||
<el-table :data="creditInfoList" v-loading="loading">
|
||||
<el-table-column label="姓名" prop="name" align="center" />
|
||||
<el-table-column label="身份证号" prop="idCard" align="center" />
|
||||
<el-table-column label="最近征信查询日期" align="center" width="160">
|
||||
@@ -60,16 +59,15 @@
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<el-dialog title="批量上传征信HTML" :visible.sync="uploadDialogVisible" width="720px" append-to-body>
|
||||
<el-upload
|
||||
@@ -337,12 +335,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -354,82 +346,6 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.upload-result {
|
||||
@@ -469,18 +385,4 @@ export default {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -90,8 +90,7 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="身份证号" align="center" prop="personId" width="180" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="企业名称" align="center" prop="enterpriseName" :show-overflow-tooltip="true"/>
|
||||
@@ -136,16 +135,15 @@
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
@@ -843,12 +841,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -860,83 +852,6 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
@@ -946,18 +861,4 @@ export default {
|
||||
.el-divider {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -91,8 +91,7 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="信贷客户身份证号" align="center" prop="personId" width="180"/>
|
||||
<el-table-column label="关系类型" align="center" prop="relationType" width="100">
|
||||
@@ -144,16 +143,15 @@
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
@@ -1099,12 +1097,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1116,100 +1108,9 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="formal-table-shell">
|
||||
<div>
|
||||
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="名称" align="center" prop="name" :show-overflow-tooltip="true" />
|
||||
@@ -74,43 +74,3 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -113,30 +113,6 @@ export default {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 18px 0 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
::v-deep .el-descriptions {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -207,30 +207,3 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
::v-deep .el-input__inner,
|
||||
::v-deep .el-select .el-input__inner,
|
||||
::v-deep .el-textarea__inner {
|
||||
border-radius: 3px;
|
||||
border-color: #dde3ec;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -332,9 +332,8 @@ export default {
|
||||
|
||||
.scene-tips {
|
||||
padding: 12px 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
color: #606266;
|
||||
line-height: 1.7;
|
||||
|
||||
@@ -350,7 +349,6 @@ export default {
|
||||
.el-button {
|
||||
min-width: 110px;
|
||||
margin: 0 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,18 +386,4 @@ export default {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .import-dialog-wrapper {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .import-dialog-wrapper .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .import-dialog-wrapper .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -80,25 +80,5 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -788,44 +788,3 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -250,6 +250,7 @@ export default {
|
||||
.abnormal-card {
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -274,7 +275,8 @@ export default {
|
||||
}
|
||||
|
||||
.abnormal-table {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -315,6 +317,7 @@ export default {
|
||||
.object-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
@@ -364,6 +367,7 @@ export default {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 6px;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
|
||||
@@ -217,22 +217,21 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(96vh - 64px);
|
||||
border: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
background: #f5f7fb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-analysis-header {
|
||||
padding: 32px 36px 24px;
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
padding: 20px 28px 18px;
|
||||
border-bottom: 1px solid #dbe4ef;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.project-analysis-header__main {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.project-analysis-header__title-group {
|
||||
@@ -240,52 +239,52 @@ export default {
|
||||
}
|
||||
|
||||
.project-analysis-header__eyebrow {
|
||||
color: #65758d;
|
||||
font-size: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.project-analysis-header__title {
|
||||
margin-top: 18px;
|
||||
color: #101a2b;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.project-analysis-header__meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 34px;
|
||||
gap: 12px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #bfd0e2;
|
||||
border-radius: 2px;
|
||||
border-radius: 6px;
|
||||
background: #eef4f9;
|
||||
}
|
||||
|
||||
.project-analysis-header__meta-label {
|
||||
color: #637187;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #637187;
|
||||
}
|
||||
|
||||
.project-analysis-header__meta-value {
|
||||
color: #245b8f;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #245b8f;
|
||||
}
|
||||
|
||||
.project-analysis-workspace {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 36px;
|
||||
gap: 20px;
|
||||
min-height: 700px;
|
||||
max-height: calc(96vh - 168px);
|
||||
padding: 36px;
|
||||
max-height: calc(96vh - 164px);
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
background: #ffffff;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.project-analysis-layout {
|
||||
@@ -295,57 +294,90 @@ export default {
|
||||
}
|
||||
|
||||
.project-analysis-layout__sidebar {
|
||||
flex: 0 0 420px;
|
||||
flex: 0 0 320px;
|
||||
}
|
||||
|
||||
.project-analysis-layout__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-left: 1px solid #dde3ec;
|
||||
padding-left: 36px;
|
||||
border: 1px solid #dbe4ef;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-analysis-layout__alert {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.project-analysis-tabs {
|
||||
margin-top: -6px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.project-analysis-workspace {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project-analysis-layout__sidebar {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.project-analysis-layout__main {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.project-analysis-dialog {
|
||||
margin-top: 2vh !important;
|
||||
border-radius: 0;
|
||||
background: #f5f6f8;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fb;
|
||||
overflow: hidden;
|
||||
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
padding: 18px 24px;
|
||||
border-bottom: 1px solid #dbe4ef;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.el-dialog__title {
|
||||
color: #101a2b;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 18px;
|
||||
right: 22px;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0;
|
||||
background: #f5f6f8;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
}
|
||||
|
||||
.project-analysis-tabs {
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #dbe4ef;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
background: #dde3ec;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
height: 46px;
|
||||
padding: 0 24px !important;
|
||||
padding: 0 22px !important;
|
||||
color: #2a374a;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 46px;
|
||||
}
|
||||
@@ -356,12 +388,13 @@ export default {
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
height: 3px;
|
||||
height: 2px;
|
||||
background: #245b8f;
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
padding-top: 20px;
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
<template>
|
||||
<aside class="project-analysis-sidebar">
|
||||
<div class="sidebar-profile-card">
|
||||
<section class="sidebar-profile">
|
||||
<div class="sidebar-profile__identity">
|
||||
<div class="sidebar-profile__identity-label">人物档案</div>
|
||||
<div class="sidebar-profile__name-row">
|
||||
<div class="sidebar-profile__name">{{ sidebarData.basicInfo.name || "-" }}</div>
|
||||
<div class="sidebar-risk-badge">{{ sidebarData.basicInfo.riskLevel || "-" }}</div>
|
||||
</div>
|
||||
<section class="sidebar-profile">
|
||||
<div class="sidebar-profile__name-row">
|
||||
<div>
|
||||
<div class="sidebar-section__eyebrow">人物档案</div>
|
||||
<div class="sidebar-profile__name">{{ sidebarData.basicInfo.name || "-" }}</div>
|
||||
</div>
|
||||
<div class="sidebar-profile__meta">
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">工号</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.staffCode || "-" }}</span>
|
||||
</div>
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">部门</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.department || "-" }}</span>
|
||||
</div>
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">所属项目</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.projectName || "-" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="sidebar-risk-badge">{{ sidebarData.basicInfo.riskLevel || "-" }}</div>
|
||||
</div>
|
||||
|
||||
<section class="sidebar-summary">
|
||||
<div class="sidebar-summary__title">命中模型摘要</div>
|
||||
<div class="sidebar-summary__count">
|
||||
<span class="sidebar-summary__count-label">命中模型数</span>
|
||||
<span class="sidebar-summary__count-value">{{ sidebarData.modelSummary.modelCount || "-" }}</span>
|
||||
<div class="sidebar-profile__meta">
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">工号</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.staffCode || "-" }}</span>
|
||||
</div>
|
||||
<div class="sidebar-summary__tags">
|
||||
<span class="sidebar-profile__label">核心异常标签</span>
|
||||
<div v-if="sidebarData.modelSummary.riskTags.length" class="sidebar-tag-list">
|
||||
<el-tag
|
||||
v-for="(tag, index) in sidebarData.modelSummary.riskTags"
|
||||
:key="`${formatRiskTag(tag)}-${index}`"
|
||||
size="mini"
|
||||
effect="plain"
|
||||
>
|
||||
{{ formatRiskTag(tag) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="sidebar-profile__value">暂无异常标签</span>
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">部门</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.department || "-" }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">所属项目</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.projectName || "-" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sidebar-summary">
|
||||
<div class="sidebar-section__eyebrow">命中模型摘要</div>
|
||||
<div class="sidebar-summary__count">
|
||||
<span class="sidebar-summary__count-label">命中模型数</span>
|
||||
<span class="sidebar-summary__count-value">{{ sidebarData.modelSummary.modelCount || "-" }}</span>
|
||||
</div>
|
||||
<div class="sidebar-summary__tags">
|
||||
<span class="sidebar-profile__label">核心异常标签</span>
|
||||
<div v-if="sidebarData.modelSummary.riskTags.length" class="sidebar-tag-list">
|
||||
<el-tag
|
||||
v-for="(tag, index) in sidebarData.modelSummary.riskTags"
|
||||
:key="`${formatRiskTag(tag)}-${index}`"
|
||||
size="mini"
|
||||
effect="plain"
|
||||
>
|
||||
{{ formatRiskTag(tag) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="sidebar-profile__value">暂无异常标签</span>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -82,126 +81,97 @@ export default {
|
||||
.project-analysis-sidebar {
|
||||
width: 100%;
|
||||
align-self: flex-start;
|
||||
border: 1px solid #dbe4ef;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-profile-card {
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
.sidebar-profile,
|
||||
.sidebar-summary {
|
||||
padding: 22px 24px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.sidebar-profile {
|
||||
padding: 24px 26px 20px;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
}
|
||||
|
||||
.sidebar-summary {
|
||||
padding: 22px 26px 26px;
|
||||
border-top: 1px solid #dde3ec;
|
||||
background: #fcfdfe;
|
||||
margin-top: 0;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.sidebar-profile__identity-label {
|
||||
color: #637187;
|
||||
font-size: 15px;
|
||||
.sidebar-section__eyebrow {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
letter-spacing: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.sidebar-profile__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sidebar-profile__name {
|
||||
color: #111827;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
margin-top: 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.sidebar-risk-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 64px;
|
||||
height: 28px;
|
||||
min-height: 26px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #edcaca;
|
||||
border-radius: 2px;
|
||||
border-radius: 6px;
|
||||
background: #fbefef;
|
||||
color: #ad2f2f;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
color: #ad2f2f;
|
||||
}
|
||||
|
||||
.sidebar-profile__meta {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin-top: 18px;
|
||||
gap: 14px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sidebar-profile__item {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
}
|
||||
|
||||
.sidebar-profile__item:last-child {
|
||||
border-bottom: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sidebar-profile__label,
|
||||
.sidebar-summary__count-label {
|
||||
color: #637187;
|
||||
font-size: 15px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.sidebar-profile__value,
|
||||
.sidebar-summary__count-value {
|
||||
color: #172033;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #0f172a;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sidebar-summary__title {
|
||||
margin: 0 0 18px;
|
||||
color: #223047;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebar-summary__count {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 22px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.sidebar-summary__tags {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sidebar-summary__count-label {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.sidebar-summary__count-value {
|
||||
color: #172033;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.sidebar-tag-list {
|
||||
@@ -212,14 +182,14 @@ export default {
|
||||
}
|
||||
|
||||
.sidebar-tag-list ::v-deep(.el-tag) {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
height: 26px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid #bfd0e2;
|
||||
border-radius: 2px;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #245b8f;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
line-height: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -195,7 +195,15 @@ export default {
|
||||
handleSubmitProject(data) {
|
||||
// 不需要再次调用API,因为AddProjectDialog已经处理了
|
||||
this.addDialogVisible = false
|
||||
this.getList() // 刷新列表
|
||||
if (!data || !data.projectId) {
|
||||
this.$modal.msgError("项目创建成功后未返回项目ID,无法进入上传数据页面")
|
||||
this.getList()
|
||||
return
|
||||
}
|
||||
this.$router.push({
|
||||
path: `/ccdiProject/detail/${data.projectId}`,
|
||||
query: { tab: "upload" },
|
||||
})
|
||||
},
|
||||
/** 导入历史项目 */
|
||||
handleImport() {
|
||||
|
||||
@@ -92,8 +92,7 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="transactionList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="transactionList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="采购事项ID" align="center" prop="purchaseId" width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="采购类别" align="center" prop="purchaseCategory" width="110" />
|
||||
@@ -147,16 +146,15 @@
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<el-dialog :title="title" :visible.sync="open" width="80%" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="140px">
|
||||
@@ -1372,12 +1370,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1389,84 +1381,6 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner,
|
||||
.query-form ::v-deep .el-range-editor.el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
@@ -1506,18 +1420,4 @@ export default {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="亲属身份证号" align="center" prop="personId" width="180" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="亲属姓名" align="center" prop="relationName" width="120" :show-overflow-tooltip="true" />
|
||||
@@ -166,16 +165,15 @@
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
@@ -992,12 +990,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1009,83 +1001,6 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
@@ -1095,18 +1010,4 @@ export default {
|
||||
.el-divider {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -105,8 +105,7 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="relationList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="员工姓名" align="center" prop="personName" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="员工身份证号" align="center" prop="personId" width="180"/>
|
||||
@@ -160,16 +159,15 @@
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="80%" append-to-body class="relation-edit-dialog">
|
||||
@@ -1567,12 +1565,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1584,83 +1576,6 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
@@ -1726,18 +1641,4 @@ export default {
|
||||
.empty-assets span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -132,96 +132,94 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="recruitmentList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="招聘记录编号" align="center" prop="recruitId" width="150" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="招聘项目名称" align="center" prop="recruitName" min-width="220" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="职位名称" align="center" prop="posName" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="候选人姓名" align="center" prop="candName" width="120"/>
|
||||
<el-table-column label="录用情况" align="center" prop="admitStatus" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.admitStatus === '录用'" type="success" size="small">录用</el-tag>
|
||||
<el-tag v-else-if="scope.row.admitStatus === '未录用'" type="info" size="small">未录用</el-tag>
|
||||
<el-tag v-else type="warning" size="small">放弃</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学历 / 毕业学校" align="center" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatEducationSchool(scope.row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="招聘类型" align="center" prop="recruitType" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.recruitType === 'SOCIAL' ? 'success' : 'info'" size="small">
|
||||
{{ formatRecruitType(scope.row.recruitType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="历史工作经历" align="center" prop="workExperienceCount" width="120">
|
||||
<template slot-scope="scope">
|
||||
{{ formatWorkExperienceCount(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="isPreviewMode()"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
v-hasPermi="['ccdi:staffRecruitment:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
v-if="isPreviewMode()"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['ccdi:staffRecruitment:edit']"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-if="isPreviewMode()"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['ccdi:staffRecruitment:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table v-loading="loading" :data="recruitmentList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="招聘记录编号" align="center" prop="recruitId" width="150" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="招聘项目名称" align="center" prop="recruitName" min-width="220" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="职位名称" align="center" prop="posName" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="候选人姓名" align="center" prop="candName" width="120"/>
|
||||
<el-table-column label="录用情况" align="center" prop="admitStatus" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.admitStatus === '录用'" type="success" size="small">录用</el-tag>
|
||||
<el-tag v-else-if="scope.row.admitStatus === '未录用'" type="info" size="small">未录用</el-tag>
|
||||
<el-tag v-else type="warning" size="small">放弃</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学历 / 毕业学校" align="center" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatEducationSchool(scope.row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="招聘类型" align="center" prop="recruitType" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.recruitType === 'SOCIAL' ? 'success' : 'info'" size="small">
|
||||
{{ formatRecruitType(scope.row.recruitType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="历史工作经历" align="center" prop="workExperienceCount" width="120">
|
||||
<template slot-scope="scope">
|
||||
{{ formatWorkExperienceCount(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="isPreviewMode()"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
v-hasPermi="['ccdi:staffRecruitment:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
v-if="isPreviewMode()"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['ccdi:staffRecruitment:edit']"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-if="isPreviewMode()"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['ccdi:staffRecruitment:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="80%" append-to-body>
|
||||
@@ -1517,12 +1515,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1534,83 +1526,6 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
@@ -1642,18 +1557,4 @@ export default {
|
||||
.work-experience-edit-table ::v-deep .el-textarea__inner {
|
||||
min-height: 54px !important;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -130,8 +130,7 @@
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<div class="formal-table-shell">
|
||||
<el-table v-loading="loading" :data="transferList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="transferList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="员工工号" align="center" prop="staffId" width="120"/>
|
||||
<el-table-column label="员工姓名" align="center" prop="staffName" :show-overflow-tooltip="true"/>
|
||||
@@ -170,16 +169,15 @@
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
|
||||
@@ -971,12 +969,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 24px;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -988,97 +980,5 @@ export default {
|
||||
|
||||
.query-form ::v-deep .el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 2px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-form-item__label {
|
||||
color: #637187;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-form ::v-deep .el-input__inner,
|
||||
.query-form ::v-deep .el-select .el-input__inner,
|
||||
.query-form ::v-deep .el-range-editor.el-input__inner {
|
||||
border-color: #dde3ec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .el-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb8 ::v-deep .top-right-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formal-table-shell {
|
||||
padding: 4px 0 16px;
|
||||
border: 1px solid #dde3ec;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th {
|
||||
background: #f6f8fb;
|
||||
color: #607086;
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td,
|
||||
.formal-table-shell ::v-deep .el-table th.is-leaf {
|
||||
border-bottom-color: #edf1f5;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .el-table th > .cell,
|
||||
.formal-table-shell ::v-deep .el-table td > .cell {
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .fixed-width .cell,
|
||||
.formal-table-shell ::v-deep .el-table-column--selection .cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formal-table-shell ::v-deep .pagination-container {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -57,8 +57,11 @@ assert(!dialog.includes("project-analysis-layout__main-scroll"), "主区不应
|
||||
"summary",
|
||||
"extraFields",
|
||||
"grid-template-columns: minmax(0, 1fr)",
|
||||
"border-radius: 6px",
|
||||
].forEach((token) => assert(abnormalTab.includes(token), token));
|
||||
|
||||
assert(!abnormalTab.includes("border-radius: 12px"), "异常明细内部不应继续使用独立 12px 圆角");
|
||||
|
||||
[
|
||||
"placeholder-panel",
|
||||
"placeholder-panel__title",
|
||||
|
||||
@@ -34,9 +34,9 @@ const mockSource = fs.readFileSync(
|
||||
"detailLoading",
|
||||
"detailError",
|
||||
"handleRetryDetail()",
|
||||
"border: 1px solid #dde3ec",
|
||||
"background: #f5f6f8",
|
||||
"font-size: 30px",
|
||||
"background: #f5f7fb",
|
||||
"border: 1px solid #dbe4ef",
|
||||
"border-radius: 8px",
|
||||
].forEach((token) => assert(dialog.includes(token), token));
|
||||
|
||||
[
|
||||
@@ -44,8 +44,9 @@ const mockSource = fs.readFileSync(
|
||||
'top="2vh"',
|
||||
"project-analysis-header__main",
|
||||
"project-analysis-header__meta",
|
||||
"border-left: 1px solid #dde3ec",
|
||||
"padding: 36px",
|
||||
"project-analysis-layout__main",
|
||||
"flex: 0 0 320px",
|
||||
"border-radius: 6px",
|
||||
].forEach((token) => assert(dialog.includes(token), token));
|
||||
|
||||
[
|
||||
@@ -56,7 +57,8 @@ const mockSource = fs.readFileSync(
|
||||
"overflow-y: auto",
|
||||
"max-height: calc(90vh - 120px)",
|
||||
"border-radius: 24px",
|
||||
"background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)",
|
||||
"border-radius: 20px",
|
||||
"letter-spacing: 0.08em",
|
||||
].forEach((token) => assert(!dialog.includes(token), token));
|
||||
|
||||
[
|
||||
|
||||
@@ -25,28 +25,27 @@ const entry = fs.readFileSync(
|
||||
);
|
||||
|
||||
[
|
||||
"sidebar-profile-card",
|
||||
"sidebar-profile",
|
||||
"sidebar-profile__name",
|
||||
"sidebar-profile__identity-label",
|
||||
"sidebar-risk-badge",
|
||||
"sidebar-profile__meta",
|
||||
"sidebar-summary",
|
||||
"sidebar-summary__title",
|
||||
"sidebar-summary__count",
|
||||
"sidebar-tag-list",
|
||||
"formatRiskTag",
|
||||
"tag.ruleName",
|
||||
"flex-wrap: wrap",
|
||||
"justify-content: space-between",
|
||||
"border: 1px solid #dde3ec",
|
||||
"align-items: flex-start",
|
||||
"border: 1px solid #dbe4ef",
|
||||
"border-radius: 6px",
|
||||
].forEach((token) => assert(sidebar.includes(token), token));
|
||||
|
||||
assert(!sidebar.includes("当前命中模型"), "命中模型摘要应移除当前命中模型字段");
|
||||
assert(!sidebar.includes("排查记录摘要"), "侧栏应移除排查记录摘要");
|
||||
assert(!sidebar.includes("position: sticky"), "左侧整卡不应保持固定");
|
||||
assert(!sidebar.includes("border-radius: 20px"), "侧栏不应继续保留旧圆角卡片样式");
|
||||
assert(!sidebar.includes("border-radius: 20px"), "侧栏不应继续保留旧大圆角卡片样式");
|
||||
assert(!sidebar.includes("background: rgba(255, 255, 255, 0.9)"), "侧栏不应继续保留旧半透明卡片底色");
|
||||
assert(!sidebar.includes("justify-content: space-between"), "不应继续以表单式左右对齐作为主体布局");
|
||||
|
||||
assert(!sidebar.includes("关系人画像"), "侧栏不应扩展到额外区块");
|
||||
assert(!sidebar.includes("资产分布"), "侧栏不应扩展到额外区块");
|
||||
@@ -54,7 +53,7 @@ assert(!sidebar.includes("资产分布"), "侧栏不应扩展到额外区块");
|
||||
[
|
||||
"this.detailData && this.detailData.basicInfo",
|
||||
"...(this.modelSummary || {})",
|
||||
"align-items: flex-end",
|
||||
"align-items: flex-start",
|
||||
].forEach((token) => assert(dialog.includes(token), token));
|
||||
|
||||
assert(entry.includes(':model-summary="projectAnalysisModelSummary"'), "入口页应继续透传模型摘要");
|
||||
|
||||
Reference in New Issue
Block a user