package com.gym.manage.application.service; import com.gym.manage.api.dto.request.BookingCreateRequest; import com.gym.manage.api.dto.response.BookingRecordResponse; import com.gym.manage.common.constant.ErrorCode; import com.gym.manage.common.exception.BusinessException; import com.gym.manage.domain.entity.BookingRecord; import com.gym.manage.domain.entity.BookingSlot; import com.gym.manage.domain.repository.BookingRecordRepository; import com.gym.manage.domain.repository.BookingSlotRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.LocalDateTime; @Slf4j @Service @RequiredArgsConstructor public class BookingService { private final BookingSlotRepository bookingSlotRepository; private final BookingRecordRepository bookingRecordRepository; @Transactional public Mono createBooking(BookingCreateRequest request) { log.info("创建预约: memberId={}, slotId={}", request.getMemberId(), request.getSlotId()); return validateAndBook(request) .map(this::toBookingRecordResponse) .doOnSuccess(response -> log.info("预约创建成功: bookingId={}", response.getId())) .doOnError(e -> log.error("预约创建失败: memberId={}, slotId={}, error={}", request.getMemberId(), request.getSlotId(), e.getMessage())); } private Mono validateAndBook(BookingCreateRequest request) { return bookingSlotRepository.findByIdAndDeletedAtIsNull(request.getSlotId()) .switchIfEmpty(Mono.error(new BusinessException(ErrorCode.SLOT_NOT_FOUND, "时段不存在"))) .flatMap(slot -> { if (!"AVAILABLE".equals(slot.getStatus())) { return Mono.error(new BusinessException(ErrorCode.SLOT_NOT_AVAILABLE, "时段不可预约")); } if (slot.getBookedCount() >= slot.getMaxCapacity()) { return Mono.error(new BusinessException(ErrorCode.SLOT_NOT_AVAILABLE, "时段已满")); } return bookingRecordRepository.findByMemberIdAndSlotIdAndDeletedAtIsNull( request.getMemberId(), request.getSlotId() ).flatMap(existing -> Mono.error( new BusinessException(ErrorCode.BOOKING_NOT_FOUND, "已预约该时段") )).switchIfEmpty(createBookingRecord(request, slot)); }); } private Mono createBookingRecord(BookingCreateRequest request, BookingSlot slot) { return bookingSlotRepository.incrementBookedCount(request.getSlotId()) .flatMap(rows -> { if (rows > 0) { BookingRecord record = new BookingRecord(); record.setTenantId(slot.getTenantId()); record.setStoreId(slot.getStoreId()); record.setMemberId(request.getMemberId()); record.setSlotId(request.getSlotId()); record.setCoachId(slot.getCoachId()); record.setCourseName(slot.getCourseName()); record.setBookingTime(LocalDateTime.now()); record.setStatus("BOOKED"); record.setRemark(request.getRemark()); record.setCreatedAt(LocalDateTime.now()); record.setUpdatedAt(LocalDateTime.now()); return bookingRecordRepository.save(record); } else { return Mono.error(new BusinessException(ErrorCode.SLOT_NOT_AVAILABLE, "预约失败,请重试")); } }); } public Mono getBooking(Long id) { log.info("查询预约: bookingId={}", id); return bookingRecordRepository.findByIdAndDeletedAtIsNull(id) .switchIfEmpty(Mono.error(new BusinessException(ErrorCode.BOOKING_NOT_FOUND, "预约不存在"))) .map(this::toBookingRecordResponse); } public Flux listMemberBookings(Long memberId, int page, int size) { log.info("查询会员预约列表: memberId={}", memberId); return bookingRecordRepository.findByMemberIdAndDeletedAtIsNull(memberId, org.springframework.data.domain.PageRequest.of(page, size)) .map(this::toBookingRecordResponse); } @Transactional public Mono cancelBooking(Long id, String reason) { log.info("取消预约: bookingId={}", id); return bookingRecordRepository.findByIdAndDeletedAtIsNull(id) .switchIfEmpty(Mono.error(new BusinessException(ErrorCode.BOOKING_NOT_FOUND, "预约不存在"))) .flatMap(record -> { if ("CANCELLED".equals(record.getStatus())) { return Mono.error(new BusinessException(ErrorCode.BOOKING_ALREADY_CANCELLED, "预约已取消")); } return bookingSlotRepository.decrementBookedCount(record.getSlotId()) .flatMap(rows -> { record.setStatus("CANCELLED"); record.setCancelReason(reason); record.setCancelTime(LocalDateTime.now()); record.setUpdatedAt(LocalDateTime.now()); return bookingRecordRepository.save(record); }); }) .then(); } private BookingRecordResponse toBookingRecordResponse(BookingRecord record) { return BookingRecordResponse.builder() .id(record.getId()) .memberId(record.getMemberId()) .slotId(record.getSlotId()) .coachId(record.getCoachId()) .courseName(record.getCourseName()) .bookingTime(record.getBookingTime()) .status(record.getStatus()) .cancelReason(record.getCancelReason()) .cancelTime(record.getCancelTime()) .checkinTime(record.getCheckinTime()) .remark(record.getRemark()) .createdAt(record.getCreatedAt()) .build(); } }