package com.njcn.rdms.module.project.service.product;

import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.annotations.VisibleForTesting;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementModuleDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;

/**
 * 产品需求 Service 实现类
 */
@Service
public class ProductRequirementServiceImpl implements ProductRequirementService {

    // 对象类型常量
    private static final String REQUIREMENT_OBJECT_TYPE = "product_requirement";
    private static final String PRODUCT_OBJECT_TYPE = "product";

    // 需求状态常量
    private static final String STATUS_PENDING_CLAIM = "pending_claim";
    private static final String STATUS_PENDING_REVIEW = "pending_review";
    private static final String STATUS_PENDING_DISPATCH = "pending_dispatch";
    private static final String STATUS_REVIEWED = "reviewed";
    private static final String STATUS_IMPLEMENTING = "implementing";
    private static final String STATUS_ACCEPTED = "accepted";
    private static final String STATUS_CLOSED = "closed";
    private static final String STATUS_REJECTED = "rejected";
    private static final String STATUS_CANCELLED = "cancelled";
    private static final String STATUS_REVIEW_REJECTED = "review_rejected";
    private static final String SOURCE_TYPE_MANUAL = "manual";
    private static final String SOURCE_TYPE_WORK_ORDER = "work_order";

    // 允许删除的状态集合（实施中之前的状态）
    private static final List<String> ALLOW_DELETE_STATUSES = List.of(STATUS_PENDING_CLAIM, STATUS_PENDING_REVIEW, STATUS_PENDING_DISPATCH);
    // 父需求取消时，子需求允许的状态集合（仅已拒绝和已取消）
    private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(STATUS_REJECTED, STATUS_CANCELLED);

    // 权限常量
    private static final String PRODUCT_CREATE_PERMISSION = "project:product:requirement:create";
    private static final String PRODUCT_UPDATE_PERMISSION = "project:product:requirement:update";
    private static final String PRODUCT_STATUS_PERMISSION = "project:product:requirement:status";
    private static final String PRODUCT_REVIEW_PERMISSION = "project:product:requirement:review";
    private static final String PRODUCT_DELETE_PERMISSION = "project:product:requirement:delete";
    private static final String PRODUCT_SPLIT_PERMISSION = "project:product:requirement:split";

    // 审计动作常量
    private static final String ACTION_CREATE = "create";
    private static final String ACTION_UPDATE = "update";
    private static final String ACTION_DELETE = "delete";
    private static final String ACTION_SPLIT = "split";
    private static final String ACTION_CLOSE = "close";
    private static final String ACTION_ACCEPT = "accept";
    private static final String ACTION_DISPATCH = "dispatch";
    private static final String ACTION_CANCEL = "cancel";
    private static final String ACTION_REJECT = "reject";
    private static final String ACTION_PASS_REVIEW = "pass_review";
    private static final String ACTION_REJECT_REVIEW = "reject_review";
    private static final String ACTION_AUTO_DERIVE = "auto_derive";

    private static final String BIZ_TYPE_REQUIREMENT = "product_requirement";
    private static final String AUTO_DERIVE_REASON = "根据子需求状态自动推导";
    private static final int DASHBOARD_RECENT_CHANGE_LIMIT = 5;
    private static final String DASHBOARD_ACTION_STATUS_TERMINAL = "status_terminal";
    private static final String DASHBOARD_LABEL_CREATE = "需求新增";
    private static final String DASHBOARD_LABEL_DELETE = "需求删除";
    private static final String DASHBOARD_LABEL_STATUS = "状态流转";

    @Resource
    private ProductRequirementMapper requirementMapper;
    @Resource
    private ProductRequirementModuleMapper moduleMapper;
    @Resource
    private ProductRequirementStatusLogMapper statusLogMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ObjectStatusTransitionMapper statusTransitionMapper;
    @Resource
    private ObjectStatusModelMapper statusModelMapper;
    @Resource
    private ProjectRequirementMapper projectRequirementMapper;
    @Resource
    private ProjectRequirementModuleMapper projectRequirementModuleMapper;
    @Resource
    private UserObjectRoleMapper userObjectRoleMapper;
    @Resource
    private AttachmentFileIdResolver attachmentFileIdResolver;
    @Resource
    private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
    @Resource
    private ProductMapper productMapper;
    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    // ========== 需求增删改查 ==========

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#createReqVO.productId",
            permission = PRODUCT_CREATE_PERMISSION)
    public Long createRequirement(ProductRequirementSaveReqVO createReqVO) {
        // 当未选择模块时，自动归属到该产品的"全部需求"模块
        Long moduleId = resolveModuleId(createReqVO.getModuleId(), createReqVO.getProductId());
        // 校验模块是否属于当前产品
        validateModuleBelongsToProduct(moduleId, createReqVO.getProductId());
        AttachmentValidator.validate(createReqVO.getAttachments());
        attachmentFileIdResolver.resolve(createReqVO.getAttachments());

        ProductRequirementDO requirement = new ProductRequirementDO();
        requirement.setProductId(createReqVO.getProductId());
        requirement.setParentId(0L); // 新增需求默认是顶级需求
        requirement.setModuleId(moduleId);
        requirement.setReviewRequired(createReqVO.getReviewRequired());
        requirement.setTitle(createReqVO.getTitle().trim());
        requirement.setDescription(normalizeNullableText(createReqVO.getDescription()));
        requirement.setCategory(createReqVO.getCategory());
        requirement.setSourceType(SOURCE_TYPE_MANUAL); // 手工新增默认来源类型
        requirement.setSourceBizCode(normalizeNullableText(createReqVO.getSourceBizCode()));
        requirement.setPriority(createReqVO.getPriority());
        // 根据是否需要评审确定初始状态
        String initialStatus = Objects.equals(createReqVO.getReviewRequired(), 1)
                ? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
        requirement.setStatusCode(initialStatus);
        requirement.setProposerId(createReqVO.getProposerId());
        requirement.setProposerNickname(normalizeNullableText(createReqVO.getProposerNickname()));
        requirement.setExpectedTime(createReqVO.getExpectedTime());
        requirement.setCurrentHandlerUserId(createReqVO.getCurrentHandlerUserId());
        requirement.setCurrentHandlerUserNickname(normalizeNullableText(createReqVO.getCurrentHandlerUserNickname()));
        requirement.setImplementProjectId(createReqVO.getImplementProjectId());
        requirement.setSort(createReqVO.getSort());
        requirement.setAttachments(createReqVO.getAttachments());
        requirementMapper.insert(requirement);

        // 写入业务审计日志
        writeBizAuditLog(requirement, ACTION_CREATE, null, initialStatus,
                buildRequirementFieldChanges(null, requirement), null);
        // 创建后通知处理人
        publishRequirementAssignedNotify(requirement);
        return requirement.getId();
    }

    /**
     * 创建产品需求后通知处理人（currentHandlerUserId）；提出人不发，操作人由监听器统一排除。
     * 仅 publish，发送由 NotifySendEventListener 事务提交后处理；通知失败不影响需求创建。
     */
    @VisibleForTesting
    void publishRequirementAssignedNotify(ProductRequirementDO requirement) {
        Long handlerId = requirement.getCurrentHandlerUserId();
        if (handlerId == null) {
            return; // 无处理人则不发
        }
        Map<String, Object> params = new HashMap<>();
        ProductDO product = productMapper.selectById(requirement.getProductId());
        params.put("productName", product == null ? "" : product.getName());
        params.put("requirementTitle", requirement.getTitle());
        applicationEventPublisher.publishEvent(NotifySendEvent.of(
                List.of(handlerId), NotifyTemplateCodeConstants.PRODUCT_REQUIREMENT_ASSIGNED, params,
                NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#updateReqVO.productId",
            permission = PRODUCT_UPDATE_PERMISSION)
    public void updateRequirement(ProductRequirementUpdateReqVO updateReqVO) {
        ProductRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
        // 校验当前状态是否允许编辑
        validateRequirementEditable(requirement);
        // 当未选择模块时，自动归属到该产品的"全部需求"模块
        Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProductId());
        // 校验模块是否属于当前产品
        validateModuleBelongsToProduct(moduleId, updateReqVO.getProductId());
        AttachmentValidator.validate(updateReqVO.getAttachments());
        attachmentFileIdResolver.resolve(updateReqVO.getAttachments());

        ProductRequirementDO before = cloneRequirement(requirement);
        String fromStatus = requirement.getStatusCode();
        requirement.setModuleId(moduleId);
        requirement.setReviewRequired(updateReqVO.getReviewRequired());
        requirement.setTitle(updateReqVO.getTitle().trim());
        requirement.setDescription(normalizeNullableText(updateReqVO.getDescription()));
        requirement.setCategory(updateReqVO.getCategory());
        requirement.setSourceBizCode(normalizeNullableText(updateReqVO.getSourceBizCode()));
        requirement.setPriority(updateReqVO.getPriority());
        requirement.setProposerId(updateReqVO.getProposerId());
        requirement.setProposerNickname(normalizeNullableText(updateReqVO.getProposerNickname()));
        requirement.setExpectedTime(updateReqVO.getExpectedTime());
        requirement.setCurrentHandlerUserId(updateReqVO.getCurrentHandlerUserId());
        requirement.setCurrentHandlerUserNickname(normalizeNullableText(updateReqVO.getCurrentHandlerUserNickname()));
        requirement.setImplementProjectId(updateReqVO.getImplementProjectId());
        requirement.setSort(updateReqVO.getSort());
        requirement.setAttachments(updateReqVO.getAttachments());
        requirementMapper.updateById(requirement);

        writeBizAuditLog(requirement, ACTION_UPDATE, fromStatus, requirement.getStatusCode(),
                buildRequirementFieldChanges(before, requirement), null);
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", accessible = true)
    public ProductRequirementRespVO getRequirement(Long id, Long productId) {
        ProductRequirementDO requirement = validateRequirementExists(id);
        return buildRequirementRespVO(requirement);
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#pageReqVO.productId", accessible = true)
    public PageResult<ProductRequirementRespVO> getRequirementPage(ProductRequirementPageReqVO pageReqVO) {
        // 处理模块ID条件：支持递归查询子模块
        if (pageReqVO.getModuleId() != null) {
            if (isAllRequirementsModule(pageReqVO.getModuleId())) {
                // "全部需求"模块，忽略模块ID条件，查询该产品下所有需求
                pageReqVO.setModuleId(null);
            } else {
                // 非"全部需求"模块，获取该模块及其所有子模块的ID列表
                List<Long> moduleIds = getAllModuleIdsWithChildren(pageReqVO.getModuleId(), pageReqVO.getProductId());
                pageReqVO.setModuleIds(moduleIds);
                pageReqVO.setModuleId(null);
            }
        }
        PageResult<ProductRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO);
        Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();
        List<ProductRequirementRespVO> list = pageResult.getList().stream()
                .map(requirement -> buildRequirementRespVO(requirement, statusModelMap))
                .collect(Collectors.toList());
        return new PageResult<>(list, pageResult.getTotal());
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#pageReqVO.productId", accessible = true)
    public PageResult<ProductRequirementRespVO> getRequirementTree(ProductRequirementPageReqVO pageReqVO) {
        Long moduleId = pageReqVO.getModuleId();
        Long productId = pageReqVO.getProductId();

        // 处理模块过滤条件：仅当选中具体模块时，递归加载子模块ID进行过滤
        if (moduleId != null) {
            pageReqVO.setModuleIds(getAllModuleIdsWithChildren(moduleId, productId));
            pageReqVO.setModuleId(null);
        }
        // 清空parentId，查询所有符合条件的需求（不限层级）
        pageReqVO.setParentId(null);

        // 第一步：查询所有符合搜索条件的需求
        List<ProductRequirementDO> matchedRequirements = requirementMapper.selectList(pageReqVO);
        if (matchedRequirements.isEmpty()) {
            return new PageResult<>(Collections.emptyList(), 0L);
        }

        Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();

        // 第二步：找出所有匹配需求的根节点ID，同时收集路径上的所有节点ID
        Set<Long> rootIds = new HashSet<>();
        Set<Long> pathNodeIds = new HashSet<>();
        Map<Long, ProductRequirementDO> requirementCache = buildRequirementCacheWithAncestors(matchedRequirements);
        for (ProductRequirementDO req : matchedRequirements) {
            pathNodeIds.add(req.getId());
            Long rootId = findRootRequirementIdAndCollectPath(req, requirementCache, pathNodeIds);
            rootIds.add(rootId);
        }

        // 第三步：查询根需求详情并按排序值升序、创建时间倒排
        List<ProductRequirementDO> rootRequirements = requirementMapper.selectBatchIds(rootIds);
        rootRequirements.sort((a, b) -> {
            int sortCompare = Comparator.nullsLast(Integer::compareTo).compare(a.getSort(), b.getSort());
            if (sortCompare != 0) {
                return sortCompare;
            }
            return b.getCreateTime().compareTo(a.getCreateTime());
        });

        // 第四步：对根节点列表进行内存分页
        int pageNo = pageReqVO.getPageNo() != null ? pageReqVO.getPageNo() : 1;
        int pageSize = pageReqVO.getPageSize() != null ? pageReqVO.getPageSize() : 10;
        int total = rootRequirements.size();
        int fromIndex = (pageNo - 1) * pageSize;
        int toIndex = Math.min(fromIndex + pageSize, total);

        if (fromIndex >= total) {
            return new PageResult<>(Collections.emptyList(), (long) total);
        }

        List<ProductRequirementDO> pagedRootRequirements = rootRequirements.subList(fromIndex, toIndex);
        Map<Long, List<ProductRequirementDO>> childrenMap = buildPathChildrenMap(pathNodeIds);

        // 第五步：构建树形结构（只包含路径上的节点）
        List<ProductRequirementRespVO> list = pagedRootRequirements.stream()
                .map(req -> buildRequirementRespVOWithPathChildren(req, pathNodeIds, childrenMap, statusModelMap))
                .collect(Collectors.toList());

        return new PageResult<>(list, (long) total);
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", accessible = true)
    public ProductRequirementDashboardRespVO getRequirementDashboard(Long productId) {
        List<ProductRequirementDO> requirements = requirementMapper.selectListByProductId(productId);
        Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();

        ProductRequirementDashboardRespVO respVO = new ProductRequirementDashboardRespVO();
        respVO.setSummary(buildRequirementDashboardSummary(requirements));
        respVO.setRecentChanges(buildRequirementDashboardRecentChanges(productId, requirements, statusModelMap));
        return respVO;
    }

    private ProductRequirementDashboardSummaryRespVO buildRequirementDashboardSummary(List<ProductRequirementDO> requirements) {
        long total = requirements.size();
        long pendingClaim = requirements.stream()
                .filter(requirement -> STATUS_PENDING_CLAIM.equals(requirement.getStatusCode()))
                .count();
        long pendingReview = requirements.stream()
                .filter(requirement -> STATUS_PENDING_REVIEW.equals(requirement.getStatusCode()))
                .count();
        long pendingDispatch = requirements.stream()
                .filter(requirement -> isPendingDispatchActionStatus(requirement.getStatusCode()))
                .count();
        long todo = pendingClaim + pendingReview + pendingDispatch;
        long completed = requirements.stream()
                .filter(requirement -> STATUS_ACCEPTED.equals(requirement.getStatusCode())
                        || STATUS_CLOSED.equals(requirement.getStatusCode()))
                .count();
        long highPriorityTodo = requirements.stream()
                .filter(requirement -> isTodoStatus(requirement.getStatusCode()))
                .filter(requirement -> requirement.getPriority() != null
                        && (requirement.getPriority() == 0 || requirement.getPriority() == 1))
                .count();

        ProductRequirementDashboardSummaryRespVO summary = new ProductRequirementDashboardSummaryRespVO();
        summary.setTotal(total);
        summary.setTodo(todo);
        summary.setPendingClaim(pendingClaim);
        summary.setPendingReview(pendingReview);
        summary.setPendingDispatch(pendingDispatch);
        summary.setCompleted(completed);
        summary.setCompletionRate(total == 0 ? 0 : Math.toIntExact(Math.round(completed * 100.0 / total)));
        summary.setHighPriorityTodo(highPriorityTodo);
        return summary;
    }

    private List<ProductRequirementDashboardRecentChangeRespVO> buildRequirementDashboardRecentChanges(
            Long productId, List<ProductRequirementDO> requirements, Map<String, ObjectStatusModelDO> statusModelMap) {
        List<DashboardRecentChangeItem> items = new ArrayList<>();
        appendRequirementAuditRecentChanges(productId, statusModelMap, items);
        appendRequirementTerminalStatusRecentChanges(requirements, statusModelMap, items);

        items.sort((left, right) -> {
            int timeCompare = compareNullableLocalDateTimeDesc(left.occurredAt(), right.occurredAt());
            if (timeCompare != 0) {
                return timeCompare;
            }
            return compareNullableLongDesc(left.sourceId(), right.sourceId());
        });

        return items.stream()
                .limit(DASHBOARD_RECENT_CHANGE_LIMIT)
                .map(DashboardRecentChangeItem::respVO)
                .collect(Collectors.toList());
    }

    private int compareNullableLocalDateTimeDesc(LocalDateTime left, LocalDateTime right) {
        if (left == null && right == null) {
            return 0;
        }
        if (left == null) {
            return 1;
        }
        if (right == null) {
            return -1;
        }
        return right.compareTo(left);
    }

    private int compareNullableLongDesc(Long left, Long right) {
        if (left == null && right == null) {
            return 0;
        }
        if (left == null) {
            return 1;
        }
        if (right == null) {
            return -1;
        }
        return right.compareTo(left);
    }

    private void appendRequirementAuditRecentChanges(Long productId, Map<String, ObjectStatusModelDO> statusModelMap,
                                                     List<DashboardRecentChangeItem> items) {
        List<BizAuditLogDO> logs = bizAuditLogMapper.selectListByBizTypeAndActions(
                BIZ_TYPE_REQUIREMENT, List.of(ACTION_CREATE, ACTION_DELETE), null, null);
        for (BizAuditLogDO log : logs) {
            if (ACTION_CREATE.equals(log.getActionType())) {
                appendRequirementCreateRecentChange(productId, statusModelMap, items, log);
            } else if (ACTION_DELETE.equals(log.getActionType())) {
                appendRequirementDeleteRecentChange(productId, items, log);
            }
        }
    }

    private void appendRequirementCreateRecentChange(Long productId, Map<String, ObjectStatusModelDO> statusModelMap,
                                                     List<DashboardRecentChangeItem> items, BizAuditLogDO log) {
        Long logProductId = getFieldChangeLong(log.getFieldChanges(), "productId", "after");
        if (!Objects.equals(logProductId, productId)) {
            return;
        }
        String title = getFieldChangeString(log.getFieldChanges(), "title", "after");
        String statusCode = getFieldChangeString(log.getFieldChanges(), "statusCode", "after");
        ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
                "requirement:create:" + log.getId(),
                log.getBizId(),
                defaultDashboardTitle(title),
                ACTION_CREATE,
                DASHBOARD_LABEL_CREATE,
                "当前状态：" + resolveStatusName(statusModelMap, statusCode),
                log.getCreateTime(),
                log.getOperatorUserId(),
                log.getOperatorName());
        items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
    }

    private void appendRequirementDeleteRecentChange(Long productId, List<DashboardRecentChangeItem> items,
                                                     BizAuditLogDO log) {
        Long logProductId = getFieldChangeLong(log.getFieldChanges(), "productId", "before");
        if (!Objects.equals(logProductId, productId)) {
            return;
        }
        String title = getFieldChangeString(log.getFieldChanges(), "title", "before");
        ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
                "requirement:delete:" + log.getId(),
                log.getBizId(),
                defaultDashboardTitle(title),
                ACTION_DELETE,
                DASHBOARD_LABEL_DELETE,
                "该需求已被删除",
                log.getCreateTime(),
                log.getOperatorUserId(),
                log.getOperatorName());
        items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
    }

    private void appendRequirementTerminalStatusRecentChanges(List<ProductRequirementDO> requirements,
                                                              Map<String, ObjectStatusModelDO> statusModelMap,
                                                              List<DashboardRecentChangeItem> items) {
        List<Long> requirementIds = requirements.stream()
                .map(ProductRequirementDO::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        List<String> terminalStatusCodes = statusModelMapper
                .selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE);
        List<ProductRequirementStatusLogDO> logs = statusLogMapper
                .selectListByRequirementIdsAndToStatuses(requirementIds, terminalStatusCodes);
        for (ProductRequirementStatusLogDO log : logs) {
            String statusName = resolveStatusName(statusModelMap, log.getToStatus());
            ProductRequirementDashboardRecentChangeRespVO respVO = buildDashboardRecentChange(
                    "requirement:status:" + log.getId(),
                    log.getRequirementId(),
                    defaultDashboardTitle(log.getRequirementTitleSnapshot()),
                    DASHBOARD_ACTION_STATUS_TERMINAL,
                    DASHBOARD_LABEL_STATUS,
                    "流转至终止态：" + statusName,
                    log.getCreateTime(),
                    log.getOperatorUserId(),
                    log.getOperatorName());
            items.add(new DashboardRecentChangeItem(log.getId(), log.getCreateTime(), respVO));
        }
    }

    private ProductRequirementDashboardRecentChangeRespVO buildDashboardRecentChange(
            String id, Long requirementId, String title, String actionType, String actionLabel,
            String content, LocalDateTime occurredAt, Long operatorUserId, String operatorName) {
        ProductRequirementDashboardRecentChangeRespVO respVO = new ProductRequirementDashboardRecentChangeRespVO();
        respVO.setId(id);
        respVO.setRequirementId(requirementId);
        respVO.setTitle(title);
        respVO.setActionType(actionType);
        respVO.setActionLabel(actionLabel);
        respVO.setContent(content);
        respVO.setOccurredAt(occurredAt);
        respVO.setOperatorUserId(operatorUserId);
        respVO.setOperatorName(operatorName);
        return respVO;
    }

    private String resolveStatusName(Map<String, ObjectStatusModelDO> statusModelMap, String statusCode) {
        if (!StringUtils.hasText(statusCode)) {
            return "未知状态";
        }
        ObjectStatusModelDO statusModel = statusModelMap.get(statusCode);
        return statusModel != null && StringUtils.hasText(statusModel.getStatusName())
                ? statusModel.getStatusName() : statusCode;
    }

    private String defaultDashboardTitle(String title) {
        return StringUtils.hasText(title) ? title : "未命名需求";
    }

    private boolean isTodoStatus(String statusCode) {
        return STATUS_PENDING_CLAIM.equals(statusCode)
                || STATUS_PENDING_REVIEW.equals(statusCode)
                || isPendingDispatchActionStatus(statusCode);
    }

    /**
     * 产品对象域的概览首页“待指派”表示等待执行指派动作，不等同于单一 pending_dispatch 状态。
     */
    private boolean isPendingDispatchActionStatus(String statusCode) {
        return STATUS_PENDING_DISPATCH.equals(statusCode) || STATUS_REVIEWED.equals(statusCode);
    }

    private Long getFieldChangeLong(String fieldChanges, String fieldName, String valueField) {
        JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
        if (valueNode == null || valueNode.isNull()) {
            return null;
        }
        if (valueNode.isNumber()) {
            return valueNode.longValue();
        }
        if (valueNode.isTextual() && StringUtils.hasText(valueNode.textValue())) {
            return Long.valueOf(valueNode.textValue().trim());
        }
        return null;
    }

    private String getFieldChangeString(String fieldChanges, String fieldName, String valueField) {
        JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
        if (valueNode == null || valueNode.isNull()) {
            return null;
        }
        if (valueNode.isTextual()) {
            return valueNode.textValue();
        }
        return valueNode.asText();
    }

    private JsonNode getFieldChangeNode(String fieldChanges, String fieldName, String valueField) {
        if (!StringUtils.hasText(fieldChanges) || !JsonUtils.isJsonObject(fieldChanges)) {
            return null;
        }
        JsonNode fieldNode = JsonUtils.parseTree(fieldChanges).path(fieldName);
        if (fieldNode.isMissingNode()) {
            return null;
        }
        JsonNode valueNode = fieldNode.path(valueField);
        return valueNode.isMissingNode() ? null : valueNode;
    }

    private record DashboardRecentChangeItem(Long sourceId, LocalDateTime occurredAt,
                                             ProductRequirementDashboardRecentChangeRespVO respVO) {
    }

    /**
     * 向上追溯需求的根节点ID，同时收集路径上的所有节点ID
     *
     * @param requirement 起始需求
     * @param cache       需求缓存（避免重复查询）
     * @param pathNodeIds 路径节点ID集合（输出参数）
     * @return 根节点ID（parentId = 0L 的需求ID）
     */
    private Long findRootRequirementIdAndCollectPath(ProductRequirementDO requirement,
                                                     Map<Long, ProductRequirementDO> cache,
                                                     Set<Long> pathNodeIds) {
        ProductRequirementDO current = requirement;
        while (current.getParentId() != null && current.getParentId() != 0L) {
            ProductRequirementDO parent = cache.get(current.getParentId());
            if (parent == null) {
                // 父需求缺失时，保持原有容错行为：就近把当前节点视为根节点
                return current.getId();
            }
            pathNodeIds.add(parent.getId());
            current = parent;
        }
        return current.getId();
    }

    /**
     * 构建需求响应VO（只包含路径上的子需求）
     *
     * @param requirement 需求
     * @param pathNodeIds 路径节点ID集合
     * @return 需求响应VO
     */
    private ProductRequirementRespVO buildRequirementRespVOWithPathChildren(ProductRequirementDO requirement,
                                                                            Set<Long> pathNodeIds,
                                                                            Map<Long, List<ProductRequirementDO>> childrenMap,
                                                                            Map<String, ObjectStatusModelDO> statusModelMap) {
        ProductRequirementRespVO respVO = buildRequirementRespVO(requirement, statusModelMap);
        List<ProductRequirementDO> allChildren = childrenMap.getOrDefault(requirement.getId(), Collections.emptyList());
        List<ProductRequirementDO> pathChildren = allChildren.stream()
                .filter(child -> pathNodeIds.contains(child.getId()))
                .toList();
        if (!pathChildren.isEmpty()) {
            respVO.setChildren(pathChildren.stream()
                    .map(child -> buildRequirementRespVOWithPathChildren(child, pathNodeIds, childrenMap, statusModelMap))
                    .collect(Collectors.toList()));
        }
        return respVO;
    }

    /**
     * 批量补齐命中需求向上的祖先链，避免树查询逐条回库查父节点。
     */
    private Map<Long, ProductRequirementDO> buildRequirementCacheWithAncestors(List<ProductRequirementDO> matchedRequirements) {
        Map<Long, ProductRequirementDO> cache = matchedRequirements.stream()
                .collect(Collectors.toMap(ProductRequirementDO::getId, Function.identity(), (left, right) -> left, HashMap::new));

        Set<Long> pendingParentIds = matchedRequirements.stream()
                .map(ProductRequirementDO::getParentId)
                .filter(parentId -> parentId != null && parentId != 0L && !cache.containsKey(parentId))
                .collect(Collectors.toSet());

        while (!pendingParentIds.isEmpty()) {
            List<ProductRequirementDO> parentRequirements = requirementMapper.selectBatchIds(pendingParentIds);
            if (parentRequirements.isEmpty()) {
                break;
            }
            Set<Long> nextPendingParentIds = new HashSet<>();
            for (ProductRequirementDO parentRequirement : parentRequirements) {
                cache.putIfAbsent(parentRequirement.getId(), parentRequirement);
                Long parentId = parentRequirement.getParentId();
                if (parentId != null && parentId != 0L && !cache.containsKey(parentId)) {
                    nextPendingParentIds.add(parentId);
                }
            }
            pendingParentIds = nextPendingParentIds;
        }
        return cache;
    }

    /**
     * 批量加载路径节点的直属子需求，后续仅在内存中组装树结构。
     */
    private Map<Long, List<ProductRequirementDO>> buildPathChildrenMap(Set<Long> pathNodeIds) {
        if (pathNodeIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<ProductRequirementDO> pathChildren = requirementMapper.selectList(
                new LambdaQueryWrapperX<ProductRequirementDO>()
                        .in(ProductRequirementDO::getParentId, pathNodeIds)
                        .orderByAsc(ProductRequirementDO::getSort)
                        .orderByDesc(ProductRequirementDO::getCreateTime));
        Map<Long, List<ProductRequirementDO>> childrenMap = new HashMap<>();
        for (ProductRequirementDO child : pathChildren) {
            childrenMap.computeIfAbsent(child.getParentId(), key -> new ArrayList<>()).add(child);
        }
        return childrenMap;
    }

    /**
     * 一次性加载当前对象类型下的状态模型，供列表和树查询复用。
     */
    private Map<String, ObjectStatusModelDO> getStatusModelMap() {
        return statusModelMapper.selectListByObjectType(REQUIREMENT_OBJECT_TYPE).stream()
                .collect(Collectors.toMap(ObjectStatusModelDO::getStatusCode, Function.identity(),
                        (left, right) -> left, LinkedHashMap::new));
    }

    /**
     * 判断指定模块是否为"全部需求"模块（parentId = 0L 的根模块）
     */
    @VisibleForTesting
    boolean isAllRequirementsModule(Long moduleId) {
        if (moduleId == null) {
            return false;
        }
        ProductRequirementModuleDO module = moduleMapper.selectById(moduleId);
        return module != null && module.getParentId() != null && module.getParentId() == 0L;
    }

    /**
     * 递归获取模块及其所有子模块的ID列表
     *
     * @param moduleId  起始模块ID
     * @param productId 产品ID
     * @return 包含自身及所有子模块的ID列表
     */
    @VisibleForTesting
    List<Long> getAllModuleIdsWithChildren(Long moduleId, Long productId) {
        List<Long> moduleIds = new ArrayList<>();
        moduleIds.add(moduleId);
        // 查询该产品下所有模块
        List<ProductRequirementModuleDO> allModules = moduleMapper.selectListByProductId(productId);
        // 递归查找子模块
        collectChildModuleIds(moduleId, allModules, moduleIds);
        return moduleIds;
    }

    /**
     * 递归收集子模块ID
     */
    private void collectChildModuleIds(Long parentId, List<ProductRequirementModuleDO> allModules, List<Long> result) {
        for (ProductRequirementModuleDO module : allModules) {
            if (Objects.equals(module.getParentId(), parentId)) {
                result.add(module.getId());
                collectChildModuleIds(module.getId(), allModules, result);
            }
        }
    }

    /**
     * 递归获取需求及其所有子需求（包含子子需求）
     *
     * @param requirementId 起始需求ID
     * @return 包含自身及所有后代需求的列表
     */
    @VisibleForTesting
    List<ProductRequirementDO> getAllRequirementsWithChildren(Long requirementId) {
        List<ProductRequirementDO> allRequirements = new ArrayList<>();
        ProductRequirementDO requirement = validateRequirementExists(requirementId);
        allRequirements.add(requirement);
        collectChildRequirements(requirementId, allRequirements);
        return allRequirements;
    }

    /**
     * 递归收集子需求
     */
    private void collectChildRequirements(Long parentId, List<ProductRequirementDO> result) {
        List<ProductRequirementDO> children = requirementMapper.selectListByParentId(parentId);
        for (ProductRequirementDO child : children) {
            result.add(child);
            collectChildRequirements(child.getId(), result);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
            permission = PRODUCT_STATUS_PERMISSION)
    public void changeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
        doChangeRequirementStatus(reqVO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
            permission = PRODUCT_REVIEW_PERMISSION)
    public void changeRequirementStatusForReview(ProductRequirementStatusActionReqVO reqVO) {
        validateReviewStatusAction(reqVO.getActionCode());
        doChangeRequirementStatus(reqVO);
    }

    private void doChangeRequirementStatus(ProductRequirementStatusActionReqVO reqVO) {
        ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
        String actionCode = reqVO.getActionCode().trim();
        Long implementProjectId = reqVO.getImplementProjectId();
        String fromStatus = requirement.getStatusCode();
        ProductRequirementDO before = cloneRequirement(requirement);

        // 校验状态流转是否合法
        ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
        validateReviewRejectedActionAllowed(requirement, actionCode);
        String reason = normalizeNullableText(reqVO.getReason());
        // 校验是否需要填写原因
        validateTransitionReason(transition, reason);
        //下一状态
        String toStatus = transition.getToStatusCode();

        // accept和close动作时，校验所有子需求（包括子子需求）是否处于允许状态
        if (ACTION_ACCEPT.equals(actionCode) || ACTION_CLOSE.equals(actionCode)) {
            validateAllChildrenAllowCloseOrAccept(reqVO.getId());
        }
        // cancel动作时，只要存在子需求就按父需求规则校验
        if (ACTION_CANCEL.equals(actionCode) && hasChildren(requirement.getId())) {
            validateParentCancelAllowed(reqVO.getId());
        }
        // close动作时，递归关闭所有已验收的子需求（包括子子需求）
        if (ACTION_CLOSE.equals(actionCode)) {
            closeAllAcceptedChildren(reqVO.getId(), reason);
        }
        // dispatch动作且选择了关联项目时，校验负责人是否在产品团队中，然后自动创建对应的项目需求
        if (ACTION_DISPATCH.equals(actionCode) && implementProjectId != null) {
            // 校验负责人是否为所属产品的团队成员
            if (requirement.getCurrentHandlerUserId() != null) {
                List<UserObjectRoleDO> userObjectRoleDOS = userObjectRoleMapper.selectActiveListByObjectAndUserId(
                        ProductObjectConstants.OBJECT_TYPE, requirement.getProductId(), requirement.getCurrentHandlerUserId());
                if (userObjectRoleDOS.isEmpty()) {
                    throw exception(ErrorCodeConstants.REQUIREMENT_HANDLER_NOT_PRODUCT_MEMBER);
                }
            }
            createProjectRequirementFromProduct(requirement, implementProjectId);
        }
        // 带并发控制的状态更新（支持同时更新关联项目ID）
        int updateCount = requirementMapper.updateStatusByIdAndStatusWithProject(requirement.getId(), fromStatus, toStatus, reason, implementProjectId);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
        }
        requirement.setStatusCode(toStatus);
        requirement.setLastStatusReason(reason);
        if (implementProjectId != null) {
            requirement.setImplementProjectId(implementProjectId);
        }

        // 写入状态变更日志
        writeRequirementStatusLog(requirement, actionCode, fromStatus, toStatus, reason);
        // 写入业务审计日志
        writeBizAuditLog(requirement, actionCode, fromStatus, toStatus,
                buildRequirementFieldChanges(before, requirement), reason);
        refreshAncestorStatusRecursively(requirement.getId());
    }

    private void validateReviewStatusAction(String actionCode) {
        String normalizedActionCode = actionCode == null ? null : actionCode.trim();
        if (!ACTION_PASS_REVIEW.equals(normalizedActionCode) && !ACTION_REJECT_REVIEW.equals(normalizedActionCode)) {
            throw invalidParamException("评审权限只能触发评审通过或评审不通过动作");
        }
    }

    /**
     * 校验需求的所有子需求（包括子子需求）是否处于允许关闭或验收的状态
     */
    @VisibleForTesting
    void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
        List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
        Set<String> terminalStatusCodes = getTerminalStatusCodes();
        // 排除自身，只校验子需求
        for (ProductRequirementDO req : allChildren) {
            if (!Objects.equals(req.getId(), requirementId)) {
                if (!STATUS_ACCEPTED.equals(req.getStatusCode()) && !terminalStatusCodes.contains(req.getStatusCode())) {
                    throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
                }
            }
        }
    }

    /**
     * 校验父需求取消时，所有子需求（包括子子需求）是否处于允许取消的状态
     * 只有子需求全部为已拒绝或已取消时，父需求才允许取消
     */
    @VisibleForTesting
    void validateParentCancelAllowed(Long requirementId) {
        List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
        // 排除自身，只校验子需求
        for (ProductRequirementDO req : allChildren) {
            if (!Objects.equals(req.getId(), requirementId)) {
                if (!CHILD_ALLOW_CANCEL_STATUSES.contains(req.getStatusCode())) {
                    throw exception(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CANCEL);
                }
            }
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
            permission = PRODUCT_DELETE_PERMISSION)
    public void deleteRequirement(Long id, Long productId) {
        ProductRequirementDO requirement = validateRequirementExists(id);
        String fromStatus = requirement.getStatusCode();

        // 校验是否存在子需求
        List<ProductRequirementDO> children = requirementMapper.selectListByParentId(id);
        if (!children.isEmpty()) {
            throw exception(ErrorCodeConstants.REQUIREMENT_HAS_CHILDREN);
        }

        // 校验状态是否允许删除（只有待认领、待评审、待指派状态才能删除）
        if (!ALLOW_DELETE_STATUSES.contains(fromStatus)) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_DELETE);
        }

        // 带并发控制的删除（以当前状态作为条件）
        int deleteCount = requirementMapper.deleteByIdAndStatus(id, fromStatus);
        if (deleteCount != 1) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
        }

        writeBizAuditLog(requirement, ACTION_DELETE, fromStatus, null,
                buildRequirementFieldChanges(requirement, null), null);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
            permission = PRODUCT_SPLIT_PERMISSION)
    public Long splitRequirement(ProductRequirementSplitReqVO reqVO) {
        // 校验父需求是否存在
        ProductRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId());
        // 产品需求一旦已指派生成项目需求，就只能到项目需求侧继续拆分
        validateRequirementNotDispatched(parentRequirement);
        // 校验父需求状态是否允许拆分（只能是待指派、已评审或实施中）
        validateParentAllowSplit(parentRequirement);
        AttachmentValidator.validate(reqVO.getAttachments());
        attachmentFileIdResolver.resolve(reqVO.getAttachments());

        // 创建子需求
        ProductRequirementDO childRequirement = new ProductRequirementDO();
        childRequirement.setParentId(reqVO.getParentId());
        childRequirement.setProductId(parentRequirement.getProductId()); // 子需求继承父需求的产品
        childRequirement.setModuleId(parentRequirement.getModuleId()); // 子需求继承父需求的模块
        childRequirement.setReviewRequired(reqVO.getReviewRequired()); // 子需求默认不需要评审
        childRequirement.setTitle(reqVO.getTitle().trim());
        childRequirement.setDescription(normalizeNullableText(reqVO.getDescription()));
        childRequirement.setCategory(reqVO.getCategory());
        childRequirement.setSourceType(parentRequirement.getSourceType()); // 继承父需求来源类型
        childRequirement.setSourceBizCode(parentRequirement.getSourceBizCode()); // 继承父需求来源业务编号
        childRequirement.setPriority(reqVO.getPriority());
        // 子需求初始状态为待指派
        // 根据是否需要评审确定初始状态
        String initialStatus = Objects.equals(reqVO.getReviewRequired(), 1)
                ? STATUS_PENDING_REVIEW : STATUS_PENDING_DISPATCH;
        childRequirement.setStatusCode(initialStatus);
        childRequirement.setProposerId(reqVO.getProposerId());
        childRequirement.setProposerNickname(normalizeNullableText(reqVO.getProposerNickname()));
        childRequirement.setExpectedTime(reqVO.getExpectedTime());
        childRequirement.setCurrentHandlerUserId(reqVO.getCurrentHandlerUserId());
        childRequirement.setCurrentHandlerUserNickname(normalizeNullableText(reqVO.getCurrentHandlerUserNickname()));
        childRequirement.setImplementProjectId(reqVO.getImplementProjectId());
        childRequirement.setSort(reqVO.getSort());
        childRequirement.setAttachments(reqVO.getAttachments());
        requirementMapper.insert(childRequirement);

        // 父需求等待执行指派动作时，一旦拆分子需求就进入实施中。
        if (isPendingDispatchActionStatus(parentRequirement.getStatusCode())) {
            ProductRequirementDO before = cloneRequirement(parentRequirement);
            String fromStatus = parentRequirement.getStatusCode();
            int updateCount = requirementMapper.updateStatusByIdAndStatus(
                    parentRequirement.getId(), fromStatus, STATUS_IMPLEMENTING, null);
            if (updateCount == 1) {
                parentRequirement.setStatusCode(STATUS_IMPLEMENTING);
                writeRequirementStatusLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null);
                writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING,
                        buildRequirementFieldChanges(before, parentRequirement), null);
            }
        }

        // 写入子需求的业务审计日志
        writeBizAuditLog(childRequirement, ACTION_CREATE, null, initialStatus,
                buildRequirementFieldChanges(null, childRequirement), null);
        return childRequirement.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
            permission = PRODUCT_STATUS_PERMISSION)
    public void closeRequirement(ProductRequirementCloseReqVO reqVO) {
        ProductRequirementDO requirement = validateRequirementExists(reqVO.getId());
        String fromStatus = requirement.getStatusCode();
        String reason = reqVO.getReason().trim();
        ProductRequirementDO before = cloneRequirement(requirement);

        // 校验当前状态是否为已验收（只有已验收才能关闭）
        if (!STATUS_ACCEPTED.equals(fromStatus)) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_CLOSE);
        }

        // 校验所有子需求（包括子子需求）是否允许关闭
        validateAllChildrenAllowCloseOrAccept(reqVO.getId());

        // 递归关闭所有已验收的子需求（包括子子需求）
        closeAllAcceptedChildren(reqVO.getId(), reason);

        // 关闭当前需求
        int updateCount = requirementMapper.updateStatusByIdAndStatus(
                requirement.getId(), fromStatus, STATUS_CLOSED, reason);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
        }
        requirement.setStatusCode(STATUS_CLOSED);
        requirement.setLastStatusReason(reason);

        writeRequirementStatusLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED, reason);
        writeBizAuditLog(requirement, ACTION_CLOSE, fromStatus, STATUS_CLOSED,
                buildRequirementFieldChanges(before, requirement), reason);
        refreshAncestorStatusRecursively(requirement.getId());
    }

    /**
     * 递归关闭所有已验收的子需求（包括子子需求）
     */
    private void closeAllAcceptedChildren(Long parentId, String reason) {
        List<ProductRequirementDO> children = requirementMapper.selectListByParentId(parentId);
        for (ProductRequirementDO child : children) {
            // 递归处理子需求的子需求
            closeAllAcceptedChildren(child.getId(), reason);
            // 如果子需求已验收，则关闭
            if (STATUS_ACCEPTED.equals(child.getStatusCode())) {
                ProductRequirementDO before = cloneRequirement(child);
                int updateCount = requirementMapper.updateStatusByIdAndStatus(
                        child.getId(), STATUS_ACCEPTED, STATUS_CLOSED, reason);
                if (updateCount == 1) {
                    child.setStatusCode(STATUS_CLOSED);
                    child.setLastStatusReason(reason);
                    writeRequirementStatusLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED, reason);
                    writeBizAuditLog(child, ACTION_CLOSE, STATUS_ACCEPTED, STATUS_CLOSED,
                            buildRequirementFieldChanges(before, child), reason);
                }
            }
        }
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", accessible = true)
    public List<ProductRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long productId) {
        ProductRequirementDO requirement = validateRequirementExists(requirementId);
        // 当产品需求已指派并生成项目需求后，产品需求端不再返回动作按钮
        if (hasDispatchedProjectRequirement(requirement)) {
            return Collections.emptyList();
        }

        String currentStatus = requirement.getStatusCode();

        // 查询当前状态允许的所有流转
        List<ObjectStatusTransitionDO> transitions = statusTransitionMapper
                .selectListByObjectTypeAndFromStatus(REQUIREMENT_OBJECT_TYPE, currentStatus);

        return transitions.stream()
                // 取消动作不满足前置条件时，不再返回给前端展示按钮
                .filter(transition -> shouldExposeTransition(requirement, transition))
                .map(transition -> {
                    ProductRequirementStatusTransitionRespVO vo = new ProductRequirementStatusTransitionRespVO();
                    vo.setActionCode(transition.getActionCode());
                    vo.setActionName(transition.getActionName());
                    vo.setToStatusCode(transition.getToStatusCode());
                    // 查询目标状态名称
                    ObjectStatusModelDO statusModel = statusModelMapper
                            .selectByObjectTypeAndStatusCode(REQUIREMENT_OBJECT_TYPE, transition.getToStatusCode());
                    vo.setToStatusName(statusModel != null ? statusModel.getStatusName() : transition.getToStatusCode());
                    vo.setNeedReason(transition.getNeedReason());
                    return vo;
                }).collect(Collectors.toList());
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId", accessible = true)
    public List<ProductRequirementAllowedTransitionBatchRespVO> getAllowedTransitionsBatch(ProductRequirementBatchReqVO reqVO) {
        List<ProductRequirementDO> requirements = getBatchRequirements(reqVO.getRequirementIds(), reqVO.getProductId());
        Set<Long> dispatchedRequirementIds = getDispatchedProductRequirementIds(requirements);
        Map<String, List<ObjectStatusTransitionDO>> transitionsByStatus = getTransitionsByStatus(requirements);
        Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();

        return requirements.stream().map(requirement -> {
            ProductRequirementAllowedTransitionBatchRespVO respVO = new ProductRequirementAllowedTransitionBatchRespVO();
            respVO.setRequirementId(requirement.getId());
            if (dispatchedRequirementIds.contains(requirement.getId())) {
                respVO.setTransitions(Collections.emptyList());
            } else {
                respVO.setTransitions(buildAllowedTransitions(requirement,
                        transitionsByStatus.getOrDefault(requirement.getStatusCode(), Collections.emptyList()), statusModelMap));
            }
            return respVO;
        }).collect(Collectors.toList());
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", accessible = true)
    public boolean hasDispatchedProjectRequirement(Long requirementId, Long productId) {
        ProductRequirementDO requirement = validateRequirementExists(requirementId);
        return hasDispatchedProjectRequirement(requirement);
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId", accessible = true)
    public List<ProductRequirementHasDispatchedBatchRespVO> hasDispatchedProjectRequirementBatch(ProductRequirementBatchReqVO reqVO) {
        List<ProductRequirementDO> requirements = getBatchRequirements(reqVO.getRequirementIds(), reqVO.getProductId());
        Set<Long> dispatchedRequirementIds = getDispatchedProductRequirementIds(requirements);
        return requirements.stream().map(requirement -> {
            ProductRequirementHasDispatchedBatchRespVO respVO = new ProductRequirementHasDispatchedBatchRespVO();
            respVO.setRequirementId(requirement.getId());
            respVO.setHasDispatched(dispatchedRequirementIds.contains(requirement.getId()));
            return respVO;
        }).collect(Collectors.toList());
    }


    @Override
    public ProductRequirementDispatchedProjectLinkRespVO getDispatchedProjectLink(Long productRequirementId) {
        // 校验产品需求是否存在，以及是否已指派到具体的关联项目
        ProductRequirementDO requirement = validateRequirementExists(productRequirementId);
        if (requirement.getImplementProjectId() == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_NOT_DISPATCHED);
        }
        Long projectId = requirement.getImplementProjectId();

        // 查询产品需求指派后生成的顶级项目需求
        List<ProjectRequirementDO> projectRequirements = projectRequirementMapper.selectList(
                new LambdaQueryWrapperX<ProjectRequirementDO>()
                        .eq(ProjectRequirementDO::getProductRequirementId, productRequirementId)
                        .eq(ProjectRequirementDO::getParentId, 0L)
        );
        if (projectRequirements.isEmpty()) {
            throw exception(ErrorCodeConstants.REQUIREMENT_DISPATCHED_PROJECT_REQUIREMENT_NOT_FOUND);
        }

        // 校验当前登录用户是否为该项目的成员
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (userObjectRoleMapper.selectActiveByObjectAndUserId(
                ProjectObjectConstants.OBJECT_TYPE, projectId, loginUserId) == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_NOT_PROJECT_MEMBER);
        }

        ProductRequirementDispatchedProjectLinkRespVO respVO = new ProductRequirementDispatchedProjectLinkRespVO();
        respVO.setProjectRequirementId(projectRequirements.get(0).getId());
        respVO.setProjectId(projectId);
        return respVO;
    }

    /**
     * 判断产品需求是否已成功分流并生成对应的项目需求
     */
    public boolean hasDispatchedProjectRequirement(ProductRequirementDO requirement) {
        if (requirement.getImplementProjectId() == null) {
            return false;
        }
        List<ProjectRequirementDO> projectRequirements = projectRequirementMapper.selectList(
                new LambdaQueryWrapperX<ProjectRequirementDO>()
                        .eq(ProjectRequirementDO::getSourceType, "product_requirement")
                        .eq(ProjectRequirementDO::getProductRequirementId, requirement.getId())
        );
        return !projectRequirements.isEmpty();
    }

    private List<ProductRequirementDO> getBatchRequirements(List<Long> requirementIds, Long productId) {
        if (requirementIds == null || requirementIds.isEmpty()) {
            return Collections.emptyList();
        }
        if (requirementIds.stream().anyMatch(Objects::isNull)) {
            throw invalidParamException("需求编号不能为空");
        }
        List<Long> distinctIds = requirementIds.stream().distinct().collect(Collectors.toList());
        Map<Long, ProductRequirementDO> requirementMap = requirementMapper.selectByIds(distinctIds).stream()
                .collect(Collectors.toMap(ProductRequirementDO::getId, Function.identity()));
        if (requirementMap.size() != distinctIds.size()) {
            throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS);
        }
        List<ProductRequirementDO> requirements = distinctIds.stream()
                .map(requirementMap::get)
                .collect(Collectors.toList());
        for (ProductRequirementDO requirement : requirements) {
            if (!Objects.equals(requirement.getProductId(), productId)) {
                throw invalidParamException("需求不属于当前产品");
            }
        }
        return requirements;
    }

    private Set<Long> getDispatchedProductRequirementIds(List<ProductRequirementDO> requirements) {
        List<Long> candidateIds = requirements.stream()
                .filter(requirement -> requirement.getImplementProjectId() != null)
                .map(ProductRequirementDO::getId)
                .collect(Collectors.toList());
        if (candidateIds.isEmpty()) {
            return Collections.emptySet();
        }
        return projectRequirementMapper.selectList(new LambdaQueryWrapperX<ProjectRequirementDO>()
                        .eq(ProjectRequirementDO::getSourceType, "product_requirement")
                        .in(ProjectRequirementDO::getProductRequirementId, candidateIds))
                .stream()
                .map(ProjectRequirementDO::getProductRequirementId)
                .collect(Collectors.toSet());
    }

    private Map<String, List<ObjectStatusTransitionDO>> getTransitionsByStatus(List<ProductRequirementDO> requirements) {
        List<String> statusCodes = requirements.stream()
                .map(ProductRequirementDO::getStatusCode)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (statusCodes.isEmpty()) {
            return Collections.emptyMap();
        }
        return statusTransitionMapper.selectListByObjectTypeAndFromStatuses(REQUIREMENT_OBJECT_TYPE, statusCodes)
                .stream()
                .collect(Collectors.groupingBy(ObjectStatusTransitionDO::getFromStatusCode));
    }

    private List<ProductRequirementStatusTransitionRespVO> buildAllowedTransitions(
            ProductRequirementDO requirement,
            List<ObjectStatusTransitionDO> transitions,
            Map<String, ObjectStatusModelDO> statusModelMap) {
        return transitions.stream()
                .filter(transition -> shouldExposeTransition(requirement, transition))
                .map(transition -> {
                    ProductRequirementStatusTransitionRespVO vo = new ProductRequirementStatusTransitionRespVO();
                    vo.setActionCode(transition.getActionCode());
                    vo.setActionName(transition.getActionName());
                    vo.setToStatusCode(transition.getToStatusCode());
                    ObjectStatusModelDO statusModel = statusModelMap.get(transition.getToStatusCode());
                    vo.setToStatusName(statusModel != null ? statusModel.getStatusName() : transition.getToStatusCode());
                    vo.setNeedReason(transition.getNeedReason());
                    return vo;
                }).collect(Collectors.toList());
    }

    /**
     * 已分流并生成项目需求后，产品需求端不再允许继续拆分。
     */
    @VisibleForTesting
    void validateRequirementNotDispatched(ProductRequirementDO requirement) {
        if (hasDispatchedProjectRequirement(requirement)) {
            throw exception(ErrorCodeConstants.REQUIREMENT_DISPATCHED_NOT_ALLOW_SPLIT);
        }
    }

    /**
     * 当前只对取消动作做额外过滤，避免前端展示点了也会报错的按钮。
     */
    private boolean shouldExposeTransition(ProductRequirementDO requirement, ObjectStatusTransitionDO transition) {
        if (!isReviewRejectedActionAllowed(requirement, transition.getActionCode())) {
            return false;
        }
        if (!ACTION_CANCEL.equals(transition.getActionCode())) {
            return true;
        }
        if (!hasChildren(requirement.getId())) {
            return true;
        }
        return isParentCancelAllowed(requirement.getId());
    }

    /**
     * 评审不通过后的动作由来源决定：手工新增只能取消，工单流转只能拒绝。
     */
    private void validateReviewRejectedActionAllowed(ProductRequirementDO requirement, String actionCode) {
        if (!isReviewRejectedActionAllowed(requirement, actionCode)) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
                    statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode()),
                    statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
        }
    }

    private boolean isReviewRejectedActionAllowed(ProductRequirementDO requirement, String actionCode) {
        if (!STATUS_REVIEW_REJECTED.equals(requirement.getStatusCode())) {
            return true;
        }
        if (SOURCE_TYPE_MANUAL.equals(requirement.getSourceType())) {
            return ACTION_CANCEL.equals(actionCode);
        }
        if (SOURCE_TYPE_WORK_ORDER.equals(requirement.getSourceType())) {
            return ACTION_REJECT.equals(actionCode);
        }
        return true;
    }

    /**
     * 父需求存在子需求时，只有全部子需求都已取消或已拒绝，才允许展示取消动作。
     */
    private boolean isParentCancelAllowed(Long requirementId) {
        List<ProductRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
        for (ProductRequirementDO req : allChildren) {
            if (!Objects.equals(req.getId(), requirementId)
                    && !CHILD_ALLOW_CANCEL_STATUSES.contains(req.getStatusCode())) {
                return false;
            }
        }
        return true;
    }

    /**
     * 只要存在直接子需求，就应按父需求的取消规则处理。
     */
    private boolean hasChildren(Long requirementId) {
        return !requirementMapper.selectListByParentId(requirementId).isEmpty();
    }

    /**
     * 子需求状态变化后，仅向上自动推导产品需求父节点状态，不涉及跨对象回写。
     */
    private void refreshAncestorStatusRecursively(Long requirementId) {
        ProductRequirementDO currentRequirement = requirementMapper.selectById(requirementId);
        if (currentRequirement == null || currentRequirement.getParentId() == null
                || Objects.equals(currentRequirement.getParentId(), 0L)) {
            return;
        }
        ProductRequirementDO parentRequirement = requirementMapper.selectById(currentRequirement.getParentId());
        if (parentRequirement == null) {
            return;
        }

        String targetStatus = deriveParentStatusByDirectChildren(parentRequirement.getId());
        if (!StringUtils.hasText(targetStatus) || Objects.equals(targetStatus, parentRequirement.getStatusCode())) {
            return;
        }

        ProductRequirementDO before = cloneRequirement(parentRequirement);
        int updateCount = requirementMapper.updateStatusByIdAndStatus(parentRequirement.getId(),
                parentRequirement.getStatusCode(), targetStatus, AUTO_DERIVE_REASON);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
        }
        parentRequirement.setStatusCode(targetStatus);
        parentRequirement.setLastStatusReason(AUTO_DERIVE_REASON);

        writeRequirementStatusLog(parentRequirement, ACTION_AUTO_DERIVE, before.getStatusCode(),
                targetStatus, AUTO_DERIVE_REASON);
        writeBizAuditLog(parentRequirement, ACTION_AUTO_DERIVE, before.getStatusCode(),
                targetStatus, buildRequirementFieldChanges(before, parentRequirement), AUTO_DERIVE_REASON);

        refreshAncestorStatusRecursively(parentRequirement.getId());
    }

    /**
     * 父需求状态只在约定好的几种场景下自动变化，其他组合保持当前状态不变：
     * accepted 全量汇总为 accepted，closed 全量汇总为 closed，
     * cancelled 全量汇总为 cancelled，accepted/closed 混合且至少一个 accepted 时汇总为 accepted。
     */
    private String deriveParentStatusByDirectChildren(Long parentRequirementId) {
        List<ProductRequirementDO> children = requirementMapper.selectListByParentId(parentRequirementId);
        if (children.isEmpty()) {
            return null;
        }

        boolean allAccepted = children.stream()
                .allMatch(child -> STATUS_ACCEPTED.equals(child.getStatusCode()));
        if (allAccepted) {
            return STATUS_ACCEPTED;
        }

        boolean allClosed = children.stream()
                .allMatch(child -> STATUS_CLOSED.equals(child.getStatusCode()));
        if (allClosed) {
            return STATUS_CLOSED;
        }

        boolean allCancelled = children.stream()
                .allMatch(child -> STATUS_CANCELLED.equals(child.getStatusCode()));
        if (allCancelled) {
            return STATUS_CANCELLED;
        }

        boolean allAcceptedOrClosed = children.stream().allMatch(child ->
                STATUS_ACCEPTED.equals(child.getStatusCode()) || STATUS_CLOSED.equals(child.getStatusCode()));
        boolean hasAccepted = children.stream()
                .anyMatch(child -> STATUS_ACCEPTED.equals(child.getStatusCode()));
        if (allAcceptedOrClosed && hasAccepted) {
            return STATUS_ACCEPTED;
        }
        return null;
    }

    // ========== 模块管理 ==========
    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
            permission = PRODUCT_CREATE_PERMISSION)
    public Long createRequirementModule(ProductRequirementModuleReqVO reqVO) {
        // 校验模块名称在同一产品下是否唯一
        validateModuleNameUnique(reqVO.getProductId(), null, reqVO.getModuleName());

        ProductRequirementModuleDO module = new ProductRequirementModuleDO();
        module.setParentId(reqVO.getParentId() != null ? reqVO.getParentId() : 0L);
        module.setProductId(reqVO.getProductId());
        module.setModuleName(reqVO.getModuleName().trim());
        module.setRemark(normalizeNullableText(reqVO.getRemark()));
        module.setIcon(normalizeNullableText(reqVO.getIcon()));
        module.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
        moduleMapper.insert(module);
        return module.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.productId",
            permission = PRODUCT_UPDATE_PERMISSION)
    public void updateRequirementModule(ProductRequirementModuleReqVO reqVO) {
        if (reqVO.getId() == null) {
            throw invalidParamException("模块编号不能为空");
        }
        ProductRequirementModuleDO module = validateModuleExists(reqVO.getId());
        // 校验模块名称唯一性
        validateModuleNameUnique(reqVO.getProductId(), reqVO.getId(), reqVO.getModuleName());

        module.setModuleName(reqVO.getModuleName().trim());
        module.setRemark(normalizeNullableText(reqVO.getRemark()));
        module.setIcon(normalizeNullableText(reqVO.getIcon()));
        module.setSort(reqVO.getSort() != null ? reqVO.getSort() : 0);
        moduleMapper.updateById(module);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
            permission = PRODUCT_DELETE_PERMISSION)
    public void deleteRequirementModule(Long moduleId, Long productId) {
        validateModuleExists(moduleId);

        // 校验是否存在子模块
        List<ProductRequirementModuleDO> childModules = moduleMapper.selectListByParentId(moduleId);
        if (!childModules.isEmpty()) {
            throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_CHILDREN);
        }

        // 校验模块下是否存在需求
        List<ProductRequirementDO> requirements = requirementMapper.selectListByModuleId(moduleId);
        if (!requirements.isEmpty()) {
            throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_REQUIREMENTS);
        }

        // 删除模块
        moduleMapper.deleteById(moduleId);
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId", accessible = true)
    public List<ProductRequirementModuleRespVO> getRequirementModuleTree(Long productId) {
        List<ProductRequirementModuleDO> modules = moduleMapper.selectListByProductId(productId);
        return buildModuleTree(modules, 0L);
    }

    @Override
    public List<ProductRequirementStatusDictRespVO> getRequirementStatusDict() {
        List<ObjectStatusModelDO> statusModels = statusModelMapper.selectListByObjectType(REQUIREMENT_OBJECT_TYPE);
        return statusModels.stream()
                .map(this::buildStatusDictRespVO)
                .collect(Collectors.toList());
    }

    @Override
    public List<ProductRequirementStatusDictRespVO> getRequirementTerminalStatusDict() {
        List<ObjectStatusModelDO> statusModels = statusModelMapper.selectListByObjectType(REQUIREMENT_OBJECT_TYPE);
        return statusModels.stream()
                .filter(ObjectStatusModelDO::getTerminalFlag)
                .map(this::buildStatusDictRespVO)
                .collect(Collectors.toList());
    }

    /**
     * 构建状态字典响应VO
     */
    private ProductRequirementStatusDictRespVO buildStatusDictRespVO(ObjectStatusModelDO statusModel) {
        ProductRequirementStatusDictRespVO respVO = new ProductRequirementStatusDictRespVO();
        respVO.setStatusCode(statusModel.getStatusCode());
        respVO.setStatusName(statusModel.getStatusName());
        respVO.setSort(statusModel.getSort());
        respVO.setInitialFlag(statusModel.getInitialFlag());
        respVO.setTerminalFlag(statusModel.getTerminalFlag());
        respVO.setAllowEdit(statusModel.getAllowEdit());
        return respVO;
    }

    // ========== 私有辅助方法 ==========

    /**
     * 构建需求响应VO（不含子需求）
     */
    private ProductRequirementRespVO buildRequirementRespVO(ProductRequirementDO requirement) {
        return buildRequirementRespVO(requirement, getStatusModelMap());
    }

    /**
     * 复用已加载的状态模型构建需求响应，避免列表场景重复查状态字典。
     */
    private ProductRequirementRespVO buildRequirementRespVO(ProductRequirementDO requirement,
                                                            Map<String, ObjectStatusModelDO> statusModelMap) {
        ProductRequirementRespVO respVO = BeanUtils.toBean(requirement, ProductRequirementRespVO.class);
        ObjectStatusModelDO statusModel = statusModelMap.get(requirement.getStatusCode());
        if (statusModel != null) {
            respVO.setStatusName(statusModel.getStatusName());
        }
        return respVO;
    }

    /**
     * 构建模块树
     */
    private List<ProductRequirementModuleRespVO> buildModuleTree(List<ProductRequirementModuleDO> modules, Long parentId) {
        return modules.stream()
                .filter(m -> Objects.equals(m.getParentId(), parentId))
                .map(m -> {
                    ProductRequirementModuleRespVO vo = BeanUtils.toBean(m, ProductRequirementModuleRespVO.class);
                    vo.setChildren(buildModuleTree(modules, m.getId()));
                    return vo;
                })
                .collect(Collectors.toList());
    }

    /**
     * 校验需求是否存在
     */
    @VisibleForTesting
    ProductRequirementDO validateRequirementExists(Long id) {
        if (id == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS);
        }
        ProductRequirementDO requirement = requirementMapper.selectById(id);
        if (requirement == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_NOT_EXISTS);
        }
        return requirement;
    }

    /**
     * 校验需求是否允许编辑，编辑能力以状态模型 allow_edit 为准。
     */
    @VisibleForTesting
    void validateRequirementEditable(ProductRequirementDO requirement) {
        ObjectStatusModelDO statusModel = statusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
                REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode());
        if (statusModel == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
        }
    }

    private Set<String> getTerminalStatusCodes() {
        return new HashSet<>(statusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(REQUIREMENT_OBJECT_TYPE));
    }

    /**
     * 校验父需求是否允许拆分
     */
    @VisibleForTesting
    void validateParentAllowSplit(ProductRequirementDO parentRequirement) {
        String status = parentRequirement.getStatusCode();
        if (!STATUS_PENDING_DISPATCH.equals(status) && !STATUS_IMPLEMENTING.equals(status)
                && !STATUS_REVIEWED.equals(status)) {
            throw exception(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT);
        }
    }

    /**
     * 校验状态流转是否合法
     */
    @VisibleForTesting
    ObjectStatusTransitionDO validateRequirementTransition(String fromStatusCode, String actionCode) {
        ObjectStatusTransitionDO transition = statusTransitionMapper
                .selectByObjectTypeAndFromStatusAndAction(REQUIREMENT_OBJECT_TYPE, fromStatusCode, actionCode);
        if (transition == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED,
                    statusActionTextResolver.statusName(REQUIREMENT_OBJECT_TYPE, fromStatusCode),
                    statusActionTextResolver.actionName(REQUIREMENT_OBJECT_TYPE, actionCode));
        }
        return transition;
    }

    /**
     * 校验状态流转是否需要填写原因
     */
    @VisibleForTesting
    void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
        if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
        }
    }

    /**
     * 校验模块是否存在
     */
    @VisibleForTesting
    ProductRequirementModuleDO validateModuleExists(Long id) {
        if (id == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
        }
        ProductRequirementModuleDO module = moduleMapper.selectById(id);
        if (module == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
        }
        return module;
    }

    /**
     * 校验模块名称在同一产品下是否唯一
     */
    @VisibleForTesting
    void validateModuleNameUnique(Long productId, Long moduleId, String moduleName) {
        if (!StringUtils.hasText(moduleName)) {
            return;
        }
        ProductRequirementModuleDO existModule = moduleMapper
                .selectByProductIdAndModuleName(productId, moduleName.trim());
        if (existModule == null) {
            return;
        }
        if (!existModule.getId().equals(moduleId)) {
            throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NAME_DUPLICATE, moduleName.trim());
        }
    }

    /**
     * 解析模块ID：当未选择模块时，自动归属到该产品的"全部需求"模块
     * "全部需求"模块的标志性特征是 parentId = 0L
     */
    @VisibleForTesting
    Long resolveModuleId(Long moduleId, Long productId) {
        if (moduleId != null) {
            return moduleId;
        }
        // 查询该产品的"全部需求"模块（parentId = 0L 的根模块）
        ProductRequirementModuleDO defaultModule = moduleMapper
                .selectByProductIdAndParentId(productId, 0L);
        if (defaultModule == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
        }
        return defaultModule.getId();
    }

    /**
     * 校验模块是否属于当前产品
     */
    @VisibleForTesting
    void validateModuleBelongsToProduct(Long moduleId, Long productId) {
        if (moduleId == null) {
            return; // 全部需求模块不需要校验
        }
        ProductRequirementModuleDO module = moduleMapper.selectById(moduleId);
        if (module == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS);
        }
        if (!Objects.equals(module.getProductId(), productId)) {
            throw exception(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_BELONG_TO_PRODUCT);
        }
    }

    /**
     * 写入需求状态变更日志
     */
    private void writeRequirementStatusLog(ProductRequirementDO requirement, String actionType,
                                           String fromStatus, String toStatus, String reason) {
        ProductRequirementStatusLogDO statusLog = new ProductRequirementStatusLogDO();
        statusLog.setRequirementId(requirement.getId());
        statusLog.setActionType(actionType);
        statusLog.setFromStatus(fromStatus);
        statusLog.setToStatus(toStatus);
        statusLog.setReason(defaultText(reason));
        statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        statusLog.setRequirementTitleSnapshot(requirement.getTitle());
        statusLogMapper.insert(statusLog);
    }

    /**
     * 写入业务审计日志
     */
    private void writeBizAuditLog(ProductRequirementDO requirement, String actionType, String fromStatus,
                                  String toStatus, String fieldChanges, String reason) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(BIZ_TYPE_REQUIREMENT);
        auditLog.setBizId(requirement.getId());
        auditLog.setActionType(actionType);
        auditLog.setFromStatus(fromStatus);
        auditLog.setToStatus(toStatus);
        auditLog.setFieldChanges(fieldChanges);
        auditLog.setReason(reason);
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    /**
     * 克隆需求快照，避免更新前后的对象引用互相污染。
     */
    private ProductRequirementDO cloneRequirement(ProductRequirementDO source) {
        ProductRequirementDO target = new ProductRequirementDO();
        target.setId(source.getId());
        target.setParentId(source.getParentId());
        target.setModuleId(source.getModuleId());
        target.setProductId(source.getProductId());
        target.setReviewRequired(source.getReviewRequired());
        target.setTitle(source.getTitle());
        target.setDescription(source.getDescription());
        target.setCategory(source.getCategory());
        target.setSourceType(source.getSourceType());
        target.setSourceBizCode(source.getSourceBizCode());
        target.setPriority(source.getPriority());
        target.setStatusCode(source.getStatusCode());
        target.setLastStatusReason(source.getLastStatusReason());
        target.setProposerId(source.getProposerId());
        target.setProposerNickname(source.getProposerNickname());
        target.setExpectedTime(source.getExpectedTime());
        target.setCurrentHandlerUserId(source.getCurrentHandlerUserId());
        target.setCurrentHandlerUserNickname(source.getCurrentHandlerUserNickname());
        target.setImplementProjectId(source.getImplementProjectId());
        target.setSort(source.getSort());
        target.setAttachments(source.getAttachments());
        return target;
    }

    /**
     * 构建需求字段变更内容，供业务审计日志落库。
     */
    private String buildRequirementFieldChanges(ProductRequirementDO before, ProductRequirementDO after) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "parentId", valueOf(before, ProductRequirementDO::getParentId),
                valueOf(after, ProductRequirementDO::getParentId));
        appendFieldChange(fieldChanges, "moduleId", valueOf(before, ProductRequirementDO::getModuleId),
                valueOf(after, ProductRequirementDO::getModuleId));
        appendFieldChange(fieldChanges, "productId", valueOf(before, ProductRequirementDO::getProductId),
                valueOf(after, ProductRequirementDO::getProductId));
        appendFieldChange(fieldChanges, "reviewRequired", valueOf(before, ProductRequirementDO::getReviewRequired),
                valueOf(after, ProductRequirementDO::getReviewRequired));
        appendFieldChange(fieldChanges, "title", valueOf(before, ProductRequirementDO::getTitle),
                valueOf(after, ProductRequirementDO::getTitle));
        appendFieldChange(fieldChanges, "description", valueOf(before, ProductRequirementDO::getDescription),
                valueOf(after, ProductRequirementDO::getDescription));
        appendFieldChange(fieldChanges, "category", valueOf(before, ProductRequirementDO::getCategory),
                valueOf(after, ProductRequirementDO::getCategory));
        appendFieldChange(fieldChanges, "sourceType", valueOf(before, ProductRequirementDO::getSourceType),
                valueOf(after, ProductRequirementDO::getSourceType));
        appendFieldChange(fieldChanges, "sourceBizCode", valueOf(before, ProductRequirementDO::getSourceBizCode),
                valueOf(after, ProductRequirementDO::getSourceBizCode));
        appendFieldChange(fieldChanges, "priority", valueOf(before, ProductRequirementDO::getPriority),
                valueOf(after, ProductRequirementDO::getPriority));
        appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProductRequirementDO::getStatusCode),
                valueOf(after, ProductRequirementDO::getStatusCode));
        appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, ProductRequirementDO::getLastStatusReason),
                valueOf(after, ProductRequirementDO::getLastStatusReason));
        appendFieldChange(fieldChanges, "proposerId", valueOf(before, ProductRequirementDO::getProposerId),
                valueOf(after, ProductRequirementDO::getProposerId));
        appendFieldChange(fieldChanges, "proposerNickname", valueOf(before, ProductRequirementDO::getProposerNickname),
                valueOf(after, ProductRequirementDO::getProposerNickname));
        appendFieldChange(fieldChanges, "expectedTime", valueOf(before, ProductRequirementDO::getExpectedTime),
                valueOf(after, ProductRequirementDO::getExpectedTime));
        appendFieldChange(fieldChanges, "currentHandlerUserId", valueOf(before, ProductRequirementDO::getCurrentHandlerUserId),
                valueOf(after, ProductRequirementDO::getCurrentHandlerUserId));
        appendFieldChange(fieldChanges, "currentHandlerUserNickname",
                valueOf(before, ProductRequirementDO::getCurrentHandlerUserNickname),
                valueOf(after, ProductRequirementDO::getCurrentHandlerUserNickname));
        appendFieldChange(fieldChanges, "implementProjectId", valueOf(before, ProductRequirementDO::getImplementProjectId),
                valueOf(after, ProductRequirementDO::getImplementProjectId));
        appendFieldChange(fieldChanges, "sort", valueOf(before, ProductRequirementDO::getSort),
                valueOf(after, ProductRequirementDO::getSort));
        appendFieldChange(fieldChanges, "attachments", valueOf(before, ProductRequirementDO::getAttachments),
                valueOf(after, ProductRequirementDO::getAttachments));
        return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
    }

    private <T> T valueOf(ProductRequirementDO requirement, Function<ProductRequirementDO, T> getter) {
        return requirement == null ? null : getter.apply(requirement);
    }

    /**
     * 仅记录真实发生变化的字段，避免审计日志写入冗余内容。
     */
    private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
        if (Objects.equals(before, after)) {
            return;
        }
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("before", before);
        value.put("after", after);
        fieldChanges.put(fieldName, value);
    }

    /**
     * 处理可能为空的文本，去除首尾空格后若为空则返回null
     */
    private String normalizeNullableText(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }

    /**
     * 处理可能为空的文本，若为空则返回空字符串
     */
    private String defaultText(String value) {
        return StringUtils.hasText(value) ? value : "";
    }

    /**
     * dispatch动作且已选择关联项目时，自动将产品需求转化为项目需求
     *
     * @param productRequirement 产品需求
     * @param implementProjectId 关联项目ID
     */
    @VisibleForTesting
    void createProjectRequirementFromProduct(ProductRequirementDO productRequirement, Long implementProjectId) {
        List<ProjectRequirementDO> existingRequirements = projectRequirementMapper.selectList(
                new LambdaQueryWrapperX<ProjectRequirementDO>()
                        .eq(ProjectRequirementDO::getSourceType, "product_requirement")
                        .eq(ProjectRequirementDO::getProductRequirementId, productRequirement.getId())
        );
        if (!existingRequirements.isEmpty()) {
            return;
        }

        // 查询关联项目下的根模块（parentId = 0）
        ProjectRequirementModuleDO rootModule = projectRequirementModuleMapper.selectByProjectIdAndParentId(implementProjectId, 0L);
        if (rootModule == null) {
            throw exception(ErrorCodeConstants.REQUIREMENT_PROJECT_MODULE_ROOT_NOT_EXISTS);
        }

        // 构建项目需求记录
        ProjectRequirementDO newRequirement = new ProjectRequirementDO();
        newRequirement.setParentId(0L); // 顶级需求
        newRequirement.setProjectId(implementProjectId);
        newRequirement.setModuleId(rootModule.getId());
        newRequirement.setProductRequirementId(productRequirement.getId()); // 溯源关系
        newRequirement.setSourceType("product_requirement");
        newRequirement.setStatusCode(STATUS_IMPLEMENTING);
        newRequirement.setReviewRequired(0); //从产品需求流转到项目需求的需求肯定不需要评审
        // 拷贝产品需求的其他字段（不拷贝需求负责人id和姓名、排序、状态原因、更新人、更新时间、逻辑删除字段）
        newRequirement.setTitle(productRequirement.getTitle());
        newRequirement.setDescription(productRequirement.getDescription());
        newRequirement.setCategory(productRequirement.getCategory());
        newRequirement.setSourceBizCode(productRequirement.getSourceBizCode());
        newRequirement.setPriority(productRequirement.getPriority());
        newRequirement.setProposerId(productRequirement.getProposerId());
        newRequirement.setProposerNickname(productRequirement.getProposerNickname());
        newRequirement.setExpectedTime(productRequirement.getExpectedTime());
        newRequirement.setAttachments(productRequirement.getAttachments());
        newRequirement.setCreator(productRequirement.getCreator());
        newRequirement.setCreateTime(productRequirement.getCreateTime());

        projectRequirementMapper.insert(newRequirement);
    }

}

