longz
2026-08-11 2a3c3cdbf2398695d61633e072f98ee2757e9e27
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;
@@ -15,12 +16,18 @@
import com.sinata.appointment.dto.ContentSettingsSaveRequest;
import com.sinata.appointment.dto.ContentSettingsVo;
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;
import com.sinata.appointment.dto.VisitReasonListVo;
import com.sinata.appointment.dto.VisitReasonUpdateRequest;
import com.sinata.appointment.entity.BizApprovalNodeConfig;
import com.sinata.appointment.entity.BizFieldConfig;
import com.sinata.appointment.entity.BizHolidayDate;
import com.sinata.appointment.entity.BizSensitiveWord;
import com.sinata.appointment.entity.BizVisitReason;
import com.sinata.appointment.entity.SysBizSetting;
import com.sinata.appointment.entity.SysDept;
@@ -28,16 +35,22 @@
import com.sinata.appointment.mapper.BizApprovalNodeConfigMapper;
import com.sinata.appointment.mapper.BizFieldConfigMapper;
import com.sinata.appointment.mapper.BizHolidayDateMapper;
import com.sinata.appointment.mapper.BizSensitiveWordMapper;
import com.sinata.appointment.mapper.BizVisitReasonMapper;
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;
@@ -47,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
@@ -58,10 +72,12 @@
    private final BizFieldConfigMapper bizFieldConfigMapper;
    private final BizApprovalNodeConfigMapper bizApprovalNodeConfigMapper;
    private final BizHolidayDateMapper bizHolidayDateMapper;
    private final BizSensitiveWordMapper bizSensitiveWordMapper;
    private final SysBizSettingMapper sysBizSettingMapper;
    private final SysDeptMapper sysDeptMapper;
    private final SysBizSettingCacheService sysBizSettingCacheService;
    private final BizHolidayDateCacheService bizHolidayDateCacheService;
    private final SensitiveWordFilterService sensitiveWordFilterService;
    private final CacheManager cacheManager;
    private final ObjectMapper objectMapper;
@@ -89,8 +105,8 @@
                        new Page<>(page, size),
                        new LambdaQueryWrapper<BizVisitReason>()
                                .eq(BizVisitReason::getDelFlag, 0)
                                .orderByDesc(BizVisitReason::getCreateTime)
                                .orderByDesc(BizVisitReason::getId));
                                .orderByAsc(BizVisitReason::getSortOrder)
                                .orderByAsc(BizVisitReason::getId));
        return pg.convert(this::toVisitReasonListVo);
    }
@@ -102,7 +118,7 @@
        row.setReasonName(reasonName);
        row.setReasonCode("VR_" + UUID.randomUUID().toString().replace("-", ""));
        row.setEnabled(1);
        row.setSortOrder(0);
        row.setSortOrder(normalizeSortOrder(req.getSortOrder()));
        bizVisitReasonMapper.insert(row);
        evictMiniVisitReasonCache();
    }
@@ -113,6 +129,7 @@
        String reasonName = req.getReasonName().trim();
        assertReasonNameUnique(reasonName, id);
        row.setReasonName(reasonName);
        row.setSortOrder(normalizeSortOrder(req.getSortOrder()));
        bizVisitReasonMapper.updateById(row);
        evictMiniVisitReasonCache();
    }
@@ -122,6 +139,123 @@
        loadActiveVisitReason(id, uuid);
        bizVisitReasonMapper.deleteById(id);
        evictMiniVisitReasonCache();
    }
    public IPage<SensitiveWordListVo> pageSensitiveWords(long page, long size, String keyword) {
        String normalizedKeyword = trimToNull(keyword);
        Page<BizSensitiveWord> pg =
                bizSensitiveWordMapper.selectPage(
                        new Page<>(page, size),
                        new LambdaQueryWrapper<BizSensitiveWord>()
                                .eq(BizSensitiveWord::getDelFlag, 0)
                                .like(StringUtils.hasText(normalizedKeyword), BizSensitiveWord::getWord, normalizedKeyword)
                                .orderByDesc(BizSensitiveWord::getCreateTime)
                                .orderByDesc(BizSensitiveWord::getId));
        return pg.convert(this::toSensitiveWordListVo);
    }
    @Transactional(rollbackFor = Exception.class)
    public void createSensitiveWord(SensitiveWordCreateRequest req) {
        String word = normalizeSensitiveWord(req.getWord());
        assertSensitiveWordUnique(word, null);
        BizSensitiveWord row = new BizSensitiveWord();
        row.setWord(word);
        row.setEnabled(1);
        bizSensitiveWordMapper.insert(row);
        sensitiveWordFilterService.evictCache();
    }
    @Transactional(rollbackFor = Exception.class)
    public void updateSensitiveWord(Long id, SensitiveWordUpdateRequest req) {
        BizSensitiveWord row = loadActiveSensitiveWord(id, req.getUuid());
        String word = normalizeSensitiveWord(req.getWord());
        assertSensitiveWordUnique(word, id);
        row.setWord(word);
        bizSensitiveWordMapper.updateById(row);
        sensitiveWordFilterService.evictCache();
    }
    @Transactional(rollbackFor = Exception.class)
    public void updateSensitiveWordEnabled(Long id, SensitiveWordEnabledPatchRequest req) {
        BizSensitiveWord row = loadActiveSensitiveWord(id, req.getUuid());
        row.setEnabled(req.getEnabled());
        bizSensitiveWordMapper.updateById(row);
        sensitiveWordFilterService.evictCache();
    }
    @Transactional(rollbackFor = Exception.class)
    public void deleteSensitiveWord(Long id, String uuid) {
        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() {
@@ -421,6 +555,13 @@
        return html == null ? "" : html;
    }
    private static String trimToNull(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }
    private void upsertSetting(String key, String value) {
        SysBizSetting s =
                sysBizSettingMapper.selectOne(
@@ -474,6 +615,68 @@
        return row;
    }
    private BizSensitiveWord loadActiveSensitiveWord(Long id, String uuid) {
        BizSensitiveWord row = bizSensitiveWordMapper.selectById(id);
        if (row == null || (row.getDelFlag() != null && row.getDelFlag() == 1)) {
            throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "敏感词不存在");
        }
        UuidAssert.match(row, uuid);
        return row;
    }
    private void assertSensitiveWordUnique(String word, Long excludeId) {
        LambdaQueryWrapper<BizSensitiveWord> q =
                new LambdaQueryWrapper<BizSensitiveWord>()
                        .eq(BizSensitiveWord::getDelFlag, 0)
                        .eq(BizSensitiveWord::getWord, word);
        if (excludeId != null) {
            q.ne(BizSensitiveWord::getId, excludeId);
        }
        if (bizSensitiveWordMapper.selectCount(q) > 0) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "敏感词已存在");
        }
    }
    private String normalizeSensitiveWord(String word) {
        String normalized = trimToNull(word);
        if (!StringUtils.hasText(normalized)) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "敏感词不能为空");
        }
        if (normalized.length() > 50) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "敏感词不能超过50个字");
        }
        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>()
@@ -487,11 +690,27 @@
        }
    }
    private int normalizeSortOrder(Integer sortOrder) {
        return sortOrder == null ? 0 : Math.max(0, sortOrder);
    }
    private VisitReasonListVo toVisitReasonListVo(BizVisitReason e) {
        VisitReasonListVo vo = new VisitReasonListVo();
        vo.setId(e.getId());
        vo.setUuid(e.getUuid());
        vo.setReasonCode(e.getReasonCode());
        vo.setReasonName(e.getReasonName());
        vo.setSortOrder(e.getSortOrder());
        vo.setCreateTime(e.getCreateTime());
        return vo;
    }
    private SensitiveWordListVo toSensitiveWordListVo(BizSensitiveWord e) {
        SensitiveWordListVo vo = new SensitiveWordListVo();
        vo.setId(e.getId());
        vo.setUuid(e.getUuid());
        vo.setWord(e.getWord());
        vo.setEnabled(e.getEnabled());
        vo.setCreateTime(e.getCreateTime());
        return vo;
    }