1.0
longz
2026-06-24 e45e74e5ae207f7642be9a88ed99b593de89fb3b
backend/src/main/java/com/sinata/appointment/service/ConfigApplicationService.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.alibaba.excel.EasyExcel;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sinata.appointment.constants.BizErrorCode;
@@ -17,6 +18,7 @@
import com.sinata.appointment.dto.FieldConfigPatchRequest;
import com.sinata.appointment.dto.SensitiveWordCreateRequest;
import com.sinata.appointment.dto.SensitiveWordEnabledPatchRequest;
import com.sinata.appointment.dto.SensitiveWordImportResultVo;
import com.sinata.appointment.dto.SensitiveWordListVo;
import com.sinata.appointment.dto.SensitiveWordUpdateRequest;
import com.sinata.appointment.dto.VisitReasonCreateRequest;
@@ -38,12 +40,17 @@
import com.sinata.appointment.mapper.SysBizSettingMapper;
import com.sinata.appointment.mapper.SysDeptMapper;
import com.sinata.appointment.util.UuidAssert;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -53,6 +60,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@Service
@RequiredArgsConstructor
@@ -180,6 +188,74 @@
        loadActiveSensitiveWord(id, uuid);
        bizSensitiveWordMapper.deleteById(id);
        sensitiveWordFilterService.evictCache();
    }
    @Transactional(rollbackFor = Exception.class)
    public SensitiveWordImportResultVo importSensitiveWords(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "导入文件不能为空");
        }
        String filename = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase();
        if (!filename.endsWith(".xlsx") && !filename.endsWith(".xls")) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "请上传 xls 或 xlsx 文件");
        }
        List<Map<Integer, String>> rows;
        try {
            rows = EasyExcel.read(file.getInputStream()).sheet().headRowNumber(0).doReadSync();
        } catch (IOException ex) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "读取导入文件失败");
        }
        SensitiveWordImportResultVo result = new SensitiveWordImportResultVo();
        Set<String> fileWords = new LinkedHashSet<>();
        Set<String> existingWords = loadExistingSensitiveWords();
        int rowNo = 0;
        for (Map<Integer, String> row : rows) {
            rowNo++;
            String raw = row == null ? null : row.get(0);
            if (rowNo == 1 && isSensitiveWordHeader(raw)) {
                continue;
            }
            if (!StringUtils.hasText(raw)) {
                continue;
            }
            result.setTotalRows(result.getTotalRows() + 1);
            String word;
            try {
                word = normalizeSensitiveWord(raw);
            } catch (BizException ex) {
                result.setInvalidCount(result.getInvalidCount() + 1);
                addImportError(result, "第" + rowNo + "行:" + ex.getMessage());
                continue;
            }
            if (fileWords.contains(word) || existingWords.contains(word)) {
                result.setDuplicateCount(result.getDuplicateCount() + 1);
                continue;
            }
            BizSensitiveWord entity = new BizSensitiveWord();
            entity.setWord(word);
            entity.setEnabled(1);
            bizSensitiveWordMapper.insert(entity);
            fileWords.add(word);
            existingWords.add(word);
            result.setSuccessCount(result.getSuccessCount() + 1);
        }
        if (result.getSuccessCount() > 0) {
            sensitiveWordFilterService.evictCache();
        }
        return result;
    }
    public byte[] sensitiveWordImportTemplateExcel() {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            List<List<String>> head = List.of(List.of("敏感词"));
            List<List<String>> rows = List.of(List.of("示例敏感词"));
            EasyExcel.write(bos).head(head).sheet("敏感词导入").doWrite(rows);
            return bos.toByteArray();
        } catch (IOException ex) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "生成导入模板失败");
        }
    }
    public List<BizFieldConfig> listFieldConfigs() {
@@ -572,6 +648,35 @@
        return normalized;
    }
    private Set<String> loadExistingSensitiveWords() {
        List<BizSensitiveWord> rows =
                bizSensitiveWordMapper.selectList(
                        new LambdaQueryWrapper<BizSensitiveWord>()
                                .eq(BizSensitiveWord::getDelFlag, 0)
                                .select(BizSensitiveWord::getWord));
        Set<String> words = new HashSet<>();
        for (BizSensitiveWord row : rows) {
            if (StringUtils.hasText(row.getWord())) {
                words.add(row.getWord().trim());
            }
        }
        return words;
    }
    private boolean isSensitiveWordHeader(String value) {
        if (!StringUtils.hasText(value)) {
            return false;
        }
        String text = value.trim();
        return "敏感词".equals(text) || "敏感词库".equals(text) || "word".equalsIgnoreCase(text);
    }
    private void addImportError(SensitiveWordImportResultVo result, String message) {
        if (result.getErrorMessages().size() < 20) {
            result.getErrorMessages().add(message);
        }
    }
    private void assertReasonNameUnique(String reasonName, Long excludeId) {
        LambdaQueryWrapper<BizVisitReason> q =
                new LambdaQueryWrapper<BizVisitReason>()