package com.sinata.appointment.service;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
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<String> requiredCodes = loadRequiredMpVisitorFieldCodes();
|
VisitorNormalizedInput input =
|
normalizeAndValidate(
|
req.getVisitorName(),
|
req.getVisitorMobile(),
|
req.getVisitorCompany(),
|
req.getIdCard(),
|
req.getGender(),
|
requiredCodes);
|
assertIdCardUniqueUnderUser(current.getId(), input.idCard(), null);
|
|
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());
|
row.setIsDefault(hasAnyActiveVisitor(current.getId()) ? 0 : 1);
|
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<String> requiredCodes = loadRequiredMpVisitorFieldCodes();
|
VisitorNormalizedInput input =
|
normalizeAndValidate(
|
req.getVisitorName(),
|
req.getVisitorMobile(),
|
req.getVisitorCompany(),
|
req.getIdCard(),
|
req.getGender(),
|
requiredCodes);
|
assertIdCardUniqueUnderUser(current.getId(), input.idCard(), row.getId());
|
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);
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void setDefault(Long id, String uuid) {
|
MpUser current = miniAuthApplicationService.currentMpUser();
|
MpVisitor row = loadOwnedVisitor(id, uuid, current.getId());
|
mpVisitorMapper.update(
|
null,
|
new LambdaUpdateWrapper<MpVisitor>()
|
.eq(MpVisitor::getMpUserId, current.getId())
|
.eq(MpVisitor::getDelFlag, 0)
|
.set(MpVisitor::getIsDefault, 0));
|
row.setIsDefault(1);
|
mpVisitorMapper.updateById(row);
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void delete(Long id, String uuid) {
|
MpUser current = miniAuthApplicationService.currentMpUser();
|
MpVisitor row = loadOwnedVisitor(id, uuid, current.getId());
|
boolean wasDefault = isDefault(row);
|
mpVisitorMapper.deleteById(row.getId());
|
if (wasDefault) {
|
resetEarliestVisitorAsDefault(current.getId());
|
}
|
}
|
|
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());
|
vo.setIsDefault(isDefault(row));
|
return vo;
|
}
|
|
public List<MiniVisitorListVo> listMine() {
|
MpUser current = miniAuthApplicationService.currentMpUser();
|
Set<String> requiredCodes = loadRequiredMpVisitorFieldCodes();
|
List<MpVisitor> rows =
|
mpVisitorMapper.selectList(
|
new LambdaQueryWrapper<MpVisitor>()
|
.eq(MpVisitor::getMpUserId, current.getId())
|
.eq(MpVisitor::getDelFlag, 0)
|
.orderByDesc(MpVisitor::getIsDefault)
|
.orderByDesc(MpVisitor::getCreateTime)
|
.orderByDesc(MpVisitor::getId));
|
return rows.stream().map(row -> toListVo(row, requiredCodes)).collect(Collectors.toList());
|
}
|
|
private MiniVisitorListVo toListVo(MpVisitor row, Set<String> 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.setIsDefault(isDefault(row));
|
vo.setMissingFieldCodes(resolveMissingFieldCodes(row, requiredCodes));
|
return vo;
|
}
|
|
private boolean isDefault(MpVisitor row) {
|
return row != null && row.getIsDefault() != null && row.getIsDefault() == 1;
|
}
|
|
private boolean hasAnyActiveVisitor(Long mpUserId) {
|
return mpVisitorMapper.selectCount(
|
new LambdaQueryWrapper<MpVisitor>()
|
.eq(MpVisitor::getMpUserId, mpUserId)
|
.eq(MpVisitor::getDelFlag, 0))
|
> 0;
|
}
|
|
private void resetEarliestVisitorAsDefault(Long mpUserId) {
|
MpVisitor next =
|
mpVisitorMapper.selectOne(
|
new LambdaQueryWrapper<MpVisitor>()
|
.eq(MpVisitor::getMpUserId, mpUserId)
|
.eq(MpVisitor::getDelFlag, 0)
|
.orderByAsc(MpVisitor::getCreateTime)
|
.orderByAsc(MpVisitor::getId)
|
.last("limit 1"));
|
if (next != null) {
|
next.setIsDefault(1);
|
mpVisitorMapper.updateById(next);
|
}
|
}
|
|
private List<String> resolveMissingFieldCodes(MpVisitor row, Set<String> requiredCodes) {
|
List<String> 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<String> loadRequiredMpVisitorFieldCodes() {
|
List<BizFieldConfig> rows =
|
bizFieldConfigMapper.selectList(
|
new LambdaQueryWrapper<BizFieldConfig>()
|
.eq(BizFieldConfig::getDelFlag, 0)
|
.eq(BizFieldConfig::getVisible, 1)
|
.eq(BizFieldConfig::getRequiredFlag, 1));
|
Set<String> 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<String> 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("^1[3-9]\\d{9}$")) {
|
throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "来访人电话格式不正确");
|
}
|
|
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 void assertIdCardUniqueUnderUser(Long mpUserId, String idCardPlain, Long excludeVisitorId) {
|
String normalizedIdCard = normalizeIdCard(idCardPlain);
|
if (!StringUtils.hasText(normalizedIdCard) || mpUserId == null) {
|
return;
|
}
|
List<MpVisitor> rows =
|
mpVisitorMapper.selectList(
|
new LambdaQueryWrapper<MpVisitor>()
|
.eq(MpVisitor::getMpUserId, mpUserId)
|
.eq(MpVisitor::getDelFlag, 0));
|
for (MpVisitor row : rows) {
|
if (excludeVisitorId != null && excludeVisitorId.equals(row.getId())) {
|
continue;
|
}
|
if (!StringUtils.hasText(row.getIdCardCipher())) {
|
continue;
|
}
|
String existing = normalizeIdCard(SensitiveDataUtil.decryptIdCard(row.getIdCardCipher()));
|
if (normalizedIdCard.equals(existing)) {
|
throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "同一账号下身份证号码不可重复");
|
}
|
}
|
}
|
|
private String normalizeIdCard(String idCard) {
|
String value = trimToNull(idCard);
|
return value == null ? null : value.toUpperCase();
|
}
|
|
private record VisitorNormalizedInput(
|
String name, String mobile, String company, String idCard, String gender) {}
|
}
|