package com.sinata.appointment.service; 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.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.sinata.appointment.constants.BizErrorCode; import com.sinata.appointment.constants.CacheNames; import com.sinata.appointment.constants.AppointmentBizConstants; import com.sinata.appointment.constants.FieldConfigLockedCodes; import com.sinata.appointment.constants.SystemSettingKeys; import com.sinata.appointment.dto.BookingSettingsSaveRequest; import com.sinata.appointment.dto.BookingSettingsVo; import com.sinata.appointment.dto.ContentSettingsSaveRequest; import com.sinata.appointment.dto.ContentSettingsVo; import com.sinata.appointment.dto.FieldConfigPatchRequest; 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.BizVisitReason; import com.sinata.appointment.entity.SysBizSetting; import com.sinata.appointment.entity.SysDept; import com.sinata.appointment.exception.BizException; import com.sinata.appointment.mapper.BizApprovalNodeConfigMapper; import com.sinata.appointment.mapper.BizFieldConfigMapper; import com.sinata.appointment.mapper.BizHolidayDateMapper; 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.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor public class ConfigApplicationService { private static final TypeReference> STRING_LIST_TYPE = new TypeReference<>() {}; private final BizVisitReasonMapper bizVisitReasonMapper; private final BizFieldConfigMapper bizFieldConfigMapper; private final BizApprovalNodeConfigMapper bizApprovalNodeConfigMapper; private final BizHolidayDateMapper bizHolidayDateMapper; private final SysBizSettingMapper sysBizSettingMapper; private final SysDeptMapper sysDeptMapper; private final SysBizSettingCacheService sysBizSettingCacheService; private final BizHolidayDateCacheService bizHolidayDateCacheService; private final CacheManager cacheManager; private final ObjectMapper objectMapper; public List listVisitReasons() { return bizVisitReasonMapper.selectList( new LambdaQueryWrapper() .eq(BizVisitReason::getDelFlag, 0) .eq(BizVisitReason::getEnabled, 1) .orderByAsc(BizVisitReason::getSortOrder)); } @Transactional(rollbackFor = Exception.class) public void saveVisitReason(BizVisitReason entity) { if (entity.getId() == null) { bizVisitReasonMapper.insert(entity); } else { bizVisitReasonMapper.updateById(entity); } evictMiniVisitReasonCache(); } public IPage pageVisitReasons(long page, long size) { Page pg = bizVisitReasonMapper.selectPage( new Page<>(page, size), new LambdaQueryWrapper() .eq(BizVisitReason::getDelFlag, 0) .orderByDesc(BizVisitReason::getCreateTime) .orderByDesc(BizVisitReason::getId)); return pg.convert(this::toVisitReasonListVo); } @Transactional(rollbackFor = Exception.class) public void createVisitReason(VisitReasonCreateRequest req) { String reasonName = req.getReasonName().trim(); assertReasonNameUnique(reasonName, null); BizVisitReason row = new BizVisitReason(); row.setReasonName(reasonName); row.setReasonCode("VR_" + UUID.randomUUID().toString().replace("-", "")); row.setEnabled(1); row.setSortOrder(0); bizVisitReasonMapper.insert(row); evictMiniVisitReasonCache(); } @Transactional(rollbackFor = Exception.class) public void updateVisitReason(Long id, VisitReasonUpdateRequest req) { BizVisitReason row = loadActiveVisitReason(id, req.getUuid()); String reasonName = req.getReasonName().trim(); assertReasonNameUnique(reasonName, id); row.setReasonName(reasonName); bizVisitReasonMapper.updateById(row); evictMiniVisitReasonCache(); } @Transactional(rollbackFor = Exception.class) public void deleteVisitReason(Long id, String uuid) { loadActiveVisitReason(id, uuid); bizVisitReasonMapper.deleteById(id); evictMiniVisitReasonCache(); } public List listFieldConfigs() { return bizFieldConfigMapper.selectList( new LambdaQueryWrapper() .eq(BizFieldConfig::getDelFlag, 0) .orderByAsc(BizFieldConfig::getSortOrder)); } @Transactional(rollbackFor = Exception.class) public void saveFieldConfig(BizFieldConfig entity) { if (entity.getId() == null) { bizFieldConfigMapper.insert(entity); } else { bizFieldConfigMapper.updateById(entity); } evictMiniFieldConfigCache(); } @Transactional(rollbackFor = Exception.class) public void patchFieldConfig(Long id, FieldConfigPatchRequest req) { if (req.getVisible() == null && req.getRequiredFlag() == null) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "请至少提交一项修改"); } BizFieldConfig e = bizFieldConfigMapper.selectById(id); if (e == null || (e.getDelFlag() != null && e.getDelFlag() == 1)) { throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "字段配置不存在"); } UuidAssert.match(e, req.getUuid()); if (FieldConfigLockedCodes.isLocked(e.getFieldCode())) { e.setVisible(1); e.setRequiredFlag(1); bizFieldConfigMapper.updateById(e); return; } if (req.getVisible() != null) { e.setVisible(req.getVisible()); } if (e.getVisible() != null && e.getVisible() == 0) { e.setRequiredFlag(0); } else if (req.getRequiredFlag() != null) { e.setRequiredFlag(req.getRequiredFlag()); } bizFieldConfigMapper.updateById(e); evictMiniFieldConfigCache(); } public BookingSettingsVo getBookingSettings() { BookingSettingsVo vo = new BookingSettingsVo(); String mode = getSettingOrDefault( SystemSettingKeys.BOOKING_RANGE_MODE, AppointmentBizConstants.TIME_MODE_SINGLE) .trim() .toUpperCase(); if (!AppointmentBizConstants.TIME_MODE_SINGLE.equals(mode) && !AppointmentBizConstants.TIME_MODE_MULTI.equals(mode)) { mode = AppointmentBizConstants.TIME_MODE_SINGLE; } vo.setRangeMode(mode); String days = getSettingOrDefault(SystemSettingKeys.BOOKING_MAX_CONSECUTIVE_DAYS, "7"); int md = 7; try { md = Math.max(1, Integer.parseInt(days.trim())); } catch (NumberFormatException ignored) { // keep default } vo.setMaxConsecutiveDays(md); vo.setHolidayDates(listHolidayDates()); String gate = getSettingOrDefault(SystemSettingKeys.VEHICLE_BARRIER_GATE_ENABLED, "0"); vo.setVehicleBarrierEnabled("1".equals(gate.trim())); return vo; } public ContentSettingsVo getContentSettings() { ContentSettingsVo vo = new ContentSettingsVo(); vo.setAboutUsHtml(getSettingOrDefault(SystemSettingKeys.CONTENT_ABOUT_US_HTML, "")); vo.setParkNoticeHtml(getSettingOrDefault(SystemSettingKeys.CONTENT_PARK_NOTICE_HTML, "")); vo.setParkFlowHtml(getSettingOrDefault(SystemSettingKeys.CONTENT_PARK_FLOW_HTML, "")); vo.setAboutUsEnabled("1".equals(getSettingOrDefault(SystemSettingKeys.CONTENT_ABOUT_US_ENABLED, "0").trim())); return vo; } @Transactional(rollbackFor = Exception.class) public void saveBookingSettings(BookingSettingsSaveRequest req) { String mode = req.getRangeMode().trim().toUpperCase(); if (!AppointmentBizConstants.TIME_MODE_SINGLE.equals(mode) && !AppointmentBizConstants.TIME_MODE_MULTI.equals(mode)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "预约时段类型无效"); } if (AppointmentBizConstants.TIME_MODE_MULTI.equals(mode)) { if (req.getMaxConsecutiveDays() == null || req.getMaxConsecutiveDays() < 1) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "请输入最长连续预约天数(正整数)"); } if (req.getMaxConsecutiveDays() > 999) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "最长连续预约天数不能超过 999"); } upsertSetting(SystemSettingKeys.BOOKING_MAX_CONSECUTIVE_DAYS, String.valueOf(req.getMaxConsecutiveDays())); } upsertSetting(SystemSettingKeys.BOOKING_RANGE_MODE, mode); List hol = req.getHolidayDates() == null ? List.of() : req.getHolidayDates(); List normalized = normalizeHolidayDates(hol); replaceHolidayDates(normalized); bizHolidayDateCacheService.evictAllHolidayDates(); // 兼容旧实现:避免 setting_value 持续变大,保存后清空 JSON upsertSetting(SystemSettingKeys.BOOKING_HOLIDAY_DATES, "[]"); upsertSetting( SystemSettingKeys.VEHICLE_BARRIER_GATE_ENABLED, Boolean.TRUE.equals(req.getVehicleBarrierEnabled()) ? "1" : "0"); } @Transactional(rollbackFor = Exception.class) public void saveContentSettings(ContentSettingsSaveRequest req) { upsertSetting(SystemSettingKeys.CONTENT_ABOUT_US_HTML, normalizeHtml(req.getAboutUsHtml())); upsertSetting(SystemSettingKeys.CONTENT_PARK_NOTICE_HTML, normalizeHtml(req.getParkNoticeHtml())); upsertSetting(SystemSettingKeys.CONTENT_PARK_FLOW_HTML, normalizeHtml(req.getParkFlowHtml())); upsertSetting(SystemSettingKeys.CONTENT_ABOUT_US_ENABLED, Boolean.TRUE.equals(req.getAboutUsEnabled()) ? "1" : "0"); } private List listHolidayDates() { List rows = bizHolidayDateMapper.selectList( new LambdaQueryWrapper() .eq(BizHolidayDate::getDelFlag, 0) .orderByAsc(BizHolidayDate::getHolidayDate)); if (rows != null && !rows.isEmpty()) { return rows.stream() .map(BizHolidayDate::getHolidayDate) .filter(x -> x != null) .map(LocalDate::toString) .collect(Collectors.toList()); } // 兜底:旧版本存于 setting_value(JSON);若存在则读出来返回 String holJson = getSettingOrDefault(SystemSettingKeys.BOOKING_HOLIDAY_DATES, "[]"); try { List raw = objectMapper.readValue(holJson, STRING_LIST_TYPE); if (raw != null && !raw.isEmpty()) { return normalizeHolidayDates(raw); } } catch (Exception ignored) { // ignore } return List.of(); } private void replaceHolidayDates(List normalized) { List existing = bizHolidayDateMapper.selectList( new LambdaQueryWrapper() .eq(BizHolidayDate::getDelFlag, 0) .orderByAsc(BizHolidayDate::getHolidayDate)); Set existingSet = existing.stream() .map(BizHolidayDate::getHolidayDate) .filter(x -> x != null) .collect(Collectors.toSet()); Set targetSet = normalized.stream().map(LocalDate::parse).collect(Collectors.toCollection(java.util.LinkedHashSet::new)); // 删除:存在但目标不包含的 List toDelete = existingSet.stream().filter(d -> !targetSet.contains(d)).collect(Collectors.toList()); if (!toDelete.isEmpty()) { bizHolidayDateMapper.deleteByHolidayDates(toDelete); } // 新增:目标包含但不存在的 for (LocalDate d : targetSet) { if (existingSet.contains(d)) { continue; } BizHolidayDate row = new BizHolidayDate(); row.setHolidayDate(d); bizHolidayDateMapper.insert(row); } } public List listApprovalNodes() { return bizApprovalNodeConfigMapper.selectList( new LambdaQueryWrapper() .eq(BizApprovalNodeConfig::getDelFlag, 0) .orderByAsc(BizApprovalNodeConfig::getSortOrder)); } @Transactional(rollbackFor = Exception.class) public void saveApprovalNode(BizApprovalNodeConfig entity) { if (entity.getId() == null) { bizApprovalNodeConfigMapper.insert(entity); } else { bizApprovalNodeConfigMapper.updateById(entity); } } @Transactional(rollbackFor = Exception.class) public void updateApprovalNodeEnabled(Long id, String uuid, int enabled) { BizApprovalNodeConfig e = bizApprovalNodeConfigMapper.selectById(id); if (e == null || (e.getDelFlag() != null && e.getDelFlag() == 1)) { throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "节点不存在"); } UuidAssert.match(e, uuid); e.setEnabled(enabled); bizApprovalNodeConfigMapper.updateById(e); } public Long getVisitCompetentDeptId() { String raw = getSettingOrDefault(SystemSettingKeys.VISIT_COMPETENT_DEPT_ID, null); if (!StringUtils.hasText(raw)) { return null; } try { long v = Long.parseLong(raw.trim()); return v > 0 ? v : null; } catch (NumberFormatException ex) { return null; } } @Transactional(rollbackFor = Exception.class) public void saveVisitCompetentDept(Long deptId) { if (deptId != null && deptId > 0) { SysDept d = sysDeptMapper.selectById(deptId); if (d == null || (d.getDelFlag() != null && d.getDelFlag() == 1)) { throw new BizException(BizErrorCode.DEPT_PARENT_INVALID, "部门不存在或已删除"); } } String val = (deptId != null && deptId > 0) ? String.valueOf(deptId) : null; upsertSetting(SystemSettingKeys.VISIT_COMPETENT_DEPT_ID, val); } private String getSettingOrDefault(String key, String defaultVal) { String value = sysBizSettingCacheService.getValue(key); if (!StringUtils.hasText(value)) { return defaultVal; } return value; } private static String normalizeHtml(String html) { return html == null ? "" : html; } private void upsertSetting(String key, String value) { SysBizSetting s = sysBizSettingMapper.selectOne( new LambdaQueryWrapper().eq(SysBizSetting::getSettingKey, key)); if (s == null) { SysBizSetting row = new SysBizSetting(); row.setSettingKey(key); row.setSettingValue(value); sysBizSettingMapper.insert(row); } else { s.setSettingValue(value); sysBizSettingMapper.updateById(s); } sysBizSettingCacheService.evict(key); } private static List normalizeHolidayDates(List raw) { LinkedHashSet set = new LinkedHashSet<>(); for (String s : raw) { if (!StringUtils.hasText(s)) { continue; } String t = s.trim(); try { LocalDate.parse(t); } catch (DateTimeParseException ex) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "非法日期: " + t); } set.add(t); } return set.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList()); } private BizVisitReason loadActiveVisitReason(Long id, String uuid) { BizVisitReason row = bizVisitReasonMapper.selectById(id); if (row == null || (row.getDelFlag() != null && row.getDelFlag() == 1)) { throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "来访事由不存在"); } // 兼容历史脏数据:若库中 uuid 为空,则仅按 id 处理;正常数据仍做 id+uuid 校验 if (StringUtils.hasText(row.getUuid())) { UuidAssert.match(row, uuid); } return row; } private void assertReasonNameUnique(String reasonName, Long excludeId) { LambdaQueryWrapper q = new LambdaQueryWrapper() .eq(BizVisitReason::getDelFlag, 0) .eq(BizVisitReason::getReasonName, reasonName); if (excludeId != null) { q.ne(BizVisitReason::getId, excludeId); } if (bizVisitReasonMapper.selectCount(q) > 0) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访事由已存在"); } } private VisitReasonListVo toVisitReasonListVo(BizVisitReason e) { VisitReasonListVo vo = new VisitReasonListVo(); vo.setId(e.getId()); vo.setUuid(e.getUuid()); vo.setReasonName(e.getReasonName()); vo.setCreateTime(e.getCreateTime()); return vo; } private void evictMiniVisitReasonCache() { Cache cache = cacheManager.getCache(CacheNames.MINI_LOOKUP); if (cache != null) { cache.evict(CacheNames.MINI_LOOKUP_VISIT_REASONS_KEY); } } private void evictMiniFieldConfigCache() { Cache cache = cacheManager.getCache(CacheNames.MINI_LOOKUP); if (cache != null) { cache.evict(CacheNames.MINI_LOOKUP_FIELD_CONFIGS_KEY); } } }