BorrowRecordServiceImpl.java

package org.darkroomlibrary.service.impl;

import org.darkroomlibrary.infrastructure.event.DomainEventPublisher;
import org.darkroomlibrary.context.CurrentUserContext;
import org.darkroomlibrary.mapper.BookMapper;
import org.darkroomlibrary.mapper.BookReservationMapper;
import org.darkroomlibrary.mapper.BorrowRecordMapper;
import org.darkroomlibrary.mapper.UserMapper;
import org.darkroomlibrary.web.response.ApiResponse;
import org.darkroomlibrary.web.response.PageResponse;
import org.darkroomlibrary.web.dto.query.BorrowRecordPageQuery;
import org.darkroomlibrary.domain.type.AccountStatus;
import org.darkroomlibrary.domain.type.UserRole;
import org.darkroomlibrary.domain.model.Book;
import org.darkroomlibrary.domain.model.BookReservation;
import org.darkroomlibrary.domain.model.BorrowRecord;
import org.darkroomlibrary.domain.model.User;
import org.darkroomlibrary.web.view.BorrowRecordView;
import org.darkroomlibrary.service.BorrowRecordService;
import org.darkroomlibrary.service.FineService;
import org.darkroomlibrary.service.ReservationWorkflowService;
import org.darkroomlibrary.service.support.RecommendationSourceVersionService;
import org.darkroomlibrary.utils.TransactionCallbacks;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;

@Slf4j
@Service
public class BorrowRecordServiceImpl implements BorrowRecordService {

    @Resource
    private BorrowRecordMapper borrowRecordMapper;

    @Resource
    private BookMapper bookMapper;

    @Resource
    private UserMapper userMapper;

    @Resource
    private FineService fineService;

    @Resource
    private BookReservationMapper bookReservationMapper;

    @Resource
    private DomainEventPublisher domainEventPublisher;

    @Resource
    private ReservationWorkflowService reservationWorkflowService;

    @Resource
    private RecommendationSourceVersionService recommendationSourceVersionService;

    @Value("${borrow.max-count:5}")
    private int maxBorrowCount;

    @Value("${borrow.renew.max-count:1}")
    private int maxRenewCount;

    @Value("${borrow.renew.window-days-before-due:3}")
    private int renewWindowDaysBeforeDue;

    @Value("${borrow.renew.extend-days:30}")
    private int renewExtendDays;

    @Value("${middleware.rabbit.book-returned-routing-key:book.returned}")
    private String bookReturnedRoutingKey;

    @Override
    @Transactional
    public ApiResponse<Void> borrow(Integer bookId) {
        Integer userId = CurrentUserContext.userId();
        if (userId == null) {
            return ApiResponse.error("身份认证失败,请先登录");
        }
        if (bookId == null) {
            return ApiResponse.error("图书编号不能为空");
        }
        User user = userMapper.findByIdForUpdate(userId);
        if (user == null) {
            return ApiResponse.error("当前用户不存在或已被删除");
        }
        if (!Objects.equals(user.getAccountStatus(), AccountStatus.NORMAL.code())
                || Boolean.TRUE.equals(user.getIsLogin())
                || !Objects.equals(user.getUserRole(), CurrentUserContext.roleCode())
                || !Objects.equals(user.getUserRole(), UserRole.READER.code())) {
            return ApiResponse.error("当前账号状态不允许借阅");
        }
        int activeCount = borrowRecordMapper.getActiveCountByUserId(userId);
        if (activeCount >= maxBorrowCount) {
            return ApiResponse.error("借阅数量已达上限(" + maxBorrowCount + "本),请先归还部分图书");
        }
        Book book = bookMapper.findByIdForUpdate(bookId);
        if (book == null || Boolean.TRUE.equals(book.getIsDeleted())) {
            return ApiResponse.error("图书不存在");
        }
        if (borrowRecordMapper.countActiveByUserIdAndBookId(userId, bookId) > 0) {
            return ApiResponse.error("您已借阅该图书,请勿重复借阅");
        }
        BookReservation notifiedReservation = bookReservationMapper.findNotifiedByBookIdAndUserId(bookId, userId);
        if (bookReservationMapper.countActiveByBookId(bookId) > 0 && notifiedReservation == null) {
            return ApiResponse.error("该图书已有预约队列,请等待预约通知");
        }
        int affectedRows = bookMapper.decreaseAvailableCount(bookId);
        if (affectedRows == 0) {
            return ApiResponse.error("图书库存不足,无法借阅");
        }
        // 条件插入(DB层原子防并发重复借阅)
        LocalDateTime now = LocalDateTime.now();
        BorrowRecord record = BorrowRecord.builder()
                .userId(userId)
                .bookId(bookId)
                .borrowTime(now)
                .dueDate(now.plusDays(30))
                .status(false)
                .fineAmount(BigDecimal.ZERO)
                .renewCount(0)
                .build();
        int inserted = borrowRecordMapper.insertIfNoActiveBorrow(record);
        if (inserted == 0) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return ApiResponse.error("您已借阅该图书,请勿重复借阅");
        }
        if (notifiedReservation != null && bookReservationMapper.markBorrowed(notifiedReservation.getId()) == 0) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return ApiResponse.error("预约状态已变化,请刷新后重试");
        }
        recommendationSourceVersionService.invalidateUserAndGlobalAfterCommit(userId);
        return ApiResponse.success("借阅成功");
    }

    @Override
    @Transactional
    public ApiResponse<Void> returnBook(Integer recordId) {
        Integer userId = CurrentUserContext.userId();
        BorrowRecord snapshot = recordId == null ? null : borrowRecordMapper.getById(recordId);
        if (snapshot == null) {
            return ApiResponse.error("借阅记录不存在");
        }
        if (!CurrentUserContext.isAdministrator() && !snapshot.getUserId().equals(userId)) {
            return ApiResponse.error("该借阅记录不属于当前用户,无法归还");
        }
        if (userMapper.findByIdForUpdate(snapshot.getUserId()) == null) {
            return ApiResponse.error("借阅用户不存在");
        }
        Book book = bookMapper.findByIdForUpdate(snapshot.getBookId());
        if (book == null || Boolean.TRUE.equals(book.getIsDeleted())) {
            return ApiResponse.error("图书库存状态异常,归还失败");
        }
        BorrowRecord record = borrowRecordMapper.findByIdForUpdate(recordId);
        if (record == null
                || !Objects.equals(record.getUserId(), snapshot.getUserId())
                || !Objects.equals(record.getBookId(), snapshot.getBookId())) {
            return ApiResponse.error("借阅记录状态已变化,请刷新后重试");
        }
        if (Boolean.TRUE.equals(record.getStatus())) {
            return ApiResponse.error("该图书已归还,请勿重复操作");
        }
        LocalDateTime now = LocalDateTime.now();
        BigDecimal fine = fineService.calculateFine(record.getDueDate(), now);
        // DB条件更新(WHERE status=0 防并发重复归还)
        int affected = borrowRecordMapper.updateReturnStatus(recordId, now, fine);
        if (affected == 0) {
            return ApiResponse.error("该图书已归还,请勿重复操作");
        }
        // 原子增加库存
        if (bookMapper.increaseAvailableCount(record.getBookId()) == 0) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return ApiResponse.error("图书库存状态异常,归还失败");
        }
        recommendationSourceVersionService.invalidateUserAndGlobalAfterCommit(record.getUserId());
        publishBookReturnedAfterCommit(record.getBookId());
        if (fine.compareTo(BigDecimal.ZERO) > 0) {
            return ApiResponse.success("归还成功,逾期罚款:" + fine + "元");
        }
        return ApiResponse.success("归还成功");
    }

    @Override
    @Transactional
    public ApiResponse<Void> renew(Integer recordId) {
        Integer userId = CurrentUserContext.userId();
        BorrowRecord snapshot = recordId == null ? null : borrowRecordMapper.getById(recordId);
        if (snapshot == null) {
            return ApiResponse.error("借阅记录不存在");
        }
        if (!snapshot.getUserId().equals(userId)) {
            return ApiResponse.error("该借阅记录不属于当前用户,无法续借");
        }
        User user = userMapper.findByIdForUpdate(userId);
        if (user == null
                || !Objects.equals(user.getAccountStatus(), AccountStatus.NORMAL.code())
                || Boolean.TRUE.equals(user.getIsLogin())
                || !Objects.equals(user.getUserRole(), CurrentUserContext.roleCode())
                || !Objects.equals(user.getUserRole(), UserRole.READER.code())) {
            return ApiResponse.error("当前账号状态不允许续借");
        }
        Book book = bookMapper.findByIdForUpdate(snapshot.getBookId());
        if (book == null || Boolean.TRUE.equals(book.getIsDeleted())) {
            return ApiResponse.error("图书不存在,无法续借");
        }
        BorrowRecord record = borrowRecordMapper.findByIdForUpdate(recordId);
        if (record == null
                || !Objects.equals(record.getUserId(), snapshot.getUserId())
                || !Objects.equals(record.getBookId(), snapshot.getBookId())) {
            return ApiResponse.error("借阅记录状态已变化,请刷新后重试");
        }
        if (Boolean.TRUE.equals(record.getStatus())) {
            return ApiResponse.error("该图书已归还,无法续借");
        }
        LocalDateTime now = LocalDateTime.now();
        if (record.getDueDate() == null || now.isAfter(record.getDueDate())) {
            return ApiResponse.error("图书已逾期,无法续借");
        }
        if (now.isBefore(record.getDueDate().minusDays(renewWindowDaysBeforeDue))) {
            return ApiResponse.error("仅允许在到期前" + renewWindowDaysBeforeDue + "天内续借");
        }
        int renewCount = record.getRenewCount() == null ? 0 : record.getRenewCount();
        if (renewCount >= maxRenewCount) {
            return ApiResponse.error("该借阅记录已达到续借次数上限");
        }
        if (bookReservationMapper.countActiveByBookId(record.getBookId()) > 0) {
            return ApiResponse.error("该图书存在预约排队,暂不允许续借");
        }
        LocalDateTime newDueDate = record.getDueDate().plusDays(renewExtendDays);
        int affected = borrowRecordMapper.updateRenewStatus(
                recordId, newDueDate, renewCount + 1, record.getDueDate(), renewCount);
        if (affected == 0) {
            return ApiResponse.error("续借失败,请刷新后重试");
        }
        return ApiResponse.success("续借成功,应还日期已延长至:" + newDueDate);
    }

    @Override
    public ApiResponse<List<BorrowRecordView>> query(BorrowRecordPageQuery dto) {
        if (dto == null) {
            dto = new BorrowRecordPageQuery();
        }
        if (!CurrentUserContext.isAdministrator()) {
            dto.setUserId(CurrentUserContext.userId());
        }
        List<BorrowRecordView> recordList = borrowRecordMapper.query(dto);
        Integer totalCount = borrowRecordMapper.queryCount(dto);
        return PageResponse.success(recordList, totalCount);
    }

    private void publishBookReturnedAfterCommit(Integer bookId) {
        Runnable publishTask = () -> {
            try {
                if (!domainEventPublisher.publish(bookReturnedRoutingKey, bookId)) {
                    reservationWorkflowService.onBookReturned(bookId);
                }
            } catch (Exception e) {
                log.warn("BookReturnedEvent fallback failed: bookId={}, error={}", bookId, e.getMessage());
            }
        };
        TransactionCallbacks.afterCommit(publishTask);
    }
}