| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | @Service |
| | | @RequiredArgsConstructor |
| | |
| | | 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; |
| | | |
| | |
| | | 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); |
| | | } |
| | | |
| | |
| | | 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(); |
| | | } |
| | |
| | | String reasonName = req.getReasonName().trim(); |
| | | assertReasonNameUnique(reasonName, id); |
| | | row.setReasonName(reasonName); |
| | | row.setSortOrder(normalizeSortOrder(req.getSortOrder())); |
| | | bizVisitReasonMapper.updateById(row); |
| | | evictMiniVisitReasonCache(); |
| | | } |
| | |
| | | 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() { |
| | |
| | | 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( |
| | |
| | | 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>() |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | | } |