BookServiceImpl.java
package org.darkroomlibrary.service.impl;
import org.darkroomlibrary.mapper.BookMapper;
import org.darkroomlibrary.mapper.BookReservationMapper;
import org.darkroomlibrary.mapper.BorrowRecordMapper;
import org.darkroomlibrary.mapper.BookshelfMapper;
import org.darkroomlibrary.mapper.CategoryMapper;
import org.darkroomlibrary.mapper.ProcurementOrderMapper;
import org.darkroomlibrary.web.response.ApiResponse;
import org.darkroomlibrary.web.response.PageResponse;
import org.darkroomlibrary.web.dto.query.PageQuery;
import org.darkroomlibrary.web.dto.query.BookPageQuery;
import org.darkroomlibrary.domain.type.FileReferenceType;
import org.darkroomlibrary.domain.model.Book;
import org.darkroomlibrary.domain.model.Bookshelf;
import org.darkroomlibrary.web.view.MetricPoint;
import org.darkroomlibrary.service.BookService;
import org.darkroomlibrary.service.FileStorageService;
import org.darkroomlibrary.service.ReservationWorkflowService;
import org.darkroomlibrary.service.support.RecommendationSourceVersionService;
import org.darkroomlibrary.utils.AnalyticsTimeline;
import org.darkroomlibrary.utils.IdListUtils;
import org.darkroomlibrary.utils.TransactionCallbacks;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 图书业务逻辑实现
*/
@Slf4j
@Service
public class BookServiceImpl implements BookService {
@Resource
private BookMapper bookMapper;
@Resource
private FileStorageService fileStorageService;
@Resource
private BorrowRecordMapper borrowRecordMapper;
@Resource
private BookReservationMapper bookReservationMapper;
@Resource
private ProcurementOrderMapper procurementOrderMapper;
@Resource
private BookshelfMapper bookshelfMapper;
@Resource
private CategoryMapper categoryMapper;
@Resource
private ReservationWorkflowService reservationWorkflowService;
@Resource
private RecommendationSourceVersionService recommendationSourceVersionService;
/**
* 新增图书
*
* @param book 图书信息
* @return ApiResponse<Void>
*/
@Override
@Transactional
public ApiResponse<Void> save(Book book) {
if (book == null) {
return ApiResponse.error("图书参数不能为空");
}
Integer totalCount = book.getTotalCount() == null ? 1 : book.getTotalCount();
Integer availableCount = book.getAvailableCount() == null ? totalCount : book.getAvailableCount();
if (totalCount < 0 || availableCount < 0) {
return ApiResponse.error("库存数量不能小于0");
}
if (availableCount > totalCount) {
return ApiResponse.error("可借数量不能大于总数量");
}
book.setTotalCount(totalCount);
book.setAvailableCount(availableCount);
book.setId(null);
book.setVersion(0);
book.setCreateTime(LocalDateTime.now());
book.setIsDeleted(false);
book.setCover(normalizeOptionalText(book.getCover()));
String categoryName = normalizeOptionalText(book.getCategory());
if (categoryName == null) {
return ApiResponse.error("请选择图书分类");
}
if (categoryMapper.findByNameForUpdate(categoryName) == null) {
return ApiResponse.error("所选分类不存在或已被调整");
}
book.setCategory(categoryName);
if (book.getBookshelfId() != null
&& bookshelfMapper.findByIdForUpdate(book.getBookshelfId()) == null) {
return ApiResponse.error("所选书架不存在");
}
if (bookMapper.insert(book) != 1) {
return ApiResponse.error("图书新增失败,请重试");
}
if (!fileStorageService.bindSingle(book.getCover(), FileReferenceType.BOOK_COVER, book.getId())) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return ApiResponse.error("封面文件无效或不属于当前用户");
}
recommendationSourceVersionService.invalidateGlobalAfterCommit();
return ApiResponse.success();
}
/**
* 修改图书(MyBatis XML 动态 SQL 自动处理 null 字段,无需手动逐字段拷贝)
*
* @param book 图书信息
* @return ApiResponse<Void>
*/
@Override
@Transactional
public ApiResponse<Void> update(Book book) {
Book snapshot = book == null || book.getId() == null
? null : bookMapper.getById(book.getId());
if (snapshot == null) {
return ApiResponse.error("图书不存在");
}
boolean fullManagementEdit = book.getName() != null || book.getAuthor() != null;
String targetCategory = fullManagementEdit
? normalizeOptionalText(book.getCategory()) : snapshot.getCategory();
if (fullManagementEdit) {
if (targetCategory == null) {
return ApiResponse.error("请选择图书分类");
}
if (categoryMapper.findByNameForUpdate(targetCategory) == null) {
return ApiResponse.error("所选分类不存在或已被调整");
}
}
Integer targetBookshelfId = fullManagementEdit
? book.getBookshelfId() : snapshot.getBookshelfId();
List<Bookshelf> lockedShelves = lockBookshelves(snapshot.getBookshelfId(), targetBookshelfId);
if (targetBookshelfId != null && lockedShelves.stream()
.noneMatch(shelf -> Objects.equals(shelf.getId(), targetBookshelfId))) {
return ApiResponse.error("所选书架不存在");
}
Book existing = bookMapper.findByIdForUpdate(book.getId());
if (existing == null) {
return ApiResponse.error("图书不存在");
}
if (book.getVersion() == null) {
return ApiResponse.error("图书版本缺失,请刷新后重试");
}
if (!Objects.equals(existing.getVersion(), book.getVersion())) {
return ApiResponse.error("图书信息已被其他操作更新,请刷新后重试");
}
if (!Objects.equals(existing.getBookshelfId(), snapshot.getBookshelfId())) {
return ApiResponse.error("图书书架状态已变化,请刷新后重试");
}
boolean updatesStock = book.getTotalCount() != null || book.getAvailableCount() != null;
if (updatesStock
&& (book.getOriginalTotalCount() == null
|| book.getOriginalAvailableCount() == null)) {
return ApiResponse.error("库存快照缺失,请刷新后重试");
}
if (updatesStock
&& (!Objects.equals(existing.getTotalCount(), book.getOriginalTotalCount())
|| !Objects.equals(existing.getAvailableCount(), book.getOriginalAvailableCount()))) {
return ApiResponse.error("图书库存已被借还或入库操作更新,请刷新后重试");
}
Integer totalCount = book.getTotalCount() == null ? existing.getTotalCount() : book.getTotalCount();
Integer availableCount = book.getAvailableCount() == null
? existing.getAvailableCount() : book.getAvailableCount();
int activeBorrowCount = borrowRecordMapper.countActiveByBookId(book.getId());
int notifiedReservationCount = bookReservationMapper.countNotifiedByBookId(book.getId());
if (totalCount == null || availableCount == null || totalCount < 0 || availableCount < 0) {
return ApiResponse.error("库存数量不能小于0");
}
if (availableCount > totalCount) {
return ApiResponse.error("可借数量不能大于总数量");
}
if (totalCount < activeBorrowCount || availableCount > totalCount - activeBorrowCount) {
return ApiResponse.error("库存调整与当前在借数量冲突");
}
if (availableCount < notifiedReservationCount) {
return ApiResponse.error("可借库存不能小于已通知预约数量");
}
String targetCover = fullManagementEdit
? normalizeOptionalText(book.getCover()) : existing.getCover();
Book update = Book.builder()
.id(book.getId())
.name(fullManagementEdit ? book.getName() : existing.getName())
.author(fullManagementEdit ? book.getAuthor() : existing.getAuthor())
.isbn(fullManagementEdit ? book.getIsbn() : existing.getIsbn())
.publisher(fullManagementEdit ? book.getPublisher() : existing.getPublisher())
.category(fullManagementEdit ? targetCategory : existing.getCategory())
.totalCount(totalCount)
.availableCount(availableCount)
.cover(targetCover)
.description(fullManagementEdit ? book.getDescription() : existing.getDescription())
.bookshelfId(targetBookshelfId)
.version(book.getVersion())
.build();
if (bookMapper.updateManaged(update) == 0) {
return ApiResponse.error("图书状态已变化,请刷新后重试");
}
if (!Objects.equals(targetCover, existing.getCover())
&& !fileStorageService.bindSingle(
targetCover, FileReferenceType.BOOK_COVER, book.getId())) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return ApiResponse.error("封面文件无效或不属于当前用户");
}
if (availableCount > existing.getAvailableCount()) {
notifyReservationsAfterCommit(book.getId());
}
recommendationSourceVersionService.invalidateGlobalAfterCommit();
return ApiResponse.success();
}
/**
* 批量删除图书(软删除)
*
* @param ids 图书ID集合
* @return ApiResponse<Void>
*/
@Override
@Transactional
public ApiResponse<Void> batchDelete(List<Integer> ids) {
List<Integer> uniqueIds = normalizeIds(ids);
if (uniqueIds.isEmpty()) {
return ApiResponse.error("请选择要删除的图书");
}
if (IdListUtils.exceedsBatchLimit(uniqueIds)) {
return ApiResponse.error("单次最多删除" + IdListUtils.MAX_BATCH_SIZE + "本图书");
}
if (bookMapper.findByIdsForUpdate(uniqueIds).size() != uniqueIds.size()) {
return ApiResponse.error("部分图书不存在");
}
if (borrowRecordMapper.countActiveByBookIds(uniqueIds) > 0) {
return ApiResponse.error("存在未归还记录,不能删除相关图书");
}
if (bookReservationMapper.countActiveByBookIds(uniqueIds) > 0) {
return ApiResponse.error("存在进行中的预约,不能删除相关图书");
}
if (procurementOrderMapper.countActiveByBookIds(uniqueIds) > 0) {
return ApiResponse.error("存在进行中的采购单,不能删除相关图书");
}
if (bookMapper.softDelete(uniqueIds) != uniqueIds.size()) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return ApiResponse.error("图书状态已变化,请刷新后重试");
}
recommendationSourceVersionService.invalidateGlobalAfterCommit();
return ApiResponse.success();
}
/**
* 恢复已删除的图书
*/
@Override
@Transactional
public ApiResponse<Void> restore(List<Integer> ids) {
List<Integer> uniqueIds = normalizeIds(ids);
if (uniqueIds.isEmpty()) {
return ApiResponse.error("请选择要恢复的图书");
}
if (IdListUtils.exceedsBatchLimit(uniqueIds)) {
return ApiResponse.error("单次最多恢复" + IdListUtils.MAX_BATCH_SIZE + "本图书");
}
if (bookMapper.findByIdsForUpdate(uniqueIds).size() != uniqueIds.size()) {
return ApiResponse.error("部分图书不存在");
}
if (bookMapper.restore(uniqueIds) != uniqueIds.size()) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return ApiResponse.error("图书状态已变化,请刷新后重试");
}
recommendationSourceVersionService.invalidateGlobalAfterCommit();
return ApiResponse.success();
}
/**
* 分页查询图书
*
* @param dto 查询参数
* @return ApiResponse<List<Book>>
*/
@Override
public ApiResponse<List<Book>> query(BookPageQuery dto) {
List<Book> bookList = bookMapper.query(dto);
Integer totalCount = bookMapper.queryCount(dto);
return PageResponse.success(bookList, totalCount);
}
@Override
public ApiResponse<List<MetricPoint>> queryByDays(Integer day) {
PageQuery queryDto = AnalyticsTimeline.queryWindow(day);
List<MetricPoint> chartPoints = AnalyticsTimeline.toDailyMetrics(
day,
bookMapper.dailyCreateStats(queryDto.getStartTime(), queryDto.getEndTime())
);
return ApiResponse.success(chartPoints);
}
private List<Integer> normalizeIds(List<Integer> ids) {
return IdListUtils.normalize(ids);
}
private List<Bookshelf> lockBookshelves(Integer currentBookshelfId, Integer targetBookshelfId) {
List<Integer> ids = new ArrayList<>();
if (currentBookshelfId != null) {
ids.add(currentBookshelfId);
}
if (targetBookshelfId != null && !ids.contains(targetBookshelfId)) {
ids.add(targetBookshelfId);
}
ids.sort(Integer::compareTo);
return ids.isEmpty() ? List.of() : bookshelfMapper.findByIdsForUpdate(ids);
}
private String normalizeOptionalText(String value) {
return value == null || value.trim().isEmpty() ? null : value.trim();
}
private void notifyReservationsAfterCommit(Integer bookId) {
Runnable notification = () -> {
try {
reservationWorkflowService.onBookReturned(bookId);
} catch (Exception e) {
log.warn("库存调整后的预约通知失败,等待定时对账: bookId={}, error={}",
bookId, e.getMessage());
}
};
TransactionCallbacks.afterCommit(notification);
}
}