ProcurementServiceImpl.java

package org.darkroomlibrary.service.impl;

import org.darkroomlibrary.context.CurrentUserContext;
import org.darkroomlibrary.mapper.BookMapper;
import org.darkroomlibrary.mapper.ProcurementLogisticsMapper;
import org.darkroomlibrary.mapper.ProcurementMessageMapper;
import org.darkroomlibrary.mapper.ProcurementOrderMapper;
import org.darkroomlibrary.mapper.UserMapper;
import org.darkroomlibrary.web.response.ApiResponse;
import org.darkroomlibrary.web.response.PageResponse;
import org.darkroomlibrary.web.dto.query.ProcurementMessagePageQuery;
import org.darkroomlibrary.web.dto.query.ProcurementOrderPageQuery;
import org.darkroomlibrary.web.dto.command.ProcurementAssignDto;
import org.darkroomlibrary.web.dto.command.ProcurementLogisticsUpdateDto;
import org.darkroomlibrary.web.dto.command.ProcurementMessageDto;
import org.darkroomlibrary.web.dto.command.ProcurementMessageReadDto;
import org.darkroomlibrary.web.dto.command.ProcurementOrderCreateDto;
import org.darkroomlibrary.web.dto.command.ProcurementStatusUpdateDto;
import org.darkroomlibrary.domain.type.UserRole;
import org.darkroomlibrary.domain.type.AccountStatus;
import org.darkroomlibrary.domain.model.Book;
import org.darkroomlibrary.domain.model.ProcurementLogistics;
import org.darkroomlibrary.domain.model.ProcurementMessage;
import org.darkroomlibrary.domain.model.ProcurementOrder;
import org.darkroomlibrary.domain.model.User;
import org.darkroomlibrary.web.view.ProcurementMessageView;
import org.darkroomlibrary.web.view.ProcurementOrderView;
import org.darkroomlibrary.web.view.OrderUnreadSummary;
import org.darkroomlibrary.service.OperationAuditService;
import org.darkroomlibrary.service.ProcurementService;
import org.darkroomlibrary.service.ReservationWorkflowService;
import org.darkroomlibrary.service.support.RecommendationSourceVersionService;
import org.darkroomlibrary.utils.ContentSanitizer;
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 org.springframework.transaction.support.TransactionSynchronizationManager;

import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 采购协作服务实现
 */
@Service
@Slf4j
public class ProcurementServiceImpl implements ProcurementService {

    private static final int ORDER_PENDING = 0;
    private static final int ORDER_PURCHASING = 1;
    private static final int ORDER_PLACED = 2;
    private static final int ORDER_SHIPPED = 3;
    private static final int ORDER_ARRIVED = 4;
    private static final int ORDER_WAREHOUSED = 5;
    private static final int ORDER_COMPLETED = 6;
    private static final int ORDER_CANCELED = 7;

    private static final int LOGISTICS_PENDING = 0;
    private static final int LOGISTICS_TRANSIT = 1;
    private static final int LOGISTICS_ARRIVED = 2;
    private static final int LOGISTICS_WAREHOUSED = 3;

    private static final int CHANNEL_ADMIN_PURCHASER = 0;
    private static final int CHANNEL_PURCHASER_LOGISTICS = 1;

    @Resource
    private ProcurementOrderMapper procurementOrderMapper;

    @Resource
    private ProcurementLogisticsMapper procurementLogisticsMapper;

    @Resource
    private ProcurementMessageMapper procurementMessageMapper;

    @Resource
    private BookMapper bookMapper;

    @Resource
    private UserMapper userMapper;

    @Resource
    private OperationAuditService operationAuditService;

    @Resource
    private ReservationWorkflowService reservationWorkflowService;

    @Resource
    private RecommendationSourceVersionService recommendationSourceVersionService;

    @Override
    @Transactional
    public ApiResponse<Void> save(ProcurementOrderCreateDto dto) {
        if (dto == null || dto.getBookId() == null) {
            return ApiResponse.error("请选择需要采购的图书");
        }
        if (dto.getRequestCount() == null || dto.getRequestCount() <= 0) {
            return ApiResponse.error("采购数量必须大于0");
        }
        Map<Integer, User> lockedUsers =
                lockUsers(CurrentUserContext.userId(), dto.getPurchaserId());
        User requester = lockedUsers.get(CurrentUserContext.userId());
        if (!isCurrentUserStateValid(requester) || !isAdmin(requester.getUserRole())) {
            return ApiResponse.error("当前账号状态不允许创建采购单");
        }
        User purchaser = dto.getPurchaserId() == null ? null : lockedUsers.get(dto.getPurchaserId());
        if (dto.getPurchaserId() != null
                && (!isActiveUser(purchaser)
                || !Objects.equals(purchaser.getUserRole(), UserRole.ACQUISITIONS.code()))) {
            return ApiResponse.error("只能指派采购员处理采购单");
        }
        Book book = bookMapper.findByIdForUpdate(dto.getBookId());
        if (book == null || Boolean.TRUE.equals(book.getIsDeleted())) {
            return ApiResponse.error("图书不存在");
        }
        LocalDateTime now = LocalDateTime.now();
        ProcurementOrder order = ProcurementOrder.builder()
                .bookId(book.getId())
                .bookName(book.getName())
                .isbn(book.getIsbn())
                .category(book.getCategory())
                .requestCount(dto.getRequestCount())
                .status(ORDER_PENDING)
                .requesterId(CurrentUserContext.userId())
                .purchaserId(dto.getPurchaserId())
                .requestNote(cleanPlainText(dto.getRequestNote()))
                .stockApplied(false)
                .createTime(now)
                .updateTime(now)
                .build();
        if (procurementOrderMapper.insert(order) != 1) {
            return ApiResponse.error("采购单创建失败,请重试");
        }
        operationAuditService.record("新增", "采购单",
                orderIdentity(order) + ",创建采购单,申请说明=" + auditText(order.getRequestNote()));
        if (order.getPurchaserId() != null) {
            operationAuditService.record("指派", "采购单",
                    orderIdentity(order) + ",采购员:未指派 -> " + userLabel(order.getPurchaserId()));
        }
        return ApiResponse.success("采购单已创建");
    }

    @Override
    @Transactional
    public ApiResponse<Void> assignPurchaser(ProcurementAssignDto dto) {
        if (dto == null || dto.getOrderId() == null || dto.getUserId() == null) {
            return ApiResponse.error("请选择采购单和采购员");
        }
        Map<Integer, User> lockedUsers =
                lockUsers(CurrentUserContext.userId(), dto.getUserId());
        User actor = lockedUsers.get(CurrentUserContext.userId());
        if (!isCurrentUserStateValid(actor) || !isAdmin(actor.getUserRole())) {
            return ApiResponse.error("当前账号权限已变化,请刷新后重试");
        }
        User purchaser = lockedUsers.get(dto.getUserId());
        if (!isActiveUser(purchaser)
                || !Objects.equals(purchaser.getUserRole(), UserRole.ACQUISITIONS.code())) {
            return ApiResponse.error("请选择有效采购员");
        }
        ProcurementOrder order = requireOrderForUpdate(dto == null ? null : dto.getOrderId());
        if (order == null) {
            return ApiResponse.error("采购单不存在");
        }
        if (isAdmin(CurrentUserContext.roleCode()) && !canCurrentAdminAccess(order)) {
            return ApiResponse.error("只能指派自己创建的采购单");
        }
        if (isFinalStatus(order.getStatus())) {
            return ApiResponse.error("已结束的采购单不能重新指派");
        }
        if (procurementOrderMapper.update(ProcurementOrder.builder()
                .id(order.getId())
                .purchaserId(dto.getUserId())
                .updateTime(LocalDateTime.now())
                .build()) != 1) {
            return stateChanged("采购单状态已变化,请刷新后重试");
        }
        if (!Objects.equals(order.getPurchaserId(), dto.getUserId())) {
            operationAuditService.record("指派", "采购单",
                    orderIdentity(order) + ",采购员:" + userLabel(order.getPurchaserId())
                            + " -> " + userLabel(dto.getUserId()));
        }
        return ApiResponse.success("采购员已指派");
    }

    @Override
    @Transactional
    public ApiResponse<Void> claim(Integer id) {
        User currentUser = lockUser(CurrentUserContext.userId());
        if (!isCurrentUserStateValid(currentUser)
                || !Objects.equals(currentUser.getUserRole(), UserRole.ACQUISITIONS.code())) {
            return ApiResponse.error("只有正常状态的采购员可以认领采购单");
        }
        ProcurementOrder order = requireOrderForUpdate(id);
        if (order == null) {
            return ApiResponse.error("采购单不存在");
        }
        if (!isPurchaser(CurrentUserContext.roleCode())) {
            return ApiResponse.error("只有采购员可以认领采购单");
        }
        Integer currentUserId = CurrentUserContext.userId();
        if (order.getPurchaserId() != null && !Objects.equals(order.getPurchaserId(), currentUserId)) {
            return ApiResponse.error("该采购单已由其他采购员处理");
        }
        if (isFinalStatus(order.getStatus())) {
            return ApiResponse.error("已结束的采购单不能认领");
        }
        ProcurementOrder update = ProcurementOrder.builder()
                .id(order.getId())
                .purchaserId(currentUserId)
                .status(Math.max(order.getStatus(), ORDER_PURCHASING))
                .updateTime(LocalDateTime.now())
                .build();
        if (procurementOrderMapper.update(update) != 1) {
            return stateChanged("采购单状态已变化,请刷新后重试");
        }
        if (!Objects.equals(order.getPurchaserId(), currentUserId)
                || order.getStatus() < ORDER_PURCHASING) {
            operationAuditService.record("认领", "采购单",
                    orderIdentity(order) + ",认领人=" + userLabel(currentUserId)
                            + ",采购状态:" + orderStatusName(order.getStatus())
                            + " -> " + orderStatusName(update.getStatus()));
        }
        return ApiResponse.success("采购单已认领");
    }

    @Override
    @Transactional
    public ApiResponse<Void> updateStatus(ProcurementStatusUpdateDto dto) {
        User currentUser = lockUser(CurrentUserContext.userId());
        if (!isCurrentUserStateValid(currentUser)) {
            return ApiResponse.error("当前账号状态不允许更新采购单");
        }
        ProcurementOrder order = requireOrderForUpdate(dto == null ? null : dto.getId());
        if (order == null) {
            return ApiResponse.error("采购单不存在");
        }
        Integer targetStatus = dto.getStatus();
        if (targetStatus == null || targetStatus < ORDER_PENDING || targetStatus > ORDER_CANCELED) {
            return ApiResponse.error("采购状态不正确");
        }
        Integer roleId = CurrentUserContext.roleCode();
        if (isLogistics(roleId)) {
            return ApiResponse.error("物流员请更新物流进度");
        }
        if (isAdmin(roleId) && !canCurrentAdminAccess(order)) {
            return ApiResponse.error("只能处理自己创建的采购单");
        }
        if (!isAdmin(roleId) && !canCurrentPurchaserHandle(order)) {
            return ApiResponse.error("无权处理该采购单");
        }
        if (isFinalStatus(order.getStatus())) {
            return ApiResponse.error("已结束的采购单不能再更新");
        }
        if (targetStatus == ORDER_WAREHOUSED) {
            return ApiResponse.error("入库状态必须通过物流入库操作完成");
        }
        if (targetStatus == ORDER_SHIPPED || targetStatus == ORDER_ARRIVED) {
            return ApiResponse.error("发货和到货状态必须通过物流进度更新");
        }
        if (!isAllowedOrderTransition(order.getStatus(), targetStatus)) {
            return ApiResponse.error("采购状态流转不合法");
        }

        LocalDateTime now = LocalDateTime.now();
        ProcurementOrder update = ProcurementOrder.builder()
                .id(order.getId())
                .status(targetStatus)
                .purchaseNote(cleanPlainText(dto.getPurchaseNote()))
                .updateTime(now)
                .build();
        if (isPurchaser(roleId) && order.getPurchaserId() == null) {
            update.setPurchaserId(CurrentUserContext.userId());
        }
        fillStatusTime(update, targetStatus, now);
        if (procurementOrderMapper.update(update) != 1) {
            return stateChanged("采购单状态已变化,请刷新后重试");
        }
        if (update.getPurchaserId() != null) {
            operationAuditService.record("认领", "采购单",
                    orderIdentity(order) + ",认领人=" + userLabel(update.getPurchaserId())
                            + ",通过状态更新自动认领");
        }
        if (!Objects.equals(order.getStatus(), targetStatus)) {
            String operation = Objects.equals(targetStatus, ORDER_CANCELED) ? "取消" : "流转";
            operationAuditService.record(operation, "采购单",
                    orderIdentity(order) + ",采购状态:" + orderStatusName(order.getStatus())
                            + " -> " + orderStatusName(targetStatus)
                            + ",采购备注=" + auditText(update.getPurchaseNote()));
        }
        return ApiResponse.success("采购状态已更新");
    }

    @Override
    @Transactional
    public ApiResponse<Void> assignLogistics(ProcurementAssignDto dto) {
        if (dto == null || dto.getOrderId() == null || dto.getUserId() == null) {
            return ApiResponse.error("请选择采购单和物流员");
        }
        Map<Integer, User> lockedUsers =
                lockUsers(CurrentUserContext.userId(), dto.getUserId());
        User actor = lockedUsers.get(CurrentUserContext.userId());
        if (!isCurrentUserStateValid(actor)
                || (!isPurchaser(actor.getUserRole()) && !isSuperAdmin(actor.getUserRole()))) {
            return ApiResponse.error("当前账号权限已变化,请刷新后重试");
        }
        User logisticsUser = lockedUsers.get(dto.getUserId());
        if (!isActiveUser(logisticsUser)
                || !Objects.equals(logisticsUser.getUserRole(), UserRole.LOGISTICS.code())) {
            return ApiResponse.error("请选择有效物流员");
        }
        ProcurementOrder order = requireOrderForUpdate(dto == null ? null : dto.getOrderId());
        if (order == null) {
            return ApiResponse.error("采购单不存在");
        }
        if (isFinalStatus(order.getStatus())) {
            return ApiResponse.error("已结束的采购单不能分配物流");
        }
        if (order.getStatus() < ORDER_PLACED) {
            return ApiResponse.error("采购单下单后才能分配物流");
        }
        if (order.getStatus() >= ORDER_WAREHOUSED) {
            return ApiResponse.error("已入库的采购单不能重新分配物流");
        }
        if (!isSuperAdmin(CurrentUserContext.roleCode()) && !canCurrentPurchaserHandle(order)) {
            return ApiResponse.error("只有负责该单的采购员可以分配物流");
        }
        LocalDateTime now = LocalDateTime.now();
        ProcurementOrder update = ProcurementOrder.builder()
                .id(order.getId())
                .logisticsId(dto.getUserId())
                .updateTime(now)
                .build();
        if (isPurchaser(CurrentUserContext.roleCode()) && order.getPurchaserId() == null) {
            update.setPurchaserId(CurrentUserContext.userId());
        }
        if (procurementOrderMapper.update(update) != 1) {
            return stateChanged("采购单状态已变化,请刷新后重试");
        }

        ProcurementLogistics logistics = procurementLogisticsMapper.getByOrderId(order.getId());
        if (logistics == null) {
            if (procurementLogisticsMapper.insert(ProcurementLogistics.builder()
                    .orderId(order.getId())
                    .logisticsUserId(dto.getUserId())
                    .status(LOGISTICS_PENDING)
                    .createTime(now)
                    .updateTime(now)
                    .build()) != 1) {
                return stateChanged("物流任务创建失败,请刷新后重试");
            }
        } else {
            logistics.setLogisticsUserId(dto.getUserId());
            logistics.setUpdateTime(now);
            if (procurementLogisticsMapper.updateById(logistics) != 1) {
                return stateChanged("物流任务状态已变化,请刷新后重试");
            }
        }
        if (!Objects.equals(order.getLogisticsId(), dto.getUserId())) {
            operationAuditService.record("指派", "物流任务",
                    orderIdentity(order) + ",物流员:" + userLabel(order.getLogisticsId())
                            + " -> " + userLabel(dto.getUserId()));
        }
        return ApiResponse.success("物流员已分配");
    }

    @Override
    @Transactional
    public ApiResponse<Void> updateLogistics(ProcurementLogisticsUpdateDto dto) {
        User currentUser = lockUser(CurrentUserContext.userId());
        if (!isCurrentUserStateValid(currentUser)) {
            return ApiResponse.error("当前账号状态不允许更新物流进度");
        }
        ProcurementOrder order = requireOrderForUpdate(dto == null ? null : dto.getOrderId());
        if (order == null) {
            return ApiResponse.error("采购单不存在");
        }
        Integer roleId = CurrentUserContext.roleCode();
        Integer currentUserId = CurrentUserContext.userId();
        if (!isSuperAdmin(roleId) && !isLogistics(roleId) && !canCurrentPurchaserHandle(order)) {
            return ApiResponse.error("无权更新物流进度");
        }
        if (isLogistics(roleId) && !Objects.equals(order.getLogisticsId(), currentUserId)) {
            return ApiResponse.error("只能更新分配给自己的物流任务");
        }
        if (isFinalStatus(order.getStatus())) {
            return ApiResponse.error("已结束的采购单不能更新物流进度");
        }
        if (order.getStatus() < ORDER_PLACED) {
            return ApiResponse.error("采购单下单后才能更新物流进度");
        }
        Integer targetStatus = dto.getStatus();
        if (targetStatus == null || targetStatus < LOGISTICS_PENDING || targetStatus > LOGISTICS_WAREHOUSED) {
            return ApiResponse.error("物流状态不正确");
        }
        if (order.getLogisticsId() == null) {
            return ApiResponse.error("请先分配物流员");
        }
        ProcurementLogistics logistics = procurementLogisticsMapper.getByOrderId(order.getId());
        if (logistics == null) {
            return ApiResponse.error("物流任务不存在,请重新分配物流员");
        }
        Integer previousLogisticsStatus = logistics.getStatus();
        if (!isAllowedLogisticsTransition(previousLogisticsStatus, targetStatus)) {
            return ApiResponse.error("物流状态流转不合法");
        }
        LocalDateTime now = LocalDateTime.now();
        logistics.setStatus(targetStatus);
        logistics.setTrackingNo(cleanPlainText(dto.getTrackingNo()));
        logistics.setCarrier(cleanPlainText(dto.getCarrier()));
        logistics.setRemark(cleanPlainText(dto.getRemark()));
        logistics.setUpdateTime(now);
        if (procurementLogisticsMapper.updateById(ProcurementLogistics.builder()
                .id(logistics.getId())
                .status(logistics.getStatus())
                .trackingNo(logistics.getTrackingNo())
                .carrier(logistics.getCarrier())
                .remark(logistics.getRemark())
                .updateTime(logistics.getUpdateTime())
                .build()) != 1) {
            return stateChanged("物流任务状态已变化,请刷新后重试");
        }

        ProcurementOrder orderUpdate = ProcurementOrder.builder()
                .id(order.getId())
                .updateTime(now)
                .build();
        Integer mappedOrderStatus = mapLogisticsStatus(targetStatus);
        if (mappedOrderStatus != null && mappedOrderStatus > order.getStatus()) {
            orderUpdate.setStatus(mappedOrderStatus);
            fillStatusTime(orderUpdate, mappedOrderStatus, now);
        }
        if (procurementOrderMapper.update(orderUpdate) != 1) {
            return stateChanged("采购单状态已变化,请刷新后重试");
        }

        boolean stockApplied = false;
        if (targetStatus == LOGISTICS_WAREHOUSED) {
            stockApplied = applyStockOnce(order);
        }
        if (!Objects.equals(previousLogisticsStatus, targetStatus)) {
            String operation = Objects.equals(targetStatus, LOGISTICS_WAREHOUSED) ? "入库" : "流转";
            String mappedStatusDetail = mappedOrderStatus != null && mappedOrderStatus > order.getStatus()
                    ? ",采购状态:" + orderStatusName(order.getStatus())
                    + " -> " + orderStatusName(mappedOrderStatus)
                    : "";
            operationAuditService.record(operation, "物流任务",
                    orderIdentity(order) + ",物流状态:"
                            + logisticsStatusName(previousLogisticsStatus)
                            + " -> " + logisticsStatusName(targetStatus)
                            + mappedStatusDetail
                            + ",物流员=" + userLabel(order.getLogisticsId())
                            + ",承运方=" + auditText(logistics.getCarrier())
                            + ",运单号=" + auditText(logistics.getTrackingNo())
                            + ",备注=" + auditText(logistics.getRemark()));
        }
        if (stockApplied) {
            operationAuditService.record("库存补充", "图书库存",
                    orderIdentity(order) + ",库存增加=" + order.getRequestCount()
                            + ",stockApplied=false -> true");
            notifyReservationsAfterCommit(order.getBookId());
        }
        return ApiResponse.success("物流进度已更新");
    }

    @Override
    public ApiResponse<List<ProcurementOrderView>> query(ProcurementOrderPageQuery dto) {
        if (dto == null) {
            dto = new ProcurementOrderPageQuery();
        }
        Integer roleId = CurrentUserContext.roleCode();
        Integer userId = CurrentUserContext.userId();
        if (isLogistics(roleId)) {
            dto.setLogisticsId(CurrentUserContext.userId());
        } else if (isPurchaser(roleId)) {
            dto.setPurchaserId(userId);
            dto.setIncludeUnassignedForPurchaser(true);
        } else if (isAdmin(roleId) && !canCurrentAdminAccessAllOrders()) {
            dto.setRequesterId(userId);
        }
        List<ProcurementOrderView> list = procurementOrderMapper.query(dto);
        Map<Integer, Integer> unreadCounts = unreadCountsByOrder(list, userId);
        for (ProcurementOrderView item : list) {
            item.setRequestNote(cleanPlainText(item.getRequestNote()));
            item.setPurchaseNote(cleanPlainText(item.getPurchaseNote()));
            item.setTrackingNo(cleanPlainText(item.getTrackingNo()));
            item.setCarrier(cleanPlainText(item.getCarrier()));
            item.setLogisticsRemark(cleanPlainText(item.getLogisticsRemark()));
            item.setUnreadCount(unreadCounts.getOrDefault(item.getId(), 0));
        }
        Integer total = procurementOrderMapper.queryCount(dto);
        return PageResponse.success(list, total);
    }

    private Map<Integer, Integer> unreadCountsByOrder(List<ProcurementOrderView> orders, Integer userId) {
        if (orders == null || orders.isEmpty() || userId == null) {
            return Collections.emptyMap();
        }
        List<Integer> orderIds = orders.stream()
                .map(ProcurementOrderView::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (orderIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<OrderUnreadSummary> summaries =
                procurementMessageMapper.countUnreadByOrderIds(userId, orderIds);
        if (summaries == null || summaries.isEmpty()) {
            return Collections.emptyMap();
        }
        return summaries.stream().collect(Collectors.toMap(
                OrderUnreadSummary::getOrderId,
                summary -> summary.getUnreadCount() == null ? 0 : summary.getUnreadCount(),
                Integer::sum,
                LinkedHashMap::new));
    }

    @Override
    public ApiResponse<Map<String, Object>> lowStock(Integer threshold) {
        int actualThreshold = threshold == null || threshold < 0 ? 3 : threshold;
        List<Book> books = bookMapper.queryLowStock(actualThreshold);
        Map<String, Object> result = new HashMap<>();
        result.put("books", books);
        result.put("total", books.size());
        result.put("threshold", actualThreshold);
        return ApiResponse.success(result);
    }

    @Override
    @Transactional
    public ApiResponse<Void> sendMessage(ProcurementMessageDto dto) {
        if (dto == null || dto.getOrderId() == null || dto.getReceiverId() == null) {
            return ApiResponse.error("采购单、接收人和消息内容不能为空");
        }
        Map<Integer, User> lockedUsers =
                lockUsers(CurrentUserContext.userId(), dto.getReceiverId());
        User sender = lockedUsers.get(CurrentUserContext.userId());
        User receiver = lockedUsers.get(dto.getReceiverId());
        if (!isCurrentUserStateValid(sender)) {
            return ApiResponse.error("当前账号状态不允许发送协作消息");
        }
        if (!isActiveUser(receiver)) {
            return ApiResponse.error("接收人不存在或账号不可用");
        }
        ProcurementOrder order = requireOrderForUpdate(dto == null ? null : dto.getOrderId());
        if (order == null) {
            return ApiResponse.error("采购单不存在");
        }
        if (ContentSanitizer.exceedsLength(
                dto.getContent(), ContentSanitizer.PROCUREMENT_MESSAGE_MAX_LENGTH)) {
            return ApiResponse.error("消息内容不能超过1000个字符");
        }
        String cleanContent = ContentSanitizer.plainText(dto.getContent());
        if (cleanContent == null || cleanContent.isEmpty()) {
            return ApiResponse.error("接收人和消息内容不能为空");
        }
        String channelError = validateMessageChannel(order, dto.getChannelType(), sender.getId(),
                sender.getUserRole(), receiver.getId(), receiver.getUserRole());
        if (channelError != null) {
            return ApiResponse.error(channelError);
        }
        ProcurementMessage message = ProcurementMessage.builder()
                .orderId(order.getId())
                .channelType(dto.getChannelType())
                .senderId(sender.getId())
                .receiverId(receiver.getId())
                .content(cleanContent)
                .readStatus(false)
                .createTime(LocalDateTime.now())
                .build();
        if (procurementMessageMapper.insert(message) != 1) {
            return stateChanged("消息发送失败,请重试");
        }
        return ApiResponse.success("消息已发送");
    }

    @Override
    public ApiResponse<List<ProcurementMessageView>> queryMessages(ProcurementMessagePageQuery dto) {
        ProcurementOrder order = requireOrder(dto == null ? null : dto.getOrderId());
        if (order == null) {
            return ApiResponse.error("采购单不存在");
        }
        String error = validateChannelView(order, dto.getChannelType());
        if (error != null) {
            return ApiResponse.error(error);
        }
        List<ProcurementMessageView> list = procurementMessageMapper.query(dto);
        for (ProcurementMessageView message : list) {
            message.setContent(ContentSanitizer.plainText(message.getContent()));
        }
        Integer total = procurementMessageMapper.queryCount(dto);
        return PageResponse.success(list, total);
    }

    @Override
    @Transactional
    public ApiResponse<Void> markRead(ProcurementMessageReadDto dto) {
        List<Integer> messageIds = IdListUtils.normalize(dto == null ? null : dto.getMessageIds());
        if (messageIds.isEmpty() || messageIds.size() > 100) {
            return ApiResponse.error("请选择不超过100条已展示消息");
        }
        ProcurementOrder order = requireOrder(dto.getOrderId());
        if (order == null) {
            return ApiResponse.error("采购单不存在");
        }
        String error = validateChannelView(order, dto.getChannelType());
        if (error != null) {
            return ApiResponse.error(error);
        }
        procurementMessageMapper.markRead(
                CurrentUserContext.userId(),
                dto.getOrderId(),
                dto.getChannelType(),
                messageIds,
                LocalDateTime.now());
        return ApiResponse.success("消息已读");
    }

    @Override
    public ApiResponse<Map<String, Object>> unreadCount(Integer orderId) {
        Map<String, Object> result = new HashMap<>();
        Integer userId = CurrentUserContext.userId();
        result.put("total", procurementMessageMapper.countUnread(userId, orderId, null));
        result.put("adminPurchaser", procurementMessageMapper.countUnread(userId, orderId, CHANNEL_ADMIN_PURCHASER));
        result.put("purchaserLogistics", procurementMessageMapper.countUnread(userId, orderId, CHANNEL_PURCHASER_LOGISTICS));
        return ApiResponse.success(result);
    }

    private ProcurementOrder requireOrder(Integer id) {
        return id == null ? null : procurementOrderMapper.getById(id);
    }

    private ProcurementOrder requireOrderForUpdate(Integer id) {
        return id == null ? null : procurementOrderMapper.findByIdForUpdate(id);
    }

    private <T> ApiResponse<T> stateChanged(String message) {
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return ApiResponse.error(message);
    }

    private boolean canCurrentPurchaserHandle(ProcurementOrder order) {
        Integer currentUserId = CurrentUserContext.userId();
        Integer roleId = CurrentUserContext.roleCode();
        return isPurchaser(roleId)
                && (order.getPurchaserId() == null || Objects.equals(order.getPurchaserId(), currentUserId));
    }

    private boolean applyStockOnce(ProcurementOrder order) {
        if (Boolean.TRUE.equals(order.getStockApplied())) {
            return false;
        }
        int marked = procurementOrderMapper.markStockApplied(order.getId());
        if (marked == 0) {
            return false;
        }
        int updated = bookMapper.increaseStock(order.getBookId(), order.getRequestCount());
        if (updated == 0) {
            throw new IllegalStateException("采购入库失败,图书不存在或数量无效");
        }
        recommendationSourceVersionService.invalidateGlobalAfterCommit();
        return true;
    }

    private Integer mapLogisticsStatus(Integer status) {
        if (Objects.equals(status, LOGISTICS_TRANSIT)) {
            return ORDER_SHIPPED;
        }
        if (Objects.equals(status, LOGISTICS_ARRIVED)) {
            return ORDER_ARRIVED;
        }
        if (Objects.equals(status, LOGISTICS_WAREHOUSED)) {
            return ORDER_WAREHOUSED;
        }
        return null;
    }

    private void fillStatusTime(ProcurementOrder order, Integer status, LocalDateTime now) {
        if (Objects.equals(status, ORDER_PLACED)) {
            order.setOrderTime(now);
        } else if (Objects.equals(status, ORDER_SHIPPED)) {
            order.setShippedTime(now);
        } else if (Objects.equals(status, ORDER_ARRIVED)) {
            order.setArrivalTime(now);
        } else if (Objects.equals(status, ORDER_COMPLETED)) {
            order.setCompletedTime(now);
        }
    }

    private String validateMessageChannel(ProcurementOrder order,
                                          Integer channelType,
                                          Integer senderId,
                                          Integer senderRole,
                                          Integer receiverId,
                                          Integer receiverRole) {
        if (Objects.equals(senderId, receiverId)) {
            return "不能给自己发送协作消息";
        }
        if (Objects.equals(channelType, CHANNEL_ADMIN_PURCHASER)) {
            boolean adminToPurchaser = isAdmin(senderRole) && isPurchaser(receiverRole);
            boolean purchaserToAdmin = isPurchaser(senderRole) && isAdmin(receiverRole);
            if (!adminToPurchaser && !purchaserToAdmin) {
                return "管理员与采购员通道只允许管理员和采购员沟通";
            }
            if (order.getPurchaserId() == null) {
                return "请先指派或认领采购员";
            }
            Integer purchaserId = isPurchaser(senderRole) ? senderId : receiverId;
            if (!Objects.equals(order.getPurchaserId(), purchaserId)) {
                return "只能与该采购单的负责采购员沟通";
            }
            Integer adminId = isAdmin(senderRole) ? senderId : receiverId;
            Integer adminRole = isAdmin(senderRole) ? senderRole : receiverRole;
            if (!canAdminAccessOrder(adminId, adminRole, order)) {
                return "普通管理员只能参与自己创建采购单的沟通";
            }
            return null;
        }
        if (Objects.equals(channelType, CHANNEL_PURCHASER_LOGISTICS)) {
            boolean purchaserToLogistics = isPurchaser(senderRole) && isLogistics(receiverRole);
            boolean logisticsToPurchaser = isLogistics(senderRole) && isPurchaser(receiverRole);
            if (!purchaserToLogistics && !logisticsToPurchaser) {
                return "采购员与物流员通道只允许采购员和物流员沟通";
            }
            if (order.getPurchaserId() == null || order.getLogisticsId() == null) {
                return "请先确认采购员和物流员";
            }
            Integer purchaserId = isPurchaser(senderRole) ? senderId : receiverId;
            Integer logisticsId = isLogistics(senderRole) ? senderId : receiverId;
            if (!Objects.equals(order.getPurchaserId(), purchaserId)
                    || !Objects.equals(order.getLogisticsId(), logisticsId)) {
                return "只能在当前采购单的采购员和物流员之间沟通";
            }
            return null;
        }
        return "消息通道不正确";
    }

    private String validateChannelView(ProcurementOrder order, Integer channelType) {
        Integer roleId = CurrentUserContext.roleCode();
        Integer userId = CurrentUserContext.userId();
        if (Objects.equals(channelType, CHANNEL_ADMIN_PURCHASER)) {
            if (isSuperAdmin(roleId)) {
                return null;
            }
            if (canAdminAccessOrder(userId, roleId, order)) {
                return null;
            }
            if (isPurchaser(roleId) && Objects.equals(order.getPurchaserId(), userId)) {
                return null;
            }
            return "无权查看管理员与采购员消息";
        }
        if (Objects.equals(channelType, CHANNEL_PURCHASER_LOGISTICS)) {
            if (isSuperAdmin(roleId)) {
                return null;
            }
            if (isPurchaser(roleId) && Objects.equals(order.getPurchaserId(), userId)) {
                return null;
            }
            if (isLogistics(roleId) && Objects.equals(order.getLogisticsId(), userId)) {
                return null;
            }
            return "无权查看采购员与物流员消息";
        }
        return "消息通道不正确";
    }

    private User lockUser(Integer userId) {
        return userId == null ? null : userMapper.findByIdForUpdate(userId);
    }

    private Map<Integer, User> lockUsers(Integer... userIds) {
        List<Integer> ids = new ArrayList<>();
        if (userIds != null) {
            for (Integer userId : userIds) {
                if (userId != null && !ids.contains(userId)) {
                    ids.add(userId);
                }
            }
        }
        Collections.sort(ids);
        Map<Integer, User> users = new LinkedHashMap<>();
        for (Integer userId : ids) {
            users.put(userId, userMapper.findByIdForUpdate(userId));
        }
        return users;
    }

    private boolean isActiveUser(User user) {
        return user != null
                && Objects.equals(user.getAccountStatus(), AccountStatus.NORMAL.code())
                && !Boolean.TRUE.equals(user.getIsLogin());
    }

    private boolean isCurrentUserStateValid(User user) {
        return isActiveUser(user)
                && Objects.equals(user.getUserRole(), CurrentUserContext.roleCode());
    }

    private boolean canCurrentAdminAccess(ProcurementOrder order) {
        Integer roleId = CurrentUserContext.roleCode();
        if (canCurrentAdminAccessAllOrders()) {
            return true;
        }
        return Objects.equals(roleId, UserRole.ADMIN.code())
                && Objects.equals(order.getRequesterId(), CurrentUserContext.userId());
    }

    private boolean canCurrentAdminAccessAllOrders() {
        Integer roleId = CurrentUserContext.roleCode();
        return isSuperAdmin(roleId) || isCoordinatorAdmin(CurrentUserContext.userId(), roleId);
    }

    private boolean canAdminAccessOrder(Integer adminId, Integer adminRole, ProcurementOrder order) {
        if (isSuperAdmin(adminRole) || isCoordinatorAdmin(adminId, adminRole)) {
            return true;
        }
        return Objects.equals(adminRole, UserRole.ADMIN.code())
                && Objects.equals(order.getRequesterId(), adminId);
    }

    private boolean isCoordinatorAdmin(Integer userId, Integer roleId) {
        if (!Objects.equals(roleId, UserRole.ADMIN.code())) {
            return false;
        }
        User user = userMapper.getById(userId);
        return user != null && Boolean.TRUE.equals(user.getIsCoordinatorAdmin());
    }

    private boolean isAdmin(Integer roleId) {
        return Objects.equals(roleId, UserRole.SUPER_ADMIN.code())
                || Objects.equals(roleId, UserRole.ADMIN.code());
    }

    private boolean isSuperAdmin(Integer roleId) {
        return Objects.equals(roleId, UserRole.SUPER_ADMIN.code());
    }

    private boolean isPurchaser(Integer roleId) {
        return Objects.equals(roleId, UserRole.ACQUISITIONS.code());
    }

    private boolean isLogistics(Integer roleId) {
        return Objects.equals(roleId, UserRole.LOGISTICS.code());
    }

    private boolean isFinalStatus(Integer status) {
        return Objects.equals(status, ORDER_COMPLETED) || Objects.equals(status, ORDER_CANCELED);
    }

    private boolean isAllowedOrderTransition(Integer current, Integer target) {
        if (Objects.equals(current, target)) {
            return true;
        }
        if (Objects.equals(target, ORDER_CANCELED)) {
            return current != null && current < ORDER_WAREHOUSED;
        }
        if (Objects.equals(target, ORDER_COMPLETED)) {
            return Objects.equals(current, ORDER_WAREHOUSED);
        }
        return current != null && target != null
                && target == current + 1
                && target <= ORDER_PLACED;
    }

    private boolean isAllowedLogisticsTransition(Integer current, Integer target) {
        return Objects.equals(current, target)
                || (current != null && target != null && target == current + 1);
    }

    private String orderIdentity(ProcurementOrder order) {
        return "采购单ID=" + order.getId()
                + ",图书ID=" + order.getBookId()
                + ",书名=" + order.getBookName()
                + ",采购数量=" + order.getRequestCount();
    }

    private String userLabel(Integer userId) {
        if (userId == null) {
            return "未指派";
        }
        User user = userMapper.getById(userId);
        return user == null ? "用户#" + userId : user.getUserName() + "(ID=" + userId + ")";
    }

    private String orderStatusName(Integer status) {
        if (Objects.equals(status, ORDER_PENDING)) return "待采购";
        if (Objects.equals(status, ORDER_PURCHASING)) return "采购中";
        if (Objects.equals(status, ORDER_PLACED)) return "已下单";
        if (Objects.equals(status, ORDER_SHIPPED)) return "已发货";
        if (Objects.equals(status, ORDER_ARRIVED)) return "已到货";
        if (Objects.equals(status, ORDER_WAREHOUSED)) return "已入库";
        if (Objects.equals(status, ORDER_COMPLETED)) return "已完成";
        if (Objects.equals(status, ORDER_CANCELED)) return "已取消";
        return "未知状态(" + status + ")";
    }

    private String logisticsStatusName(Integer status) {
        if (Objects.equals(status, LOGISTICS_PENDING)) return "待接收";
        if (Objects.equals(status, LOGISTICS_TRANSIT)) return "运输中";
        if (Objects.equals(status, LOGISTICS_ARRIVED)) return "已到馆";
        if (Objects.equals(status, LOGISTICS_WAREHOUSED)) return "已入库";
        return "未知状态(" + status + ")";
    }

    private String auditText(String value) {
        String cleanValue = trimToNull(value);
        if (cleanValue == null) {
            return "无";
        }
        return cleanValue.length() <= 200 ? cleanValue : cleanValue.substring(0, 200) + "...";
    }

    private String trimToNull(String value) {
        if (value == null || value.trim().isEmpty()) {
            return null;
        }
        return value.trim();
    }

    private String cleanPlainText(String value) {
        return trimToNull(ContentSanitizer.plainText(value));
    }

    private void notifyReservationsAfterCommit(Integer bookId) {
        Runnable notification = () -> {
            try {
                reservationWorkflowService.onBookReturned(bookId);
            } catch (Exception e) {
                log.warn("采购入库后的预约通知失败,等待定时对账: bookId={}, error={}",
                        bookId, e.getMessage());
            }
        };
        TransactionCallbacks.afterCommit(notification);
    }
}