From 9d4cbff7464ebfd7803c2ce53ded49d0f73fe011 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期一, 29 六月 2026 20:33:55 +0800
Subject: [PATCH] 初步调整fix

---
 backend/src/main/java/com/sinata/appointment/service/ConfigApplicationService.java |  290 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 287 insertions(+), 3 deletions(-)

diff --git a/backend/src/main/java/com/sinata/appointment/service/ConfigApplicationService.java b/backend/src/main/java/com/sinata/appointment/service/ConfigApplicationService.java
index 839bcf0..9dc8214 100644
--- a/backend/src/main/java/com/sinata/appointment/service/ConfigApplicationService.java
+++ b/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,15 +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;
@@ -46,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
@@ -57,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;
 
@@ -88,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);
     }
 
@@ -101,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();
     }
@@ -112,6 +129,7 @@
         String reasonName = req.getReasonName().trim();
         assertReasonNameUnique(reasonName, id);
         row.setReasonName(reasonName);
+        row.setSortOrder(normalizeSortOrder(req.getSortOrder()));
         bizVisitReasonMapper.updateById(row);
         evictMiniVisitReasonCache();
     }
@@ -121,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() {
@@ -230,6 +365,62 @@
         upsertSetting(
                 SystemSettingKeys.VEHICLE_BARRIER_GATE_ENABLED,
                 Boolean.TRUE.equals(req.getVehicleBarrierEnabled()) ? "1" : "0");
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void toggleHolidayDate(String date, boolean holiday) {
+        LocalDate target = parseDate(date);
+        if (holiday) {
+            Long exists =
+                    bizHolidayDateMapper.selectCount(
+                            new LambdaQueryWrapper<BizHolidayDate>()
+                                    .eq(BizHolidayDate::getDelFlag, 0)
+                                    .eq(BizHolidayDate::getHolidayDate, target));
+            if (exists == null || exists == 0L) {
+                BizHolidayDate row = new BizHolidayDate();
+                row.setHolidayDate(target);
+                bizHolidayDateMapper.insert(row);
+            }
+        } else {
+            bizHolidayDateMapper.deleteByHolidayDates(List.of(target));
+        }
+        bizHolidayDateCacheService.evictAllHolidayDates();
+        upsertSetting(SystemSettingKeys.BOOKING_HOLIDAY_DATES, "[]");
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void addMonthWeekends(String month) {
+        YearMonth yearMonth;
+        try {
+            yearMonth = YearMonth.parse(month.trim());
+        } catch (Exception ex) {
+            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "非法月份: " + month);
+        }
+        Set<LocalDate> existsSet =
+                bizHolidayDateMapper.selectList(
+                                new LambdaQueryWrapper<BizHolidayDate>()
+                                        .eq(BizHolidayDate::getDelFlag, 0)
+                                        .ge(BizHolidayDate::getHolidayDate, yearMonth.atDay(1))
+                                        .le(BizHolidayDate::getHolidayDate, yearMonth.atEndOfMonth()))
+                        .stream()
+                        .map(BizHolidayDate::getHolidayDate)
+                        .collect(Collectors.toSet());
+
+        for (int d = 1; d <= yearMonth.lengthOfMonth(); d++) {
+            LocalDate current = yearMonth.atDay(d);
+            int dayOfWeek = current.getDayOfWeek().getValue(); // 1..7 (Mon..Sun)
+            if (dayOfWeek != 6 && dayOfWeek != 7) {
+                continue;
+            }
+            if (existsSet.contains(current)) {
+                continue;
+            }
+            BizHolidayDate row = new BizHolidayDate();
+            row.setHolidayDate(current);
+            bizHolidayDateMapper.insert(row);
+        }
+        bizHolidayDateCacheService.evictAllHolidayDates();
+        upsertSetting(SystemSettingKeys.BOOKING_HOLIDAY_DATES, "[]");
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -364,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(
@@ -397,6 +595,14 @@
         return set.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList());
     }
 
+    private LocalDate parseDate(String date) {
+        try {
+            return LocalDate.parse(date.trim());
+        } catch (Exception ex) {
+            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "非法日期: " + date);
+        }
+    }
+
     private BizVisitReason loadActiveVisitReason(Long id, String uuid) {
         BizVisitReason row = bizVisitReasonMapper.selectById(id);
         if (row == null || (row.getDelFlag() != null && row.getDelFlag() == 1)) {
@@ -407,6 +613,68 @@
             UuidAssert.match(row, uuid);
         }
         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) {
@@ -422,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;
     }

--
Gitblit v1.7.1