package com.sinata.appointment.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.sinata.appointment.constants.AppointmentFieldCodes; import com.sinata.appointment.constants.BizErrorCode; import com.sinata.appointment.dto.MiniVisitorCreateRequest; import com.sinata.appointment.dto.MiniVisitorDetailVo; import com.sinata.appointment.dto.MiniVisitorListVo; import com.sinata.appointment.dto.MiniVisitorUpdateRequest; import com.sinata.appointment.entity.BizFieldConfig; import com.sinata.appointment.entity.MpUser; import com.sinata.appointment.entity.MpVisitor; import com.sinata.appointment.exception.BizException; import com.sinata.appointment.mapper.BizFieldConfigMapper; import com.sinata.appointment.mapper.MpVisitorMapper; import com.sinata.appointment.util.IdCardUtil; import com.sinata.appointment.util.SensitiveDataUtil; import com.sinata.appointment.util.UuidAssert; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor public class MiniVisitorApplicationService { private final MiniAuthApplicationService miniAuthApplicationService; private final MpVisitorMapper mpVisitorMapper; private final BizFieldConfigMapper bizFieldConfigMapper; @Transactional(rollbackFor = Exception.class) public void create(MiniVisitorCreateRequest req) { MpUser current = miniAuthApplicationService.currentMpUser(); Set requiredCodes = loadRequiredMpVisitorFieldCodes(); VisitorNormalizedInput input = normalizeAndValidate( req.getVisitorName(), req.getVisitorMobile(), req.getVisitorCompany(), req.getIdCard(), req.getGender(), requiredCodes); MpVisitor row = new MpVisitor(); row.setMpUserId(current.getId()); // 兼容当前表结构 NOT NULL:未填字段统一落空字符串 row.setVisitorName(input.name() == null ? "" : input.name()); row.setVisitorMobile(input.mobile() == null ? "" : input.mobile()); row.setVisitorCompany(input.company() == null ? "" : input.company()); row.setIdCardCipher( StringUtils.hasText(input.idCard()) ? SensitiveDataUtil.encryptIdCard(input.idCard()) : ""); row.setGender(input.gender() == null ? "" : input.gender()); mpVisitorMapper.insert(row); } @Transactional(rollbackFor = Exception.class) public void update(Long id, String uuid, MiniVisitorUpdateRequest req) { MpUser current = miniAuthApplicationService.currentMpUser(); MpVisitor row = loadOwnedVisitor(id, uuid, current.getId()); Set requiredCodes = loadRequiredMpVisitorFieldCodes(); VisitorNormalizedInput input = normalizeAndValidate( req.getVisitorName(), req.getVisitorMobile(), req.getVisitorCompany(), req.getIdCard(), req.getGender(), requiredCodes); row.setVisitorName(input.name() == null ? "" : input.name()); row.setVisitorMobile(input.mobile() == null ? "" : input.mobile()); row.setVisitorCompany(input.company() == null ? "" : input.company()); row.setIdCardCipher( StringUtils.hasText(input.idCard()) ? SensitiveDataUtil.encryptIdCard(input.idCard()) : ""); row.setGender(input.gender() == null ? "" : input.gender()); mpVisitorMapper.updateById(row); } public MiniVisitorDetailVo detail(Long id, String uuid) { MpUser current = miniAuthApplicationService.currentMpUser(); MpVisitor row = loadOwnedVisitor(id, uuid, current.getId()); MiniVisitorDetailVo vo = new MiniVisitorDetailVo(); vo.setId(row.getId()); vo.setUuid(row.getUuid()); vo.setVisitorName(row.getVisitorName()); vo.setVisitorMobile(row.getVisitorMobile()); vo.setVisitorCompany(row.getVisitorCompany()); vo.setIdCard( StringUtils.hasText(row.getIdCardCipher()) ? SensitiveDataUtil.decryptIdCard(row.getIdCardCipher()) : ""); vo.setGender(row.getGender()); return vo; } public List listMine() { MpUser current = miniAuthApplicationService.currentMpUser(); Set requiredCodes = loadRequiredMpVisitorFieldCodes(); List rows = mpVisitorMapper.selectList( new LambdaQueryWrapper() .eq(MpVisitor::getMpUserId, current.getId()) .eq(MpVisitor::getDelFlag, 0) .orderByDesc(MpVisitor::getCreateTime) .orderByDesc(MpVisitor::getId)); return rows.stream().map(row -> toListVo(row, requiredCodes)).collect(Collectors.toList()); } private MiniVisitorListVo toListVo(MpVisitor row, Set requiredCodes) { MiniVisitorListVo vo = new MiniVisitorListVo(); vo.setId(row.getId()); vo.setUuid(row.getUuid()); vo.setVisitorName(row.getVisitorName()); vo.setGender(row.getGender()); if (StringUtils.hasText(row.getIdCardCipher())) { String plain = SensitiveDataUtil.decryptIdCard(row.getIdCardCipher()); vo.setIdCardMasked(SensitiveDataUtil.maskIdCard(plain)); } else { vo.setIdCardMasked(null); } vo.setVisitorCompany(row.getVisitorCompany()); vo.setMissingFieldCodes(resolveMissingFieldCodes(row, requiredCodes)); return vo; } private List resolveMissingFieldCodes(MpVisitor row, Set requiredCodes) { List missing = new ArrayList<>(); for (String code : requiredCodes) { if (AppointmentFieldCodes.VISITOR_NAME.equals(code) && !StringUtils.hasText(trim(row.getVisitorName()))) { missing.add(code); } else if (AppointmentFieldCodes.ID_CARD.equals(code) && !StringUtils.hasText(trim(row.getIdCardCipher()))) { missing.add(code); } else if (AppointmentFieldCodes.GENDER.equals(code) && !StringUtils.hasText(trim(row.getGender()))) { missing.add(code); } else if (AppointmentFieldCodes.COMPANY.equals(code) && !StringUtils.hasText(trim(row.getVisitorCompany()))) { missing.add(code); } else if (AppointmentFieldCodes.MOBILE.equals(code) && !StringUtils.hasText(trim(row.getVisitorMobile()))) { missing.add(code); } } return missing; } private Set loadRequiredMpVisitorFieldCodes() { List rows = bizFieldConfigMapper.selectList( new LambdaQueryWrapper() .eq(BizFieldConfig::getDelFlag, 0) .eq(BizFieldConfig::getVisible, 1) .eq(BizFieldConfig::getRequiredFlag, 1)); Set supported = Set.of( AppointmentFieldCodes.VISITOR_NAME, AppointmentFieldCodes.ID_CARD, AppointmentFieldCodes.GENDER, AppointmentFieldCodes.COMPANY, AppointmentFieldCodes.MOBILE); return rows.stream() .map(BizFieldConfig::getFieldCode) .filter(StringUtils::hasText) .map(String::trim) .filter(supported::contains) .collect(Collectors.toCollection(LinkedHashSet::new)); } private static String trim(String s) { return s == null ? null : s.trim(); } private static String trimToNull(String s) { String t = trim(s); return (t == null || t.isEmpty()) ? null : t; } private MpVisitor loadOwnedVisitor(Long visitorId, String uuid, Long mpUserId) { MpVisitor row = mpVisitorMapper.selectById(visitorId); if (row == null || (row.getDelFlag() != null && row.getDelFlag() == 1)) { throw new BizException(BizErrorCode.ORDER_NOT_FOUND, "访客不存在"); } UuidAssert.match(row, uuid); if (!mpUserId.equals(row.getMpUserId())) { throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "无权操作该访客"); } return row; } private VisitorNormalizedInput normalizeAndValidate( String visitorName, String visitorMobile, String visitorCompany, String idCard, String gender, Set requiredCodes) { String name = trimToNull(visitorName); String mobile = trimToNull(visitorMobile); String company = trimToNull(visitorCompany); String idCardPlain = trimToNull(idCard); String genderText = trimToNull(gender); if (requiredCodes.contains(AppointmentFieldCodes.VISITOR_NAME) && !StringUtils.hasText(name)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访人姓名不能为空"); } if (StringUtils.hasText(name) && name.length() > 30) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访人姓名不能超过30个字"); } if (requiredCodes.contains(AppointmentFieldCodes.MOBILE) && !StringUtils.hasText(mobile)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访人电话不能为空"); } if (StringUtils.hasText(mobile) && !mobile.matches("^\\d{11}$")) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访人电话须为11位数字"); } if (requiredCodes.contains(AppointmentFieldCodes.COMPANY) && !StringUtils.hasText(company)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访人单位不能为空"); } if (requiredCodes.contains(AppointmentFieldCodes.ID_CARD) && !StringUtils.hasText(idCardPlain)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "身份证号码不能为空"); } if (StringUtils.hasText(idCardPlain) && !IdCardUtil.isValidMainland18(idCardPlain)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "身份证号码格式不正确"); } if (requiredCodes.contains(AppointmentFieldCodes.GENDER) && !StringUtils.hasText(genderText)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "性别不能为空"); } if (StringUtils.hasText(genderText) && !"男".equals(genderText) && !"女".equals(genderText)) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "性别仅支持男/女"); } return new VisitorNormalizedInput(name, mobile, company, idCardPlain, genderText); } private record VisitorNormalizedInput( String name, String mobile, String company, String idCard, String gender) {} }