package com.sinata.appointment.service;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.sinata.appointment.constants.AppointmentBizConstants;
|
import com.sinata.appointment.constants.AppointmentOrderStatus;
|
import com.sinata.appointment.constants.ApprovalInstanceStatus;
|
import com.sinata.appointment.constants.IntegrationConstant;
|
import com.sinata.appointment.constants.QrcodeTicketStatus;
|
import com.sinata.appointment.constants.SystemSettingKeys;
|
import com.sinata.appointment.config.HikvisionQrcodeProperties;
|
import com.sinata.appointment.entity.ApprovalInstance;
|
import com.sinata.appointment.entity.ApprovalTask;
|
import com.sinata.appointment.entity.AppointmentOrder;
|
import com.sinata.appointment.entity.AppointmentVisitor;
|
import com.sinata.appointment.entity.AppointmentVehicle;
|
import com.sinata.appointment.entity.BizVisitReason;
|
import com.sinata.appointment.entity.MpUser;
|
import com.sinata.appointment.entity.MpVisitor;
|
import com.sinata.appointment.entity.QrcodeTicket;
|
import com.sinata.appointment.mapper.AppointmentOrderMapper;
|
import com.sinata.appointment.mapper.AppointmentVehicleMapper;
|
import com.sinata.appointment.mapper.AppointmentVisitorMapper;
|
import com.sinata.appointment.mapper.ApprovalInstanceMapper;
|
import com.sinata.appointment.mapper.ApprovalTaskMapper;
|
import com.sinata.appointment.mapper.BizVisitReasonMapper;
|
import com.sinata.appointment.mapper.MpUserMapper;
|
import com.sinata.appointment.mapper.MpVisitorMapper;
|
import com.sinata.appointment.mapper.QrcodeTicketMapper;
|
import com.sinata.appointment.util.SensitiveDataUtil;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.util.LinkedHashMap;
|
import java.util.LinkedHashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.UUID;
|
import java.util.stream.Stream;
|
import lombok.RequiredArgsConstructor;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.util.StringUtils;
|
|
/**
|
* 审批通过后的编排:出码、通知(异步由集成服务触发)。
|
*/
|
@Slf4j
|
@Service
|
@RequiredArgsConstructor
|
public class PostApprovalOrchestrationService {
|
|
private static final int DEFAULT_MAX_CONSECUTIVE_DAYS = 7;
|
|
private final IntegrationHikvisionService integrationHikvisionService;
|
private final IntegrationWechatService integrationWechatService;
|
private final QrcodeTicketMapper qrcodeTicketMapper;
|
private final AppointmentOrderMapper appointmentOrderMapper;
|
private final ApprovalInstanceMapper approvalInstanceMapper;
|
private final ApprovalTaskMapper approvalTaskMapper;
|
private final AppointmentVisitorMapper appointmentVisitorMapper;
|
private final AppointmentVehicleMapper appointmentVehicleMapper;
|
private final MpVisitorMapper mpVisitorMapper;
|
private final MpUserMapper mpUserMapper;
|
private final BizVisitReasonMapper bizVisitReasonMapper;
|
private final SysBizSettingCacheService sysBizSettingCacheService;
|
private final HikvisionQrcodeProperties hikvisionQrcodeProperties;
|
|
@Transactional(rollbackFor = Exception.class)
|
public void onOrderFullyApproved(AppointmentOrder order) {
|
ApprovalInstance latestInstance =
|
approvalInstanceMapper.selectOne(
|
new LambdaQueryWrapper<ApprovalInstance>()
|
.eq(ApprovalInstance::getDelFlag, 0)
|
.eq(ApprovalInstance::getOrderId, order.getId())
|
.orderByDesc(ApprovalInstance::getId)
|
.last("limit 1"));
|
if (latestInstance != null
|
&& !ApprovalInstanceStatus.APPROVED.equals(latestInstance.getStatus())) {
|
log.info(
|
"{} orderId={} skipped, instanceStatus={}",
|
IntegrationConstant.HIKVISION_QRCODE_EVENT,
|
order.getId(),
|
latestInstance.getStatus());
|
return;
|
}
|
|
order.setStatus(AppointmentOrderStatus.APPROVED);
|
appointmentOrderMapper.updateById(order);
|
|
List<AppointmentVisitor> visitors =
|
appointmentVisitorMapper.selectList(
|
new LambdaQueryWrapper<AppointmentVisitor>()
|
.eq(AppointmentVisitor::getDelFlag, 0)
|
.eq(AppointmentVisitor::getOrderId, order.getId())
|
.orderByAsc(AppointmentVisitor::getId));
|
Map<Long, String> driverPlateMap = loadDriverPlateMap(order.getId());
|
Map<Long, MpVisitor> visitorProfileMap = loadVisitorProfileMap(visitors);
|
Map<Long, QrcodeTicket> existingTicketMap = loadActiveUnusedTicketMap(order.getId(), visitors);
|
int validMinutes = resolveValidMinutes();
|
LocalDateTime generateTime = resolveGenerateTime(order);
|
LocalDateTime expireTime = generateTime.plusMinutes(validMinutes);
|
String visitPurpose = buildVisitPurpose(order);
|
String areas = normalizeAreas(order.getPassAccessAreaNames());
|
MpUser visitedUser = loadVisitedUser(order.getVisitedUserId());
|
|
int createdCount = 0;
|
for (AppointmentVisitor visitor : visitors) {
|
QrcodeTicket existed = existingTicketMap.get(visitor.getMpVisitorId());
|
if (existed != null) {
|
log.info(
|
"{} orderId={} mpVisitorId={} reuse ticketId={}",
|
IntegrationConstant.HIKVISION_QRCODE_EVENT,
|
order.getId(),
|
visitor.getMpVisitorId(),
|
existed.getId());
|
continue;
|
}
|
MpVisitor visitorProfile = visitorProfileMap.get(visitor.getMpVisitorId());
|
visitor.setPlateNo(driverPlateMap.get(visitor.getMpVisitorId()));
|
String ticketUuid = UUID.randomUUID().toString().replace("-", "");
|
String visitorIdCardPlain = resolveVisitorIdCardPlain(visitor, visitorProfile);
|
String payload =
|
integrationHikvisionService.requestQrcodePayload(
|
order,
|
visitor,
|
visitorProfile,
|
visitedUser,
|
visitPurpose,
|
areas,
|
generateTime,
|
expireTime,
|
validMinutes,
|
ticketUuid,
|
visitorIdCardPlain);
|
QrcodeTicket ticket = new QrcodeTicket();
|
ticket.setUuid(ticketUuid);
|
ticket.setOrderId(order.getId());
|
ticket.setMpVisitorId(visitor.getMpVisitorId());
|
ticket.setPayload(payload);
|
ticket.setTicketStatus(QrcodeTicketStatus.UNUSED);
|
ticket.setGenerateTime(generateTime);
|
ticket.setExpireTime(expireTime);
|
qrcodeTicketMapper.insert(ticket);
|
existingTicketMap.put(visitor.getMpVisitorId(), ticket);
|
createdCount++;
|
log.info(
|
"{} raw-payload orderId={} mpVisitorId={} ticketUuid={} payloadLength={}",
|
IntegrationConstant.HIKVISION_QRCODE_EVENT,
|
order.getId(),
|
visitor.getMpVisitorId(),
|
ticketUuid,
|
payload == null ? 0 : payload.length());
|
}
|
|
if (visitors.isEmpty()) {
|
log.info("{} orderId={} no visitors found, skip qrcode creation", IntegrationConstant.HIKVISION_QRCODE_EVENT, order.getId());
|
}
|
|
try {
|
integrationWechatService.notifyApprovalFinalResult(order, true);
|
integrationWechatService.notifyApprovalNoticeUsers(
|
order, true, loadNoticeUsers(latestInstance.getId()), visitors.size());
|
} catch (Exception e) {
|
log.warn("{} orderId={} failed: {}", IntegrationConstant.WECHAT_NOTIFY_EVENT, order.getId(), e.getMessage());
|
}
|
log.info(
|
"{} orderId={} createdTicketCount={}",
|
IntegrationConstant.HIKVISION_QRCODE_EVENT,
|
order.getId(),
|
createdCount);
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void onOrderRejected(AppointmentOrder order, String reason) {
|
order.setStatus(AppointmentOrderStatus.REJECTED);
|
order.setRejectReason(reason);
|
appointmentOrderMapper.updateById(order);
|
try {
|
integrationWechatService.notifyApprovalFinalResult(order, false);
|
ApprovalInstance latestInstance = loadLatestInstance(order.getId());
|
if (latestInstance != null) {
|
List<AppointmentVisitor> visitors =
|
appointmentVisitorMapper.selectList(
|
new LambdaQueryWrapper<AppointmentVisitor>()
|
.eq(AppointmentVisitor::getDelFlag, 0)
|
.eq(AppointmentVisitor::getOrderId, order.getId()));
|
integrationWechatService.notifyApprovalNoticeUsers(
|
order, false, loadNoticeUsers(latestInstance.getId()), visitors.size());
|
}
|
} catch (Exception e) {
|
log.warn(
|
"{} orderId={} failed: {}",
|
IntegrationConstant.WECHAT_NOTIFY_EVENT,
|
order.getId(),
|
e.getMessage());
|
}
|
}
|
|
private ApprovalInstance loadLatestInstance(Long orderId) {
|
if (orderId == null) {
|
return null;
|
}
|
return approvalInstanceMapper.selectOne(
|
new LambdaQueryWrapper<ApprovalInstance>()
|
.eq(ApprovalInstance::getDelFlag, 0)
|
.eq(ApprovalInstance::getOrderId, orderId)
|
.orderByDesc(ApprovalInstance::getId)
|
.last("limit 1"));
|
}
|
|
private List<MpUser> loadNoticeUsers(Long instanceId) {
|
if (instanceId == null) {
|
return List.of();
|
}
|
List<ApprovalTask> tasks =
|
approvalTaskMapper.selectList(
|
new LambdaQueryWrapper<ApprovalTask>()
|
.eq(ApprovalTask::getDelFlag, 0)
|
.eq(ApprovalTask::getInstanceId, instanceId)
|
.isNotNull(ApprovalTask::getNoticeIds)
|
.ne(ApprovalTask::getNoticeIds, "")
|
.orderByAsc(ApprovalTask::getId));
|
if (tasks.isEmpty()) {
|
return List.of();
|
}
|
LinkedHashSet<Long> noticeUserIds = new LinkedHashSet<>();
|
for (ApprovalTask task : tasks) {
|
noticeUserIds.addAll(parseCommaSeparatedIds(task.getNoticeIds()));
|
}
|
if (noticeUserIds.isEmpty()) {
|
return List.of();
|
}
|
List<MpUser> users =
|
mpUserMapper.selectList(
|
new LambdaQueryWrapper<MpUser>()
|
.eq(MpUser::getDelFlag, 0)
|
.in(MpUser::getId, noticeUserIds));
|
Map<Long, MpUser> userMap = new LinkedHashMap<>();
|
for (MpUser user : users) {
|
userMap.putIfAbsent(user.getId(), user);
|
}
|
return noticeUserIds.stream().map(userMap::get).filter(Objects::nonNull).toList();
|
}
|
|
private List<Long> parseCommaSeparatedIds(String raw) {
|
if (!StringUtils.hasText(raw)) {
|
return List.of();
|
}
|
return Stream.of(raw.split(","))
|
.filter(StringUtils::hasText)
|
.map(String::trim)
|
.map(value -> {
|
try {
|
return Long.parseLong(value);
|
} catch (NumberFormatException ex) {
|
return null;
|
}
|
})
|
.filter(Objects::nonNull)
|
.filter(id -> id > 0)
|
.distinct()
|
.toList();
|
}
|
|
private Map<Long, MpVisitor> loadVisitorProfileMap(List<AppointmentVisitor> visitors) {
|
if (visitors == null || visitors.isEmpty()) {
|
return Map.of();
|
}
|
List<Long> visitorIds =
|
visitors.stream()
|
.map(AppointmentVisitor::getMpVisitorId)
|
.filter(Objects::nonNull)
|
.distinct()
|
.toList();
|
if (visitorIds.isEmpty()) {
|
return Map.of();
|
}
|
List<MpVisitor> rows =
|
mpVisitorMapper.selectList(
|
new LambdaQueryWrapper<MpVisitor>()
|
.eq(MpVisitor::getDelFlag, 0)
|
.in(MpVisitor::getId, visitorIds));
|
Map<Long, MpVisitor> result = new LinkedHashMap<>();
|
for (MpVisitor row : rows) {
|
result.putIfAbsent(row.getId(), row);
|
}
|
return result;
|
}
|
|
private Map<Long, String> loadDriverPlateMap(Long orderId) {
|
if (orderId == null) {
|
return Map.of();
|
}
|
List<AppointmentVehicle> rows =
|
appointmentVehicleMapper.selectList(
|
new LambdaQueryWrapper<AppointmentVehicle>()
|
.eq(AppointmentVehicle::getDelFlag, 0)
|
.eq(AppointmentVehicle::getOrderId, orderId)
|
.orderByAsc(AppointmentVehicle::getId));
|
if (rows.isEmpty()) {
|
return Map.of();
|
}
|
Map<Long, List<String>> grouped = new LinkedHashMap<>();
|
for (AppointmentVehicle row : rows) {
|
if (row.getDriverVisitorId() == null || !StringUtils.hasText(row.getPlateNo())) {
|
continue;
|
}
|
grouped.computeIfAbsent(row.getDriverVisitorId(), key -> new java.util.ArrayList<>()).add(row.getPlateNo().trim());
|
}
|
Map<Long, String> result = new LinkedHashMap<>();
|
for (Map.Entry<Long, List<String>> entry : grouped.entrySet()) {
|
result.put(entry.getKey(), String.join(",", entry.getValue()));
|
}
|
return result;
|
}
|
|
private Map<Long, QrcodeTicket> loadActiveUnusedTicketMap(Long orderId, List<AppointmentVisitor> visitors) {
|
if (orderId == null || visitors == null || visitors.isEmpty()) {
|
return new LinkedHashMap<>();
|
}
|
LinkedHashSet<Long> visitorIds =
|
visitors.stream()
|
.map(AppointmentVisitor::getMpVisitorId)
|
.filter(Objects::nonNull)
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
if (visitorIds.isEmpty()) {
|
return new LinkedHashMap<>();
|
}
|
List<QrcodeTicket> rows =
|
qrcodeTicketMapper.selectList(
|
new LambdaQueryWrapper<QrcodeTicket>()
|
.eq(QrcodeTicket::getDelFlag, 0)
|
.eq(QrcodeTicket::getOrderId, orderId)
|
.eq(QrcodeTicket::getTicketStatus, QrcodeTicketStatus.UNUSED)
|
.in(QrcodeTicket::getMpVisitorId, visitorIds)
|
.orderByDesc(QrcodeTicket::getId));
|
Map<Long, QrcodeTicket> result = new LinkedHashMap<>();
|
for (QrcodeTicket row : rows) {
|
if (row.getMpVisitorId() != null) {
|
result.putIfAbsent(row.getMpVisitorId(), row);
|
}
|
}
|
return result;
|
}
|
|
private LocalDateTime resolveGenerateTime(AppointmentOrder order) {
|
LocalDate visitDate = order.getVisitStartDate() != null ? order.getVisitStartDate() : order.getVisitEndDate();
|
if (visitDate == null) {
|
visitDate = LocalDate.now();
|
}
|
return visitDate.atStartOfDay();
|
}
|
|
private int getMaxConsecutiveDays() {
|
String raw = trimToNull(sysBizSettingCacheService.getValue(SystemSettingKeys.BOOKING_MAX_CONSECUTIVE_DAYS));
|
if (!StringUtils.hasText(raw)) {
|
return DEFAULT_MAX_CONSECUTIVE_DAYS;
|
}
|
try {
|
return Math.max(1, Integer.parseInt(raw));
|
} catch (NumberFormatException ex) {
|
return DEFAULT_MAX_CONSECUTIVE_DAYS;
|
}
|
}
|
|
private int resolveValidMinutes() {
|
Integer configured = hikvisionQrcodeProperties.getValidMinutes();
|
if (configured != null && configured > 0) {
|
return configured;
|
}
|
return getMaxConsecutiveDays() * 24 * 60;
|
}
|
|
private String buildVisitPurpose(AppointmentOrder order) {
|
String code = trimToNull(order.getVisitReasonCode());
|
if (!StringUtils.hasText(code)) {
|
return "";
|
}
|
if (AppointmentBizConstants.VISIT_REASON_OTHER.equalsIgnoreCase(code)) {
|
return "其他";
|
}
|
BizVisitReason reason =
|
bizVisitReasonMapper.selectOne(
|
new LambdaQueryWrapper<BizVisitReason>()
|
.eq(BizVisitReason::getDelFlag, 0)
|
.eq(BizVisitReason::getReasonCode, code)
|
.last("limit 1"));
|
return reason != null && StringUtils.hasText(reason.getReasonName()) ? reason.getReasonName().trim() : code;
|
}
|
|
private String normalizeAreas(String rawJson) {
|
String raw = trimToNull(rawJson);
|
if (!StringUtils.hasText(raw)) {
|
return "";
|
}
|
String cleaned = raw.trim();
|
if (cleaned.startsWith("[") && cleaned.endsWith("]")) {
|
cleaned = cleaned.substring(1, cleaned.length() - 1);
|
}
|
cleaned = cleaned.replace("\"", "").trim();
|
while (cleaned.endsWith(",")) {
|
cleaned = cleaned.substring(0, cleaned.length() - 1).trim();
|
}
|
return cleaned;
|
}
|
|
private MpUser loadVisitedUser(Long visitedUserId) {
|
if (visitedUserId == null) {
|
return null;
|
}
|
return mpUserMapper.selectOne(
|
new LambdaQueryWrapper<MpUser>()
|
.eq(MpUser::getDelFlag, 0)
|
.eq(MpUser::getId, visitedUserId)
|
.last("limit 1"));
|
}
|
|
private MpVisitor loadVisitor(Long mpVisitorId) {
|
if (mpVisitorId == null) {
|
return null;
|
}
|
return mpVisitorMapper.selectOne(
|
new LambdaQueryWrapper<MpVisitor>()
|
.eq(MpVisitor::getDelFlag, 0)
|
.eq(MpVisitor::getId, mpVisitorId)
|
.last("limit 1"));
|
}
|
|
private String resolveVisitorIdCardPlain(AppointmentVisitor visitor, MpVisitor visitorProfile) {
|
if (StringUtils.hasText(visitor.getIdCardCipher())) {
|
return SensitiveDataUtil.decryptIdCard(visitor.getIdCardCipher());
|
}
|
if (visitorProfile != null && StringUtils.hasText(visitorProfile.getIdCardCipher())) {
|
return SensitiveDataUtil.decryptIdCard(visitorProfile.getIdCardCipher());
|
}
|
return null;
|
}
|
|
private String trimToNull(String value) {
|
if (!StringUtils.hasText(value)) {
|
return null;
|
}
|
return value.trim();
|
}
|
}
|