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

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.ProjectExecutionConstants;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.constant.ProjectRequirementConstants;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementAllowedTransitionBatchRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementBatchReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementCloseReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementModuleRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementSplitReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusDictRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementStatusTransitionRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.requirement.ProjectRequirementUpdateReqVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
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.project.ProjectRequirementStatusLogDO;
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.product.ProductRequirementMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
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.project.ProjectRequirementStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
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.system.enums.notify.NotifyMessageLevelConstants;
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
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.math.BigDecimal;
import java.math.RoundingMode;
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 ProjectRequirementServiceImpl implements ProjectRequirementService {

    private static final String REQUIREMENT_OBJECT_TYPE = ProjectRequirementConstants.OBJECT_TYPE;
    private static final String PROJECT_OBJECT_TYPE = ProjectObjectConstants.OBJECT_TYPE;

    private static final String STATUS_PENDING_CLAIM = "pending_claim";
    private static final String STATUS_PENDING_REVIEW = "pending_review";
    private static final String STATUS_REVIEWED = "reviewed";
    private static final String STATUS_REVIEW_REJECTED = "review_rejected";
    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 SOURCE_TYPE_PRODUCT_REQUIREMENT = "product_requirement";
    private static final String SOURCE_TYPE_MANUAL = "manual";
    private static final String SOURCE_TYPE_WORK_ORDER = "work_order";

    private static final Set<String> REVIEW_STATUS_CODES = Set.of(
            STATUS_PENDING_REVIEW, STATUS_REVIEWED, STATUS_REVIEW_REJECTED);
    private static final List<String> ALLOW_DELETE_STATUSES = List.of(
            STATUS_PENDING_CLAIM, STATUS_PENDING_REVIEW);
    private static final List<String> CHILD_ALLOW_CANCEL_STATUSES = List.of(
            STATUS_REJECTED, STATUS_CANCELLED);

    private static final String PROJECT_CREATE_PERMISSION = "project:project:requirement:create";
    private static final String PROJECT_UPDATE_PERMISSION = "project:project:requirement:update";
    private static final String PROJECT_STATUS_PERMISSION = "project:project:requirement:status";
    private static final String PROJECT_REVIEW_PERMISSION = "project:project:requirement:review";
    private static final String PROJECT_DELETE_PERMISSION = "project:project:requirement:delete";
    private static final String PROJECT_SPLIT_PERMISSION = "project:project: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_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 ACTION_SYNC_PRODUCT_STATUS = "sync_project_requirement_status";
    private static final String BIZ_TYPE_REQUIREMENT = REQUIREMENT_OBJECT_TYPE;
    private static final String PRODUCT_BIZ_TYPE_REQUIREMENT = SOURCE_TYPE_PRODUCT_REQUIREMENT;
    private static final String AUTO_DERIVE_REASON = "根据子需求状态自动推导";
    private static final String SYNC_PRODUCT_REASON = "根据项目需求根节点状态自动回写";

    @Resource
    private ProjectRequirementMapper requirementMapper;
    @Resource
    private ProjectRequirementModuleMapper moduleMapper;
    @Resource
    private ProjectRequirementStatusLogMapper statusLogMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ProductRequirementMapper productRequirementMapper;
    @Resource
    private ProductRequirementStatusLogMapper productRequirementStatusLogMapper;
    @Resource
    private ObjectStatusTransitionMapper statusTransitionMapper;
    @Resource
    private ObjectStatusModelMapper statusModelMapper;
    @Resource
    private AttachmentFileIdResolver attachmentFileIdResolver;
    @Resource
    private ProjectExecutionMapper projectExecutionMapper;
    @Resource
    private StatusActionTextResolver statusActionTextResolver;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#createReqVO.projectId",
            permission = PROJECT_CREATE_PERMISSION)
    public Long createRequirement(ProjectRequirementSaveReqVO createReqVO) {
        Long moduleId = resolveModuleId(createReqVO.getModuleId(), createReqVO.getProjectId());
        validateModuleBelongsToProject(moduleId, createReqVO.getProjectId());
        AttachmentValidator.validate(createReqVO.getAttachments());
        attachmentFileIdResolver.resolve(createReqVO.getAttachments());

        ProjectRequirementDO requirement = new ProjectRequirementDO();
        requirement.setProjectId(createReqVO.getProjectId());
        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_IMPLEMENTING;
        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.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(ProjectRequirementDO requirement) {
        Long handlerId = requirement.getCurrentHandlerUserId();
        if (handlerId == null) {
            return; // 无处理人则不发
        }
        Map<String, Object> params = new HashMap<>();
        ProjectDO project = projectMapper.selectById(requirement.getProjectId());
        params.put("projectName", project == null ? "" : project.getProjectName());
        params.put("requirementTitle", requirement.getTitle());
        applicationEventPublisher.publishEvent(NotifySendEvent.of(
                List.of(handlerId), NotifyTemplateCodeConstants.PROJECT_REQUIREMENT_ASSIGNED, params,
                NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#updateReqVO.projectId",
            permission = PROJECT_UPDATE_PERMISSION)
    public void updateRequirement(ProjectRequirementUpdateReqVO updateReqVO) {
        ProjectRequirementDO requirement = validateRequirementExists(updateReqVO.getId());
        validateRequirementBelongsToProject(requirement, updateReqVO.getProjectId());
        if (!(SOURCE_TYPE_PRODUCT_REQUIREMENT.equals(requirement.getSourceType())
                && Objects.equals(requirement.getParentId(), 0L)
                && STATUS_IMPLEMENTING.equals(requirement.getStatusCode()))) {
            validateRequirementEditable(requirement);
        }

        Long moduleId = resolveModuleId(updateReqVO.getModuleId(), updateReqVO.getProjectId());
        validateModuleBelongsToProject(moduleId, updateReqVO.getProjectId());
        AttachmentValidator.validate(updateReqVO.getAttachments());
        attachmentFileIdResolver.resolve(updateReqVO.getAttachments());

        ProjectRequirementDO 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.setSort(updateReqVO.getSort());
        requirement.setAttachments(updateReqVO.getAttachments());
        requirementMapper.updateById(requirement);

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

    @Override
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId", accessible = true)
    public ProjectRequirementRespVO getRequirement(Long id, Long projectId) {
        ProjectRequirementDO requirement = validateRequirementExists(id);
        validateRequirementBelongsToProject(requirement, projectId);
        ProjectRequirementRespVO respVO = buildRequirementRespVO(requirement);
        return respVO;
    }

    @Override
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#pageReqVO.projectId", accessible = true)
    public PageResult<ProjectRequirementRespVO> getRequirementPage(ProjectRequirementPageReqVO pageReqVO) {
        normalizePageModuleFilter(pageReqVO);
        PageResult<ProjectRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO);
        return toRequirementRespPage(pageResult);
    }

    @Override
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#pageReqVO.projectId", accessible = true)
    public PageResult<ProjectRequirementRespVO> getRequirementTree(ProjectRequirementPageReqVO pageReqVO) {
        Long moduleId = pageReqVO.getModuleId();
        Long projectId = pageReqVO.getProjectId();
        if (moduleId != null) {
            pageReqVO.setModuleIds(getAllModuleIdsWithChildren(moduleId, projectId));
            pageReqVO.setModuleId(null);
        }
        pageReqVO.setParentId(null);

        List<ProjectRequirementDO> matchedRequirements = requirementMapper.selectList(pageReqVO);
        if (matchedRequirements.isEmpty()) {
            return new PageResult<>(Collections.emptyList(), 0L);
        }

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

        Set<Long> rootIds = new HashSet<>();
        Set<Long> pathNodeIds = new HashSet<>();
        Map<Long, ProjectRequirementDO> requirementCache = buildRequirementCacheWithAncestors(matchedRequirements);
        for (ProjectRequirementDO requirement : matchedRequirements) {
            pathNodeIds.add(requirement.getId());
            Long rootId = findRootRequirementIdAndCollectPath(requirement, requirementCache, pathNodeIds);
            rootIds.add(rootId);
        }

        List<ProjectRequirementDO> 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<ProjectRequirementDO> pagedRootRequirements = rootRequirements.subList(fromIndex, toIndex);
        Map<Long, List<ProjectRequirementDO>> childrenMap = buildPathChildrenMap(pathNodeIds);
        List<ProjectRequirementRespVO> list = pagedRootRequirements.stream()
                .map(requirement -> buildRequirementRespVOWithPathChildren(requirement, pathNodeIds, childrenMap, statusModelMap))
                .collect(Collectors.toList());
        return new PageResult<>(list, (long) total);
    }

    @Override
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#pageReqVO.projectId", accessible = true)
    public PageResult<ProjectRequirementRespVO> getExecutionRequirementOptions(ProjectRequirementPageReqVO pageReqVO) {
        normalizePageModuleFilter(pageReqVO);
        Set<String> excludedStatusCodes = new HashSet<>(getTerminalStatusCodes());
        excludedStatusCodes.add(STATUS_REVIEW_REJECTED);
        PageResult<ProjectRequirementDO> pageResult = requirementMapper.selectPage(pageReqVO, excludedStatusCodes);
        return toRequirementRespPage(pageResult);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
            permission = PROJECT_STATUS_PERMISSION)
    public void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO) {
        doChangeRequirementStatus(reqVO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
            permission = PROJECT_REVIEW_PERMISSION)
    public void changeRequirementStatusForReview(ProjectRequirementStatusActionReqVO reqVO) {
        validateReviewStatusAction(reqVO.getActionCode());
        doChangeRequirementStatus(reqVO);
    }

    private void doChangeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO) {
        ProjectRequirementDO requirement = validateRequirementExists(reqVO.getId());
        validateRequirementBelongsToProject(requirement, reqVO.getProjectId());
        String actionCode = reqVO.getActionCode().trim();
        String fromStatus = requirement.getStatusCode();
        String reason = normalizeNullableText(reqVO.getReason());
        ProjectRequirementDO before = cloneRequirement(requirement);

        ObjectStatusTransitionDO transition = validateRequirementTransition(fromStatus, actionCode);
        validateReviewRejectedActionAllowed(requirement, actionCode);
        validateTransitionReason(transition, reason);
        String toStatus = transition.getToStatusCode();

        if (ACTION_ACCEPT.equals(actionCode) || ACTION_CLOSE.equals(actionCode)) {
            validateAllChildrenAllowCloseOrAccept(reqVO.getId());
        }
        // 产品需求流转生成的项目需求不允许在项目侧取消，避免与产品侧主状态冲突。
        if (ACTION_CANCEL.equals(actionCode) && isFromProductRequirement(requirement)) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_SYNCED_FROM_PRODUCT_NOT_ALLOW_CANCEL);
        }
        if (ACTION_CANCEL.equals(actionCode) && hasChildren(requirement.getId())) {
            validateParentCancelAllowed(reqVO.getId());
        }
        if (ACTION_CLOSE.equals(actionCode)) {
            closeAllAcceptedChildren(reqVO.getId(), reason);
        }

        int updateCount = requirementMapper.updateStatusByIdAndStatus(requirement.getId(), fromStatus, toStatus, reason);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
        }
        requirement.setStatusCode(toStatus);
        requirement.setLastStatusReason(reason);

        writeRequirementStatusLog(requirement, actionCode, fromStatus, toStatus, reason);
        writeBizAuditLog(requirement, actionCode, fromStatus, toStatus,
                buildRequirementFieldChanges(before, requirement), reason);
        refreshAncestorStatusAndSyncProduct(requirement.getId());
        onRequirementStatusChanged(requirement, toStatus);
    }

    /**
     * TD-015 推模式钩子：需求状态变更后触发，留作后续推送 / 催办的挂载点。
     * 本期空占位：不在此查终态码（避免每次需求状态变更白查一次 DB）；接入通知时再在这里查 terminal_flag
     * 判断 toStatus 是否终态、再触发所属项目「能否完成」判定。终态判定走 DB、不硬编码，与需求状态机后续简化解耦。
     */
    private void onRequirementStatusChanged(ProjectRequirementDO requirement, String toStatus) {
        // TODO(TD-015 推模式)：目标状态为终态时，通知项目负责人项目可能已满足完成条件
        //（该类未配 @Slf4j；接入时可用 requirement.getProjectId() / toStatus 补观测点）。
    }

    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("评审权限只能触发评审通过或评审不通过动作");
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId",
            permission = PROJECT_DELETE_PERMISSION)
    public void deleteRequirement(Long id, Long projectId) {
        ProjectRequirementDO requirement = validateRequirementExists(id);
        validateRequirementBelongsToProject(requirement, projectId);
        String fromStatus = requirement.getStatusCode();

        List<ProjectRequirementDO> children = requirementMapper.selectListByParentId(id);
        if (!children.isEmpty()) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_HAS_CHILDREN);
        }
        if (!ALLOW_DELETE_STATUSES.contains(fromStatus)) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_DELETE);
        }
        // TD-013 口径 Q4=A：项目需求下有承接执行时禁止删除，需先解绑或转移
        if (projectExecutionMapper.countActiveByProjectRequirementId(id) > 0) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_HAS_EXECUTIONS_NOT_ALLOW_DELETE);
        }

        int deleteCount = requirementMapper.deleteByIdAndStatus(id, fromStatus);
        if (deleteCount != 1) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
        }
        writeBizAuditLog(requirement, ACTION_DELETE, fromStatus, null,
                buildRequirementFieldChanges(requirement, null), null);
    }

    @Override
    public void validateUsableForExecution(Long projectId, Long projectRequirementId) {
        if (projectRequirementId == null) {
            return;
        }
        ProjectRequirementDO requirement = requirementMapper.selectById(projectRequirementId);
        if (requirement == null) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_NOT_EXISTS);
        }
        if (!Objects.equals(requirement.getProjectId(), projectId)) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_NOT_BELONG_TO_PROJECT);
        }
        if (getTerminalStatusCodes().contains(requirement.getStatusCode())) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_REQUIREMENT_TERMINAL);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
            permission = PROJECT_SPLIT_PERMISSION)
    public Long splitRequirement(ProjectRequirementSplitReqVO reqVO) {
        ProjectRequirementDO parentRequirement = validateRequirementExists(reqVO.getParentId());
        validateRequirementBelongsToProject(parentRequirement, reqVO.getProjectId());
        validateParentAllowSplit(parentRequirement);
        AttachmentValidator.validate(reqVO.getAttachments());
        attachmentFileIdResolver.resolve(reqVO.getAttachments());

        Long moduleId = reqVO.getModuleId() != null ? reqVO.getModuleId() : parentRequirement.getModuleId();
        validateModuleBelongsToProject(moduleId, reqVO.getProjectId());

        ProjectRequirementDO childRequirement = new ProjectRequirementDO();
        childRequirement.setParentId(reqVO.getParentId());
        childRequirement.setProjectId(parentRequirement.getProjectId());
        childRequirement.setModuleId(moduleId);
        childRequirement.setReviewRequired(reqVO.getReviewRequired());
        childRequirement.setTitle(reqVO.getTitle().trim());
        childRequirement.setDescription(normalizeNullableText(reqVO.getDescription()));
        childRequirement.setCategory(reqVO.getCategory());
        childRequirement.setSourceType(parentRequirement.getSourceType());
        childRequirement.setProductRequirementId(parentRequirement.getProductRequirementId());
        childRequirement.setSourceBizCode(parentRequirement.getSourceBizCode());
        childRequirement.setPriority(reqVO.getPriority());
        String initialStatus = Objects.equals(reqVO.getReviewRequired(), 1)
                ? STATUS_PENDING_REVIEW : STATUS_IMPLEMENTING;
        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.setSort(reqVO.getSort());
        childRequirement.setAttachments(reqVO.getAttachments());
        requirementMapper.insert(childRequirement);

        writeBizAuditLog(childRequirement, ACTION_CREATE, null, initialStatus,
                buildRequirementFieldChanges(null, childRequirement), null);
        if (STATUS_REVIEWED.equals(parentRequirement.getStatusCode())) {
            ProjectRequirementDO before = cloneRequirement(parentRequirement);
            String fromStatus = parentRequirement.getStatusCode();
            int updateCount = requirementMapper.updateStatusByIdAndStatus(
                    parentRequirement.getId(), fromStatus, STATUS_IMPLEMENTING, null);
            if (updateCount != 1) {
                throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
            }
            parentRequirement.setStatusCode(STATUS_IMPLEMENTING);
            parentRequirement.setLastStatusReason(null);
            writeRequirementStatusLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING, null);
            writeBizAuditLog(parentRequirement, ACTION_SPLIT, fromStatus, STATUS_IMPLEMENTING,
                    buildRequirementFieldChanges(before, parentRequirement), null);
        } else {
            writeBizAuditLog(parentRequirement, ACTION_SPLIT, parentRequirement.getStatusCode(),
                    parentRequirement.getStatusCode(), null, null);
        }
        return childRequirement.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
            permission = PROJECT_STATUS_PERMISSION)
    public void closeRequirement(ProjectRequirementCloseReqVO reqVO) {
        ProjectRequirementDO requirement = validateRequirementExists(reqVO.getId());
        validateRequirementBelongsToProject(requirement, reqVO.getProjectId());
        String fromStatus = requirement.getStatusCode();
        String reason = reqVO.getReason().trim();
        ProjectRequirementDO before = cloneRequirement(requirement);

        if (!STATUS_ACCEPTED.equals(fromStatus)) {
            throw exception(ErrorCodeConstants.PROJECT_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.PROJECT_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);
        refreshAncestorStatusAndSyncProduct(requirement.getId());
    }

    @Override
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId", accessible = true)
    public List<ProjectRequirementStatusTransitionRespVO> getAllowedTransitions(Long requirementId, Long projectId) {
        ProjectRequirementDO requirement = validateRequirementExists(requirementId);
        validateRequirementBelongsToProject(requirement, projectId);
        String currentStatus = requirement.getStatusCode();

        List<ObjectStatusTransitionDO> transitions = statusTransitionMapper
                .selectListByObjectTypeAndFromStatus(REQUIREMENT_OBJECT_TYPE, currentStatus);

        return transitions.stream()
                // 取消动作不满足前置条件时，不再返回给前端展示按钮
                .filter(transition -> shouldExposeTransition(requirement, transition))
                .map(transition -> {
                    ProjectRequirementStatusTransitionRespVO vo = new ProjectRequirementStatusTransitionRespVO();
                    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 = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId", accessible = true)
    public List<ProjectRequirementAllowedTransitionBatchRespVO> getAllowedTransitionsBatch(ProjectRequirementBatchReqVO reqVO) {
        List<ProjectRequirementDO> requirements = getBatchRequirements(reqVO.getRequirementIds(), reqVO.getProjectId());
        Map<String, List<ObjectStatusTransitionDO>> transitionsByStatus = getTransitionsByStatus(requirements);
        Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();

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


    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
            permission = PROJECT_CREATE_PERMISSION)
    public Long createRequirementModule(ProjectRequirementModuleReqVO reqVO) {
        validateModuleNameUnique(reqVO.getProjectId(), null, reqVO.getModuleName());

        ProjectRequirementModuleDO module = new ProjectRequirementModuleDO();
        module.setParentId(reqVO.getParentId() != null ? reqVO.getParentId() : 0L);
        module.setProjectId(reqVO.getProjectId());
        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 = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId",
            permission = PROJECT_UPDATE_PERMISSION)
    public void updateRequirementModule(ProjectRequirementModuleReqVO reqVO) {
        if (reqVO.getId() == null) {
            throw invalidParamException("模块编号不能为空");
        }
        ProjectRequirementModuleDO module = validateModuleExists(reqVO.getId());
        validateModuleBelongsToProject(module, reqVO.getProjectId());
        validateModuleNameUnique(reqVO.getProjectId(), 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 = PROJECT_OBJECT_TYPE, objectId = "#projectId",
            permission = PROJECT_DELETE_PERMISSION)
    public void deleteRequirementModule(Long moduleId, Long projectId) {
        ProjectRequirementModuleDO module = validateModuleExists(moduleId);
        validateModuleBelongsToProject(module, projectId);

        List<ProjectRequirementModuleDO> childModules = moduleMapper.selectListByParentId(moduleId);
        if (!childModules.isEmpty()) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_HAS_CHILDREN);
        }

        List<ProjectRequirementDO> requirements = requirementMapper.selectListByModuleId(moduleId);
        if (!requirements.isEmpty()) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_HAS_REQUIREMENTS);
        }
        moduleMapper.deleteById(moduleId);
    }

    @Override
    @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#projectId", accessible = true)
    public List<ProjectRequirementModuleRespVO> getRequirementModuleTree(Long projectId) {
        List<ProjectRequirementModuleDO> modules = moduleMapper.selectListByProjectId(projectId);
        return buildModuleTree(modules, 0L);
    }

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

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

    /**
     * 向上追溯需求根节点，同时收集命中路径上的全部节点。
     */
    private Long findRootRequirementIdAndCollectPath(ProjectRequirementDO requirement,
                                                     Map<Long, ProjectRequirementDO> cache,
                                                     Set<Long> pathNodeIds) {
        ProjectRequirementDO current = requirement;
        while (current.getParentId() != null && current.getParentId() != 0L) {
            ProjectRequirementDO parent = cache.get(current.getParentId());
            if (parent == null) {
                // 父需求缺失时，保持原有容错行为：就近把当前节点视为根节点
                return current.getId();
            }
            pathNodeIds.add(parent.getId());
            current = parent;
        }
        return current.getId();
    }

    /**
     * 只构建命中搜索路径上的树节点，避免整棵树无差别返回。
     */
    private ProjectRequirementRespVO buildRequirementRespVOWithPathChildren(ProjectRequirementDO requirement,
                                                                            Set<Long> pathNodeIds,
                                                                            Map<Long, List<ProjectRequirementDO>> childrenMap,
                                                                            Map<String, ObjectStatusModelDO> statusModelMap) {
        ProjectRequirementRespVO respVO = buildRequirementRespVO(requirement, statusModelMap);
        List<ProjectRequirementDO> allChildren = childrenMap.getOrDefault(requirement.getId(), Collections.emptyList());
        List<ProjectRequirementDO> 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, ProjectRequirementDO> buildRequirementCacheWithAncestors(List<ProjectRequirementDO> matchedRequirements) {
        Map<Long, ProjectRequirementDO> cache = matchedRequirements.stream()
                .collect(Collectors.toMap(ProjectRequirementDO::getId, Function.identity(), (left, right) -> left, HashMap::new));

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

        while (!pendingParentIds.isEmpty()) {
            List<ProjectRequirementDO> parentRequirements = requirementMapper.selectBatchIds(pendingParentIds);
            if (parentRequirements.isEmpty()) {
                break;
            }
            Set<Long> nextPendingParentIds = new HashSet<>();
            for (ProjectRequirementDO 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<ProjectRequirementDO>> buildPathChildrenMap(Set<Long> pathNodeIds) {
        if (pathNodeIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<ProjectRequirementDO> pathChildren = requirementMapper.selectList(
                new LambdaQueryWrapperX<ProjectRequirementDO>()
                        .in(ProjectRequirementDO::getParentId, pathNodeIds)
                        .orderByAsc(ProjectRequirementDO::getSort)
                        .orderByDesc(ProjectRequirementDO::getCreateTime));
        Map<Long, List<ProjectRequirementDO>> childrenMap = new HashMap<>();
        for (ProjectRequirementDO child : pathChildren) {
            childrenMap.computeIfAbsent(child.getParentId(), key -> new ArrayList<>()).add(child);
        }
        return childrenMap;
    }

    private void normalizePageModuleFilter(ProjectRequirementPageReqVO pageReqVO) {
        if (pageReqVO.getModuleId() == null) {
            return;
        }
        if (isAllRequirementsModule(pageReqVO.getModuleId())) {
            pageReqVO.setModuleId(null);
            return;
        }
        List<Long> moduleIds = getAllModuleIdsWithChildren(pageReqVO.getModuleId(), pageReqVO.getProjectId());
        pageReqVO.setModuleIds(moduleIds);
        pageReqVO.setModuleId(null);
    }

    private PageResult<ProjectRequirementRespVO> toRequirementRespPage(PageResult<ProjectRequirementDO> pageResult) {
        Map<String, ObjectStatusModelDO> statusModelMap = getStatusModelMap();
        List<ProjectRequirementRespVO> list = pageResult.getList().stream()
                .map(requirement -> buildRequirementRespVO(requirement, statusModelMap))
                .collect(Collectors.toList());
        return new PageResult<>(list, pageResult.getTotal());
    }

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

    private List<ProjectRequirementDO> getBatchRequirements(List<Long> requirementIds, Long projectId) {
        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, ProjectRequirementDO> requirementMap = requirementMapper.selectByIds(distinctIds).stream()
                .collect(Collectors.toMap(ProjectRequirementDO::getId, Function.identity()));
        if (requirementMap.size() != distinctIds.size()) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_NOT_EXISTS);
        }
        List<ProjectRequirementDO> requirements = distinctIds.stream()
                .map(requirementMap::get)
                .collect(Collectors.toList());
        for (ProjectRequirementDO requirement : requirements) {
            validateRequirementBelongsToProject(requirement, projectId);
        }
        return requirements;
    }

    private Map<String, List<ObjectStatusTransitionDO>> getTransitionsByStatus(List<ProjectRequirementDO> requirements) {
        List<String> statusCodes = requirements.stream()
                .map(ProjectRequirementDO::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<ProjectRequirementStatusTransitionRespVO> buildAllowedTransitions(
            ProjectRequirementDO requirement,
            List<ObjectStatusTransitionDO> transitions,
            Map<String, ObjectStatusModelDO> statusModelMap) {
        return transitions.stream()
                .filter(transition -> shouldExposeTransition(requirement, transition))
                .map(transition -> {
                    ProjectRequirementStatusTransitionRespVO vo = new ProjectRequirementStatusTransitionRespVO();
                    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
    boolean isAllRequirementsModule(Long moduleId) {
        if (moduleId == null) {
            return false;
        }
        ProjectRequirementModuleDO module = moduleMapper.selectById(moduleId);
        return module != null && module.getParentId() != null && module.getParentId() == 0L;
    }

    /**
     * 递归获取模块及其全部子模块 ID。
     */
    @VisibleForTesting
    List<Long> getAllModuleIdsWithChildren(Long moduleId, Long projectId) {
        List<Long> moduleIds = new ArrayList<>();
        moduleIds.add(moduleId);
        List<ProjectRequirementModuleDO> allModules = moduleMapper.selectListByProjectId(projectId);
        collectChildModuleIds(moduleId, allModules, moduleIds);
        return moduleIds;
    }

    private void collectChildModuleIds(Long parentId, List<ProjectRequirementModuleDO> allModules, List<Long> result) {
        for (ProjectRequirementModuleDO module : allModules) {
            if (Objects.equals(module.getParentId(), parentId)) {
                result.add(module.getId());
                collectChildModuleIds(module.getId(), allModules, result);
            }
        }
    }

    /**
     * 获取需求自身及其全部后代需求。
     */
    @VisibleForTesting
    List<ProjectRequirementDO> getAllRequirementsWithChildren(Long requirementId) {
        ProjectRequirementDO root = validateRequirementExists(requirementId);
        List<ProjectRequirementDO> result = new ArrayList<>();
        result.add(root);
        collectChildRequirements(root.getId(), result);
        return result;
    }

    private void collectChildRequirements(Long parentId, List<ProjectRequirementDO> result) {
        List<ProjectRequirementDO> children = requirementMapper.selectListByParentId(parentId);
        for (ProjectRequirementDO child : children) {
            result.add(child);
            collectChildRequirements(child.getId(), result);
        }
    }

    /**
     * 校验所有子需求都已处理到允许验收或关闭的状态。
     */
    @VisibleForTesting
    void validateAllChildrenAllowCloseOrAccept(Long requirementId) {
        List<ProjectRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
        Set<String> terminalStatusCodes = getTerminalStatusCodes();
        for (ProjectRequirementDO requirement : allChildren) {
            if (!Objects.equals(requirement.getId(), requirementId)
                    && !STATUS_ACCEPTED.equals(requirement.getStatusCode())
                    && !terminalStatusCodes.contains(requirement.getStatusCode())) {
                throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CLOSE);
            }
        }
    }

    /**
     * 校验父需求取消时，全部子需求是否都已进入允许取消的状态集合。
     */
    @VisibleForTesting
    void validateParentCancelAllowed(Long requirementId) {
        List<ProjectRequirementDO> allChildren = getAllRequirementsWithChildren(requirementId);
        for (ProjectRequirementDO requirement : allChildren) {
            if (!Objects.equals(requirement.getId(), requirementId)
                    && !CHILD_ALLOW_CANCEL_STATUSES.contains(requirement.getStatusCode())) {
                throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CANCEL);
            }
        }
    }

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

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

    private boolean isReviewRejectedActionAllowed(ProjectRequirementDO requirement, String actionCode) {
        if (!STATUS_REVIEW_REJECTED.equals(requirement.getStatusCode())) {
            return true;
        }
        if (SOURCE_TYPE_MANUAL.equals(requirement.getSourceType())
                || SOURCE_TYPE_PRODUCT_REQUIREMENT.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 isFromProductRequirement(ProjectRequirementDO requirement) {
        return Objects.equals(requirement.getSourceType(), SOURCE_TYPE_PRODUCT_REQUIREMENT)
                && requirement.getProductRequirementId() != null
                && Objects.equals(requirement.getParentId(), 0L);
    }

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

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

    /**
     * 递归关闭所有已验收的子需求。
     */
    private void closeAllAcceptedChildren(Long parentId, String reason) {
        List<ProjectRequirementDO> children = requirementMapper.selectListByParentId(parentId);
        for (ProjectRequirementDO child : children) {
            closeAllAcceptedChildren(child.getId(), reason);
            if (STATUS_ACCEPTED.equals(child.getStatusCode())) {
                ProjectRequirementDO 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);
                }
            }
        }
    }

    /**
     * 子需求状态变化后，先向上自动推导项目父需求状态，再按根需求状态回写产品需求状态。
     */
    private void refreshAncestorStatusAndSyncProduct(Long requirementId) {
        autoDeriveParentStatusRecursively(requirementId);
        syncRootRequirementStatusToProduct(requirementId);
    }

    /**
     * 仅根据直接子需求状态推导父需求状态。
     * 这里只实现约定好的几条规则，其他组合保持父需求当前状态不变。
     */
    private void autoDeriveParentStatusRecursively(Long requirementId) {
        ProjectRequirementDO currentRequirement = requirementMapper.selectById(requirementId);
        if (currentRequirement == null || currentRequirement.getParentId() == null
                || Objects.equals(currentRequirement.getParentId(), 0L)) {
            return;
        }
        ProjectRequirementDO parentRequirement = requirementMapper.selectById(currentRequirement.getParentId());
        if (parentRequirement == null) {
            return;
        }

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

        ProjectRequirementDO before = cloneRequirement(parentRequirement);
        int updateCount = requirementMapper.updateStatusByIdAndStatus(parentRequirement.getId(),
                parentRequirement.getStatusCode(), targetStatus, AUTO_DERIVE_REASON);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PROJECT_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);

        autoDeriveParentStatusRecursively(parentRequirement.getId());
    }

    /**
     * 父需求状态只在三种场景下自动变化：
     * 1. 子需求全部 accepted，父需求自动 accepted；
     * 2. 子需求全部 closed，父需求自动 closed；
     * 3. 子需求全部 cancelled，父需求自动 cancelled；
     * 4. 子需求全部处于 accepted/closed，且至少一个 accepted，父需求保持 accepted。
     */
    private String deriveParentStatusByDirectChildren(Long parentRequirementId) {
        List<ProjectRequirementDO> 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;
    }

    /**
     * 项目需求根节点来自产品需求时，用根节点状态回写产品需求状态。
     */
    private void syncRootRequirementStatusToProduct(Long requirementId) {
        ProjectRequirementDO rootRequirement = findRootRequirement(requirementId);
        if (rootRequirement == null
                || !SOURCE_TYPE_PRODUCT_REQUIREMENT.equals(rootRequirement.getSourceType())
                || rootRequirement.getProductRequirementId() == null) {
            return;
        }
        if (REVIEW_STATUS_CODES.contains(rootRequirement.getStatusCode())) {
            return;
        }

        ProductRequirementDO productRequirement = productRequirementMapper.selectById(rootRequirement.getProductRequirementId());
        if (productRequirement == null || Objects.equals(productRequirement.getStatusCode(), rootRequirement.getStatusCode())) {
            return;
        }

        ProductRequirementDO before = cloneProductRequirement(productRequirement);
        String syncReason = StringUtils.hasText(rootRequirement.getLastStatusReason())
                ? rootRequirement.getLastStatusReason() : SYNC_PRODUCT_REASON;
        int updateCount = productRequirementMapper.updateStatusByIdAndStatus(productRequirement.getId(),
                productRequirement.getStatusCode(), rootRequirement.getStatusCode(), syncReason);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED);
        }
        productRequirement.setStatusCode(rootRequirement.getStatusCode());
        productRequirement.setLastStatusReason(syncReason);

        writeProductRequirementStatusLog(productRequirement, ACTION_SYNC_PRODUCT_STATUS, before.getStatusCode(),
                rootRequirement.getStatusCode(), syncReason);
        writeProductRequirementAuditLog(productRequirement, ACTION_SYNC_PRODUCT_STATUS, before.getStatusCode(),
                rootRequirement.getStatusCode(), buildProductRequirementFieldChanges(before, productRequirement),
                syncReason);
    }

    /**
     * 沿父链回溯项目需求根节点，产品状态回写统一以根节点状态为准。
     */
    private ProjectRequirementDO findRootRequirement(Long requirementId) {
        ProjectRequirementDO currentRequirement = requirementMapper.selectById(requirementId);
        while (currentRequirement != null && currentRequirement.getParentId() != null
                && !Objects.equals(currentRequirement.getParentId(), 0L)) {
            currentRequirement = requirementMapper.selectById(currentRequirement.getParentId());
        }
        return currentRequirement;
    }

    private ProjectRequirementStatusDictRespVO buildStatusDictRespVO(ObjectStatusModelDO statusModel) {
        ProjectRequirementStatusDictRespVO respVO = new ProjectRequirementStatusDictRespVO();
        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;
    }

    /**
     * 构建需求响应对象，不包含子需求。
     */
    private ProjectRequirementRespVO buildRequirementRespVO(ProjectRequirementDO requirement) {
        return buildRequirementRespVO(requirement, getStatusModelMap());
    }

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

    private List<ProjectRequirementModuleRespVO> buildModuleTree(List<ProjectRequirementModuleDO> modules, Long parentId) {
        return modules.stream()
                .filter(module -> Objects.equals(module.getParentId(), parentId))
                .map(module -> {
                    ProjectRequirementModuleRespVO vo = BeanUtils.toBean(module, ProjectRequirementModuleRespVO.class);
                    vo.setChildren(buildModuleTree(modules, module.getId()));
                    return vo;
                })
                .collect(Collectors.toList());
    }

    @VisibleForTesting
    ProjectRequirementDO validateRequirementExists(Long id) {
        if (id == null) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_NOT_EXISTS);
        }
        ProjectRequirementDO requirement = requirementMapper.selectById(id);
        if (requirement == null) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_NOT_EXISTS);
        }
        return requirement;
    }

    /**
     * 要求接口上传入的项目编号与需求归属项目一致，避免串项目操作。
     */
    @VisibleForTesting
    void validateRequirementBelongsToProject(ProjectRequirementDO requirement, Long projectId) {
        if (!Objects.equals(requirement.getProjectId(), projectId)) {
            throw invalidParamException("需求不属于当前项目");
        }
    }

    @VisibleForTesting
    void validateRequirementEditable(ProjectRequirementDO requirement) {
        ObjectStatusModelDO statusModel = statusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
                REQUIREMENT_OBJECT_TYPE, requirement.getStatusCode());
        if (statusModel == null) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_STATUS_NOT_ALLOW_EDIT);
        }
    }

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

    @VisibleForTesting
    void validateParentAllowSplit(ProjectRequirementDO parentRequirement) {
        String status = parentRequirement.getStatusCode();
        if (!STATUS_IMPLEMENTING.equals(status)&& !STATUS_REVIEWED.equals(status)) {
            throw exception(ErrorCodeConstants.PROJECT_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.PROJECT_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.PROJECT_REQUIREMENT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
        }
    }

    @VisibleForTesting
    ProjectRequirementModuleDO validateModuleExists(Long id) {
        if (id == null) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_NOT_EXISTS);
        }
        ProjectRequirementModuleDO module = moduleMapper.selectById(id);
        if (module == null) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_NOT_EXISTS);
        }
        return module;
    }

    @VisibleForTesting
    void validateModuleNameUnique(Long projectId, Long moduleId, String moduleName) {
        if (!StringUtils.hasText(moduleName)) {
            return;
        }
        ProjectRequirementModuleDO existModule = moduleMapper
                .selectByProjectIdAndModuleName(projectId, moduleName.trim());
        if (existModule == null) {
            return;
        }
        if (!existModule.getId().equals(moduleId)) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_NAME_DUPLICATE, moduleName.trim());
        }
    }

    @VisibleForTesting
    Long resolveModuleId(Long moduleId, Long projectId) {
        if (moduleId != null) {
            return moduleId;
        }
        ProjectRequirementModuleDO defaultModule = moduleMapper.selectByProjectIdAndParentId(projectId, 0L);
        if (defaultModule == null) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_NOT_EXISTS);
        }
        return defaultModule.getId();
    }

    @VisibleForTesting
    void validateModuleBelongsToProject(Long moduleId, Long projectId) {
        ProjectRequirementModuleDO module = validateModuleExists(moduleId);
        validateModuleBelongsToProject(module, projectId);
    }

    @VisibleForTesting
    void validateModuleBelongsToProject(ProjectRequirementModuleDO module, Long projectId) {
        if (!Objects.equals(module.getProjectId(), projectId)) {
            throw exception(ErrorCodeConstants.PROJECT_REQUIREMENT_MODULE_NOT_BELONG_TO_PROJECT);
        }
    }

    private void writeRequirementStatusLog(ProjectRequirementDO requirement, String actionType,
                                           String fromStatus, String toStatus, String reason) {
        ProjectRequirementStatusLogDO statusLog = new ProjectRequirementStatusLogDO();
        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(ProjectRequirementDO 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(defaultText(reason));
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private ProjectRequirementDO cloneRequirement(ProjectRequirementDO source) {
        ProjectRequirementDO target = new ProjectRequirementDO();
        target.setId(source.getId());
        target.setParentId(source.getParentId());
        target.setProjectId(source.getProjectId());
        target.setModuleId(source.getModuleId());
        target.setProductRequirementId(source.getProductRequirementId());
        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.setSort(source.getSort());
        target.setAttachments(source.getAttachments());
        return target;
    }

    /**
     * 仅记录真正发生变化的字段，便于审计日志后续解析。
     */
    private String buildRequirementFieldChanges(ProjectRequirementDO before, ProjectRequirementDO after) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "parentId", valueOf(before, ProjectRequirementDO::getParentId),
                valueOf(after, ProjectRequirementDO::getParentId));
        appendFieldChange(fieldChanges, "projectId", valueOf(before, ProjectRequirementDO::getProjectId),
                valueOf(after, ProjectRequirementDO::getProjectId));
        appendFieldChange(fieldChanges, "moduleId", valueOf(before, ProjectRequirementDO::getModuleId),
                valueOf(after, ProjectRequirementDO::getModuleId));
        appendFieldChange(fieldChanges, "productRequirementId", valueOf(before, ProjectRequirementDO::getProductRequirementId),
                valueOf(after, ProjectRequirementDO::getProductRequirementId));
        appendFieldChange(fieldChanges, "reviewRequired", valueOf(before, ProjectRequirementDO::getReviewRequired),
                valueOf(after, ProjectRequirementDO::getReviewRequired));
        appendFieldChange(fieldChanges, "title", valueOf(before, ProjectRequirementDO::getTitle),
                valueOf(after, ProjectRequirementDO::getTitle));
        appendFieldChange(fieldChanges, "description", valueOf(before, ProjectRequirementDO::getDescription),
                valueOf(after, ProjectRequirementDO::getDescription));
        appendFieldChange(fieldChanges, "category", valueOf(before, ProjectRequirementDO::getCategory),
                valueOf(after, ProjectRequirementDO::getCategory));
        appendFieldChange(fieldChanges, "sourceType", valueOf(before, ProjectRequirementDO::getSourceType),
                valueOf(after, ProjectRequirementDO::getSourceType));
        appendFieldChange(fieldChanges, "sourceBizCode", valueOf(before, ProjectRequirementDO::getSourceBizCode),
                valueOf(after, ProjectRequirementDO::getSourceBizCode));
        appendFieldChange(fieldChanges, "priority", valueOf(before, ProjectRequirementDO::getPriority),
                valueOf(after, ProjectRequirementDO::getPriority));
        appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProjectRequirementDO::getStatusCode),
                valueOf(after, ProjectRequirementDO::getStatusCode));
        appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, ProjectRequirementDO::getLastStatusReason),
                valueOf(after, ProjectRequirementDO::getLastStatusReason));
        appendFieldChange(fieldChanges, "proposerId", valueOf(before, ProjectRequirementDO::getProposerId),
                valueOf(after, ProjectRequirementDO::getProposerId));
        appendFieldChange(fieldChanges, "proposerNickname", valueOf(before, ProjectRequirementDO::getProposerNickname),
                valueOf(after, ProjectRequirementDO::getProposerNickname));
        appendFieldChange(fieldChanges, "expectedTime", valueOf(before, ProjectRequirementDO::getExpectedTime),
                valueOf(after, ProjectRequirementDO::getExpectedTime));
        appendFieldChange(fieldChanges, "currentHandlerUserId",
                valueOf(before, ProjectRequirementDO::getCurrentHandlerUserId),
                valueOf(after, ProjectRequirementDO::getCurrentHandlerUserId));
        appendFieldChange(fieldChanges, "currentHandlerUserNickname",
                valueOf(before, ProjectRequirementDO::getCurrentHandlerUserNickname),
                valueOf(after, ProjectRequirementDO::getCurrentHandlerUserNickname));
        appendFieldChange(fieldChanges, "sort", valueOf(before, ProjectRequirementDO::getSort),
                valueOf(after, ProjectRequirementDO::getSort));
        appendFieldChange(fieldChanges, "attachments", valueOf(before, ProjectRequirementDO::getAttachments),
                valueOf(after, ProjectRequirementDO::getAttachments));
        return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
    }

    /**
     * 写入产品需求状态日志，便于追踪项目需求对产品需求的回写。
     */
    private void writeProductRequirementStatusLog(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());
        productRequirementStatusLogMapper.insert(statusLog);
    }

    /**
     * 写入产品需求审计日志，保留状态回写前后的字段变化。
     */
    private void writeProductRequirementAuditLog(ProductRequirementDO requirement, String actionType, String fromStatus,
                                                 String toStatus, String fieldChanges, String reason) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(PRODUCT_BIZ_TYPE_REQUIREMENT);
        auditLog.setBizId(requirement.getId());
        auditLog.setActionType(actionType);
        auditLog.setFromStatus(fromStatus);
        auditLog.setToStatus(toStatus);
        auditLog.setFieldChanges(fieldChanges);
        auditLog.setReason(defaultText(reason));
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private ProductRequirementDO cloneProductRequirement(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 buildProductRequirementFieldChanges(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(ProjectRequirementDO requirement, Function<ProjectRequirementDO, T> getter) {
        return requirement == null ? null : getter.apply(requirement);
    }

    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);
    }

    private String normalizeNullableText(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }

    private String defaultText(String value) {
        return StringUtils.hasText(value) ? value : "";
    }

}
