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

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.dataobject.BaseDO;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
import com.njcn.rdms.module.project.controller.admin.common.vo.CurrentUserRoleVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextNavRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextProductRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRoleRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductCreateWithTeamReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOptionRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.*;
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO;
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.ProductDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO;
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.mysql.product.ProductMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductStatusLogMapper;
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.security.annotation.CheckObjectPermission;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.permission.dto.ObjectMenuRespDTO;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRolePermissionRespDTO;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
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 ProductServiceImpl implements ProductService {

    private static final List<String> PRODUCT_BIND_EXCLUDED_STATUS_CODES = List.of(
            ProductObjectConstants.STATUS_PAUSED,
            ProductObjectConstants.STATUS_ABANDONED);

    @Resource
    private ProductMapper productMapper;
    @Resource
    private ProductStatusLogMapper productStatusLogMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ObjectStatusTransitionMapper objectStatusTransitionMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private UserObjectRoleMapper userObjectRoleMapper;
    @Resource
    private ObjectPermissionApi objectPermissionApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private ProductRequirementModuleMapper requirementModuleMapper;
    @Resource
    private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
    @Resource
    private com.njcn.rdms.module.project.service.member.CurrentUserRoleResolver currentUserRoleResolver;
    @Resource
    private ObjectDataScopeService objectDataScopeService;
    @Resource
    private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
    @Resource
    private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
    @Resource
    private com.njcn.rdms.module.project.framework.notify.TeamNotifyRecipientResolver teamNotifyRecipientResolver;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createProduct(ProductSaveReqVO createReqVO) {
        validateCreateReqVO(createReqVO);
        validateManagerUser(createReqVO.getManagerUserId());

        ProductDO product = new ProductDO();
        product.setCode(generateProductCode(createReqVO.getCode()));
        product.setDirectionCode(createReqVO.getDirectionCode());
        String initialStatus = getInitialStatusCode();
        product.setStatusCode(initialStatus);
        product.setName(createReqVO.getName().trim());
        product.setManagerUserId(createReqVO.getManagerUserId());
        product.setDescription(normalizeNullableText(createReqVO.getDescription()));
        productMapper.insert(product);

        initManagerMemberRelation(product);
        // 多角色支持：自动落 creator 行（即使 creator == manager 也独立写一条，确保创建者身份不丢）
        objectRoleAutoAssignService.assignCreator(ProductObjectConstants.OBJECT_TYPE, product.getId(),
                SecurityFrameworkUtils.getLoginUserId(),
                com.njcn.rdms.module.system.enums.permission.RoleCodeEnum.PRODUCT_CREATOR.getCode());
        initDefaultRequirementModule(product);
        writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_CREATE, null, initialStatus,
                buildProductFieldChanges(null, product), null);
        // 团队成员已全部落库，发「产品创建」通知（操作人由监听器统一排除）
        publishProductCreatedNotify(product);
        return product.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createProductWithTeam(ProductCreateWithTeamReqVO reqVO) {
        ProductSaveReqVO productReq = reqVO.getProduct();
        List<ProductMemberSaveReqVO> members = reqVO.getMembers();

        // 1) 复用旧 create 接口的产品资料校验（名称 / 编码唯一性、产品经理用户存在）
        validateCreateReqVO(productReq);
        validateManagerUser(productReq.getManagerUserId());

        // 2) 初始团队校验：包含经理、userId 不重复、roleId 合法（含经理那条必须为产品经理角色）
        validateInitialMembers(productReq.getManagerUserId(), members);

        // 3) 写入产品基础资料：与 createProduct 完全一致，唯一差异是不调用 initManagerMemberRelation —— 经理成员由 members 显式提供，避免重复插入
        ProductDO product = new ProductDO();
        product.setCode(generateProductCode(productReq.getCode()));
        product.setDirectionCode(productReq.getDirectionCode());
        String initialStatus = getInitialStatusCode();
        product.setStatusCode(initialStatus);
        product.setName(productReq.getName().trim());
        product.setManagerUserId(productReq.getManagerUserId());
        product.setDescription(normalizeNullableText(productReq.getDescription()));
        productMapper.insert(product);

        initDefaultRequirementModule(product);

        // 4) 遍历写入初始成员，含成员维度审计；roleMap 在校验阶段已确保合法
        LocalDateTime now = LocalDateTime.now();
        for (ProductMemberSaveReqVO memberReq : members) {
            UserObjectRoleDO member = new UserObjectRoleDO();
            member.setUserId(memberReq.getUserId());
            member.setObjectType(ProductObjectConstants.OBJECT_TYPE);
            member.setObjectId(product.getId());
            member.setRoleId(memberReq.getRoleId());
            member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
            member.setJoinedTime(now);
            member.setLeftTime(null);
            member.setRemark(normalizeNullableText(memberReq.getRemark()));
            userObjectRoleMapper.insert(member);
            writeMemberInitAuditLog(member);
        }

        // 5) 产品维度的"指定经理"审计：与旧 initManagerMemberRelation 末尾一致，保留活动时间线一致性
        writeManagerInitAuditLog(product.getId(), product.getManagerUserId());

        // 6) 多角色支持：自动落 creator 行（不论 creator 是否在 members 列表里，都独立写一条 creator 角色）
        objectRoleAutoAssignService.assignCreator(ProductObjectConstants.OBJECT_TYPE, product.getId(),
                SecurityFrameworkUtils.getLoginUserId(),
                com.njcn.rdms.module.system.enums.permission.RoleCodeEnum.PRODUCT_CREATOR.getCode());

        // 7) 关心人批量落地（watcherUserIds 可空；可与 members 的 user 重叠 —— 多角色合法）
        objectRoleAutoAssignService.assignWatchers(ProductObjectConstants.OBJECT_TYPE, product.getId(),
                reqVO.getWatcherUserIds(),
                com.njcn.rdms.module.system.enums.permission.RoleCodeEnum.PRODUCT_WATCHER.getCode());

        // 8) 产品创建审计
        writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_CREATE, null, initialStatus,
                buildProductFieldChanges(null, product), null);
        // 9) 团队成员已全部落库，发「产品创建」通知（操作人由监听器统一排除）
        publishProductCreatedNotify(product);
        return product.getId();
    }

    /**
     * 创建产品后逐成员发「产品创建」站内信：显式团队成员（排除创建者角色），每人带自己的角色名；
     * 操作人自己由监听器统一排除。仅 publish，发送由 NotifySendEventListener 事务提交后处理。
     */
    @VisibleForTesting
    void publishProductCreatedNotify(ProductDO product) {
        Long operatorId = SecurityFrameworkUtils.getLoginUserId();
        List<com.njcn.rdms.module.project.framework.notify.TeamRecipient> recipients =
                teamNotifyRecipientResolver.resolveActiveExcludingCreator(
                        ProductObjectConstants.OBJECT_TYPE, product.getId(),
                        com.njcn.rdms.module.system.enums.permission.RoleCodeEnum.PRODUCT_CREATOR.getCode(),
                        ProductObjectConstants.MANAGER_ROLE_CODE);
        for (com.njcn.rdms.module.project.framework.notify.TeamRecipient r : recipients) {
            // 逐成员一条事件：每人带自己的代表角色中文名
            Map<String, Object> params = new HashMap<>();
            params.put("productName", product.getName());
            params.put("roleName", r.roleName());
            applicationEventPublisher.publishEvent(
                    com.njcn.rdms.module.project.framework.notify.NotifySendEvent.of(
                            java.util.List.of(r.userId()),
                            com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants.PRODUCT_CREATED,
                            params,
                            com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants.NORMAL, operatorId));
        }
    }

    /**
     * 校验初始团队成员列表。
     *
     * <p>校验维度：
     * <ol>
     *   <li>members 中 userId 不可重复</li>
     *   <li>members 必须包含产品经理本人（userId == managerUserId）</li>
     *   <li>所有 roleId 必须落在 scopeType=object, objectType=product 且启用</li>
     *   <li>经理那条 members 的 roleId 必须是 {@code product_manager} 角色（数据一致性兜底）</li>
     * </ol>
     */
    @VisibleForTesting
    void validateInitialMembers(Long managerUserId, List<ProductMemberSaveReqVO> members) {
        if (members == null || members.isEmpty()) {
            // @NotEmpty 已挡，此处兜底
            throw exception(ErrorCodeConstants.PRODUCT_INITIAL_TEAM_MANAGER_REQUIRED);
        }

        Set<Long> userIds = new HashSet<>();
        for (ProductMemberSaveReqVO m : members) {
            if (!userIds.add(m.getUserId())) {
                throw exception(ErrorCodeConstants.PRODUCT_INITIAL_TEAM_MEMBER_DUPLICATE);
            }
        }
        if (!userIds.contains(managerUserId)) {
            throw exception(ErrorCodeConstants.PRODUCT_INITIAL_TEAM_MANAGER_REQUIRED);
        }

        Set<Long> roleIds = members.stream().map(ProductMemberSaveReqVO::getRoleId).collect(Collectors.toSet());
        List<ObjectRoleRespDTO> roles = objectPermissionApi
                .getObjectRoleList(roleIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProductObjectConstants.OBJECT_TYPE)
                .getCheckedData();
        Map<Long, ObjectRoleRespDTO> roleMap = roles == null ? Collections.emptyMap()
                : roles.stream().collect(Collectors.toMap(ObjectRoleRespDTO::getId, Function.identity()));
        for (Long roleId : roleIds) {
            if (!roleMap.containsKey(roleId)) {
                throw exception(ErrorCodeConstants.PRODUCT_INITIAL_TEAM_ROLE_INVALID);
            }
        }

        // 经理那条 members 的 roleId 必须对应 product_manager 角色，否则 product.managerUserId 与成员表角色语义会错乱
        ProductMemberSaveReqVO managerMember = members.stream()
                .filter(m -> Objects.equals(m.getUserId(), managerUserId))
                .findFirst()
                .orElseThrow(() -> exception(ErrorCodeConstants.PRODUCT_INITIAL_TEAM_MANAGER_REQUIRED));
        ObjectRoleRespDTO managerRole = roleMap.get(managerMember.getRoleId());
        if (managerRole == null
                || !Objects.equals(ProductObjectConstants.MANAGER_ROLE_CODE, managerRole.getCode())) {
            throw exception(ErrorCodeConstants.PRODUCT_INITIAL_TEAM_ROLE_INVALID);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#updateReqVO.id",
            permission = ProductObjectConstants.PERMISSION_UPDATE)
    public void updateProduct(ProductSaveReqVO updateReqVO) {
        if (updateReqVO.getId() == null) {
            throw invalidParamException("产品编号不能为空");
        }
        ProductDO product = validateProductExists(updateReqVO.getId());
        validateProductCodeUnchanged(product, updateReqVO.getCode());
        validateManagerUserUnchanged(product, updateReqVO.getManagerUserId());
        applyProductBaseInfoUpdate(product, updateReqVO.getDirectionCode(), updateReqVO.getName(), updateReqVO.getDescription());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
            permission = ProductObjectConstants.PERMISSION_UPDATE)
    public void updateProductBaseInfo(Long productId, ProductSettingBaseInfoUpdateReqVO reqVO) {
        ProductDO product = validateProductExists(productId);
        applyProductBaseInfoUpdate(product, reqVO.getDirectionCode(), reqVO.getName(), reqVO.getDescription());
    }

    @Override
    public ProductDO getProduct(Long id) {
        return validateProductExists(id);
    }

    @Override
    public ProductContextRespVO getProductContext(Long id) {
        ProductDO product = validateProductExists(id);

        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        // 多角色支持：拿全部 ACTIVE 角色 ID 传聚合 API；主角色由 API 端按 sort 升序挑选 + 附 additionalRoleNames
        List<UserObjectRoleDO> userRoles = userObjectRoleMapper
                .selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, id, loginUserId);
        if (!userRoles.isEmpty()) {
            List<Long> roleIds = userRoles.stream().map(UserObjectRoleDO::getRoleId).distinct().toList();
            return buildProductContext(product, roleIds, false, null);
        }

        // 显式角色为空：走 scope.contains 判定隐式 observer 兜底（设计文档 2.1 节末段）
        ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
        if (!scope.contains(id, product.getDirectionCode())) {
            throw exception(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED);
        }
        return buildImplicitObserverContext(product);
    }

    /**
     * 隐式 observer 兜底上下文：用户无显式产品角色但在 scope 范围内，按 implicit_observer_product 角色渲染菜单/权限。
     */
    private ProductContextRespVO buildImplicitObserverContext(ProductDO product) {
        ObjectRoleRespDTO observerRole = objectPermissionApi
                .getObjectRoleByCode(
                        com.njcn.rdms.module.system.enums.permission.ObjectRoleConstants.IMPLICIT_OBSERVER_PRODUCT.getCode(),
                        ObjectRoleConstants.ROLE_SCOPE_OBJECT,
                        ProductObjectConstants.OBJECT_TYPE)
                .getCheckedData();
        if (observerRole == null || observerRole.getId() == null) {
            // 角色未配置，降级为无菜单的 guest 上下文（不应发生，属配置缺失）
            return buildProductContextWithoutMenus(product, true);
        }
        return buildProductContext(product, List.of(observerRole.getId()), true, observerRole);
    }

    private ProductContextRespVO buildProductContext(ProductDO product, List<Long> roleIds, boolean guestFlag,
                                                     ObjectRoleRespDTO fallbackRole) {
        ProductContextRespVO respVO = new ProductContextRespVO();
        respVO.setCurrentProduct(buildCurrentProduct(product));

        ObjectRolePermissionRespDTO permissionDetail = objectPermissionApi
                .getObjectRolePermissionDetailMerged(roleIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT,
                        ProductObjectConstants.OBJECT_TYPE)
                .getCheckedData();
        ObjectRoleRespDTO currentRole = permissionDetail == null || permissionDetail.getCurrentRole() == null
                ? fallbackRole
                : permissionDetail.getCurrentRole();
        List<ObjectMenuRespDTO> menus = permissionDetail == null || permissionDetail.getMenus() == null
                ? Collections.emptyList()
                : permissionDetail.getMenus();
        List<String> additionalRoleNames = permissionDetail == null || permissionDetail.getAdditionalRoleNames() == null
                ? Collections.emptyList()
                : permissionDetail.getAdditionalRoleNames();
        Long primaryRoleId = currentRole != null ? currentRole.getId() : (roleIds.isEmpty() ? null : roleIds.get(0));
        respVO.setCurrentRole(buildCurrentRole(primaryRoleId, currentRole, guestFlag, additionalRoleNames));
        respVO.setNavs(buildContextNavs(menus));
        respVO.setButtons(buildContextButtons(menus));
        return respVO;
    }

    private ProductContextRespVO buildProductContextWithoutMenus(ProductDO product, boolean guestFlag) {
        ProductContextRespVO respVO = new ProductContextRespVO();
        respVO.setCurrentProduct(buildCurrentProduct(product));
        respVO.setCurrentRole(buildCurrentRole(null, null, guestFlag, Collections.emptyList()));
        respVO.setNavs(Collections.emptyList());
        respVO.setButtons(Collections.emptyList());
        return respVO;
    }

    @Override
    public PageResult<ProductRespVO> getProductPage(ProductPageReqVO pageReqVO) {
        // 计算当前用户在 product 域的数据权限范围
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
        if (scope.getState() == ObjectDataScope.State.EMPTY) {
            return PageResult.empty();
        }

        // 保留原有业务过滤条件（同 ProductMapper.selectPage 默认方法）
        LambdaQueryWrapperX<ProductDO> wrapper = new LambdaQueryWrapperX<>();
        if (StringUtils.hasText(pageReqVO.getKeyword())) {
            wrapper.and(w -> w.like(ProductDO::getCode, pageReqVO.getKeyword())
                    .or()
                    .like(ProductDO::getName, pageReqVO.getKeyword()));
        }
        wrapper.eqIfPresent(ProductDO::getDirectionCode, pageReqVO.getDirectionCode())
                .eqIfPresent(ProductDO::getManagerUserId, pageReqVO.getManagerUserId())
                .eqIfPresent(ProductDO::getStatusCode, pageReqVO.getStatusCode())
                .betweenIfPresent(BaseDO::getUpdateTime, pageReqVO.getUpdateTime())
                .orderByDesc(BaseDO::getCreateTime);

        // 注入 scope 数据权限过滤条件（在所有业务条件之后）
        if (scope.getState() == ObjectDataScope.State.ID_LIST) {
            Set<Long> ids = scope.getIds();
            Set<String> dcs = scope.getDirectionCodes();
            wrapper.and(w -> {
                boolean addedAny = false;
                if (!ids.isEmpty()) {
                    w.in(ProductDO::getId, ids);
                    addedAny = true;
                }
                if (!dcs.isEmpty()) {
                    if (addedAny) {
                        w.or();
                    }
                    w.in(ProductDO::getDirectionCode, dcs);
                }
            });
        }
        // ALL 状态不加任何 scope 条件，直接查全部

        PageResult<ProductDO> doPage = productMapper.selectPage(pageReqVO, wrapper);
        return convertProductListWithRoles(doPage);
    }

    /**
     * 产品分页 DO → RespVO，并回填「当前登录用户在该产品的角色」。
     * 角色聚合对当前页仅两次查询（角色行 + 角色名翻译），无 N+1；无角色的产品返回空数组。
     */
    private PageResult<ProductRespVO> convertProductListWithRoles(PageResult<ProductDO> doPage) {
        List<ProductDO> list = doPage.getList();
        if (list == null || list.isEmpty()) {
            return new PageResult<>(new ArrayList<>(), doPage.getTotal());
        }
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        List<Long> objectIds = list.stream()
                .map(ProductDO::getId).filter(Objects::nonNull).collect(Collectors.toList());
        Map<Long, List<CurrentUserRoleVO>> rolesByProduct = currentUserRoleResolver.resolveByObjectIds(
                ProductObjectConstants.OBJECT_TYPE, loginUserId, objectIds, ProductObjectConstants.MANAGER_ROLE_CODE);
        List<ProductRespVO> voList = list.stream().map(p -> {
            ProductRespVO vo = BeanUtils.toBean(p, ProductRespVO.class);
            vo.setCurrentUserRoles(rolesByProduct.getOrDefault(p.getId(), Collections.emptyList()));
            return vo;
        }).collect(Collectors.toList());
        return new PageResult<>(voList, doPage.getTotal());
    }

    @Override
    public ProductOverviewSummaryRespVO getProductOverviewSummary() {
        // 与列表对称：按当前用户的 ObjectDataScope 过滤统计数字（超管 ALL 走全表 SQL，普通用户走 scope 过滤）
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
        List<Map<String, Object>> rows = buildStatusCountRows(scope);
        List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
                .selectListByObjectType(ProductObjectConstants.OBJECT_TYPE);

        ProductOverviewSummaryRespVO respVO = new ProductOverviewSummaryRespVO();
        Map<String, Long> statusCounts = buildProductStatusCounts(statusModels, rows);
        respVO.setStatusCounts(statusCounts);
        respVO.setItems(buildStatusBoardItems(statusModels, statusCounts));
        respVO.setTotal(respVO.getItems().stream()
                .mapToLong(ProductOverviewSummaryRespVO.StatusBoardItemVO::getCount).sum());
        return respVO;
    }

    @Override
    public List<ProductOptionRespVO> getProductOptions() {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
        if (scope.getState() == ObjectDataScope.State.EMPTY) {
            return Collections.emptyList();
        }
        LambdaQueryWrapperX<ProductDO> wrapper = new LambdaQueryWrapperX<>();
        wrapper.notIn(ProductDO::getStatusCode, PRODUCT_BIND_EXCLUDED_STATUS_CODES);
        wrapper.orderByDesc(ProductDO::getCreateTime);
        applyProductScopeFilter(wrapper, scope);
        return productMapper.selectList(wrapper).stream()
                .map(product -> {
                    ProductOptionRespVO respVO = new ProductOptionRespVO();
                    respVO.setId(product.getId());
                    respVO.setCode(product.getCode());
                    respVO.setName(product.getName());
                    respVO.setDirectionCode(product.getDirectionCode());
                    return respVO;
                })
                .toList();
    }

    /**
     * 按 scope 算出 (statusCode, countValue) 行集，喂给 {@link #buildProductStatusCounts}。
     * EMPTY 直接空集；ALL 走原全表 GROUP BY SQL；ID_LIST 用 wrapper 取 status_code，Java 端 group + count。
     */
    private List<Map<String, Object>> buildStatusCountRows(ObjectDataScope scope) {
        if (scope.getState() == ObjectDataScope.State.EMPTY) {
            return Collections.emptyList();
        }
        if (scope.getState() == ObjectDataScope.State.ALL) {
            return productMapper.selectStatusCountList();
        }
        // ID_LIST
        LambdaQueryWrapperX<ProductDO> wrapper = new LambdaQueryWrapperX<>();
        wrapper.select(ProductDO::getStatusCode);
        applyProductScopeFilter(wrapper, scope);
        return productMapper.selectList(wrapper).stream()
                .filter(p -> p.getStatusCode() != null)
                .collect(Collectors.groupingBy(ProductDO::getStatusCode, Collectors.counting()))
                .entrySet().stream()
                .map(e -> {
                    Map<String, Object> row = new HashMap<>();
                    row.put("statusCode", e.getKey());
                    row.put("countValue", e.getValue());
                    return row;
                })
                .collect(Collectors.toList());
    }

    private void applyProductScopeFilter(LambdaQueryWrapperX<ProductDO> wrapper, ObjectDataScope scope) {
        if (scope.getState() != ObjectDataScope.State.ID_LIST) {
            return;
        }
        Set<Long> ids = scope.getIds();
        Set<String> dcs = scope.getDirectionCodes();
        wrapper.and(w -> {
            boolean addedAny = false;
            if (!ids.isEmpty()) {
                w.in(ProductDO::getId, ids);
                addedAny = true;
            }
            if (!dcs.isEmpty()) {
                if (addedAny) {
                    w.or();
                }
                w.in(ProductDO::getDirectionCode, dcs);
            }
        });
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#reqVO.id",
            permission = ProductObjectConstants.PERMISSION_STATUS)
    public void changeProductStatus(ProductStatusActionReqVO reqVO) {
        ProductDO product = validateProductExists(reqVO.getId());
        String actionCode = reqVO.getActionCode().trim();
        ObjectStatusTransitionDO transition = validateProductTransition(product.getStatusCode(), actionCode);
        String reason = normalizeNullableText(reqVO.getReason());
        validateTransitionReason(transition, reason);

        String fromStatus = product.getStatusCode();
        String toStatus = transition.getToStatusCode();
        int updateCount = productMapper.updateStatusByIdAndStatus(product.getId(), fromStatus, toStatus, reason);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_CONCURRENT_MODIFIED);
        }
        product.setStatusCode(toStatus);
        product.setLastStatusReason(reason);

        writeProductStatusLog(product, actionCode, fromStatus, toStatus, reason);
        writeBizAuditLog(product, actionCode, fromStatus, toStatus, null, reason);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#reqVO.id",
            permission = ProductObjectConstants.PERMISSION_DELETE)
    public void deleteProduct(ProductDeleteReqVO reqVO) {
        ProductDO product = validateProductExists(reqVO.getId());
        validateDeleteConfirmText(reqVO.getConfirmText());
        if (!Objects.equals(product.getName(), reqVO.getProductName().trim())) {
            throw exception(ErrorCodeConstants.PRODUCT_DELETE_NAME_MISMATCH);
        }

        String reason = reqVO.getReason().trim();
        String fromStatus = product.getStatusCode();
        int deleteCount = productMapper.deleteByIdAndStatus(reqVO.getId(), fromStatus);
        if (deleteCount != 1) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_CONCURRENT_MODIFIED);
        }

        writeProductStatusLog(product, ObjectActivityConstants.PRODUCT_ACTION_DELETE, fromStatus, null, reason);
        writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_DELETE, fromStatus, null, null, reason);
    }

    @VisibleForTesting
    void validateCreateReqVO(ProductSaveReqVO createReqVO) {
        validateProductCodeUnique(null, createReqVO.getCode());
        validateProductNameUnique(null, createReqVO.getName());
    }

    @VisibleForTesting
    ProductDO validateProductExists(Long id) {
        if (id == null) {
            throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
        }
        ProductDO product = productMapper.selectById(id);
        if (product == null) {
            throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
        }
        return product;
    }

    @VisibleForTesting
    void validateProductCodeUnique(Long id, String code) {
        if (!StringUtils.hasText(code)) {
            return;
        }
        String normalizedCode = code.trim();
        ProductDO product = productMapper.selectByCode(normalizedCode);
        if (product == null) {
            return;
        }
        if (id == null || !product.getId().equals(id)) {
            throw exception(ErrorCodeConstants.PRODUCT_CODE_DUPLICATE, normalizedCode);
        }
    }

    @VisibleForTesting
    void validateProductNameUnique(Long id, String name) {
        String normalizedName = name.trim();
        ProductDO product = productMapper.selectByName(normalizedName);
        if (product == null) {
            return;
        }
        if (id == null || !product.getId().equals(id)) {
            throw exception(ErrorCodeConstants.PRODUCT_NAME_DUPLICATE, normalizedName);
        }
    }

    @VisibleForTesting
    void validateManagerUser(Long managerUserId) {
        adminUserApi.validateUser(managerUserId);
    }

    @VisibleForTesting
    void validateProductCodeUnchanged(ProductDO product, String code) {
        if (!StringUtils.hasText(code)) {
            return;
        }
        if (!Objects.equals(product.getCode(), code.trim())) {
            throw exception(ErrorCodeConstants.PRODUCT_CODE_NOT_MODIFIABLE);
        }
    }

    @VisibleForTesting
    void validateManagerUserUnchanged(ProductDO product, Long managerUserId) {
        if (!Objects.equals(product.getManagerUserId(), managerUserId)) {
            throw exception(ErrorCodeConstants.PRODUCT_MANAGER_NOT_MODIFIABLE);
        }
    }

    @VisibleForTesting
    void validateProductEditable(ProductDO product, ProductSaveReqVO updateReqVO) {
        validateProductEditable(product, updateReqVO.getDirectionCode(), updateReqVO.getName());
    }

    @VisibleForTesting
    void validateProductEditable(ProductDO product, String directionCode, String name) {
        ObjectStatusModelDO statusModel = validateEnabledStatusModel(product.getStatusCode());
        if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_NOT_ALLOW_EDIT);
        }
        if (!ProductObjectConstants.STATUS_PAUSED.equals(product.getStatusCode())) {
            return;
        }
        if (!Objects.equals(product.getDirectionCode(), directionCode)
                || !Objects.equals(product.getName(), name.trim())) {
            throw exception(ErrorCodeConstants.PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE);
        }
    }

    @VisibleForTesting
    ObjectStatusTransitionDO validateProductTransition(String fromStatusCode, String actionCode) {
        ObjectStatusTransitionDO transition = objectStatusTransitionMapper
                .selectByObjectTypeAndFromStatusAndAction(ProductObjectConstants.OBJECT_TYPE, fromStatusCode, actionCode);
        if (transition == null) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED,
                    statusActionTextResolver.statusName(ProductObjectConstants.OBJECT_TYPE, fromStatusCode),
                    statusActionTextResolver.actionName(ProductObjectConstants.OBJECT_TYPE, actionCode));
        }
        return transition;
    }

    @VisibleForTesting
    void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
        if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
        }
    }

    @VisibleForTesting
    ObjectStatusModelDO validateEnabledStatusModel(String statusCode) {
        ObjectStatusModelDO statusModel = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(ProductObjectConstants.OBJECT_TYPE, statusCode);
        if (statusModel == null) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return statusModel;
    }

    @VisibleForTesting
    String getInitialStatusCode() {
        ObjectStatusModelDO statusModel = objectStatusModelMapper
                .selectInitialByObjectTypeEnabled(ProductObjectConstants.OBJECT_TYPE);
        if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return statusModel.getStatusCode();
    }

    @VisibleForTesting
    void validateDeleteConfirmText(String confirmText) {
        String normalizedConfirmText = normalizeNullableText(confirmText);
        if (!Objects.equals(ProductObjectConstants.DELETE_CONFIRM_TEXT, normalizedConfirmText)) {
            throw exception(ErrorCodeConstants.PRODUCT_DELETE_CONFIRM_TEXT_INVALID);
        }
    }

    private String generateProductCode(String code) {
        String normalizedCode = normalizeNullableText(code);
        if (StringUtils.hasText(normalizedCode)) {
            validateProductCodeUnique(null, normalizedCode);
            return normalizedCode;
        }

        String year = String.valueOf(LocalDate.now().getYear());
        String codePrefix = ProductObjectConstants.CODE_PREFIX + year;
        int nextSequence = 1;
        for (ProductDO product : productMapper.selectListByCodePrefix(codePrefix)) {
            String existedCode = product.getCode();
            if (!StringUtils.hasText(existedCode) || !existedCode.matches(codePrefix + "\\d{3}")) {
                continue;
            }
            nextSequence = Integer.parseInt(existedCode.substring(codePrefix.length())) + 1;
            break;
        }
        if (nextSequence > 999) {
            throw invalidParamException("{} 年产品自动编码序号已用尽", year);
        }
        String generatedCode = codePrefix + String.format("%03d", nextSequence);
        validateProductCodeUnique(null, generatedCode);
        return generatedCode;
    }

    private void initDefaultRequirementModule(ProductDO product) {
        com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO module =
                new com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO();
        module.setParentId(0L);
        module.setProductId(product.getId());
        module.setModuleName("全部需求");
        module.setRemark("自动创建的模块");
        module.setSort(0);
        requirementModuleMapper.insert(module);
    }

    private void initManagerMemberRelation(ProductDO product) {
        ObjectRoleRespDTO managerRole = objectPermissionApi
                .getObjectRoleByCode(ProductObjectConstants.MANAGER_ROLE_CODE, ObjectRoleConstants.ROLE_SCOPE_OBJECT,
                        ProductObjectConstants.OBJECT_TYPE)
                .getCheckedData();
        if (managerRole == null) {
            throw invalidParamException("未找到产品经理对象角色配置：{}", ProductObjectConstants.MANAGER_ROLE_CODE);
        }

        UserObjectRoleDO member = new UserObjectRoleDO();
        member.setUserId(product.getManagerUserId());
        member.setObjectType(ProductObjectConstants.OBJECT_TYPE);
        member.setObjectId(product.getId());
        member.setRoleId(managerRole.getId());
        member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
        member.setJoinedTime(LocalDateTime.now());
        member.setLeftTime(null);
        userObjectRoleMapper.insert(member);

        writeMemberInitAuditLog(member);
        writeManagerInitAuditLog(product.getId(), product.getManagerUserId());
    }

    private ProductContextProductRespVO buildCurrentProduct(ProductDO product) {
        return BeanUtils.toBean(product, ProductContextProductRespVO.class);
    }

    private ProductContextRoleRespVO buildCurrentRole(Long roleId, ObjectRoleRespDTO currentRole, boolean guestFlag,
                                                     List<String> additionalRoleNames) {
        ProductContextRoleRespVO roleRespVO = new ProductContextRoleRespVO();
        roleRespVO.setRoleId(roleId);
        roleRespVO.setGuestFlag(guestFlag);
        if (currentRole != null) {
            roleRespVO.setRoleCode(currentRole.getCode());
            roleRespVO.setRoleName(currentRole.getName());
        }
        roleRespVO.setAdditionalRoleNames(additionalRoleNames == null ? Collections.emptyList() : additionalRoleNames);
        return roleRespVO;
    }

    private List<ProductContextNavRespVO> buildContextNavs(List<ObjectMenuRespDTO> menus) {
        if (menus.isEmpty()) {
            return Collections.emptyList();
        }

        List<ProductContextNavRespVO> navs = menus.stream()
                .filter(menu -> !MenuTypeEnum.BUTTON.getType().equals(menu.getType()))
                .filter(menu -> !Boolean.FALSE.equals(menu.getVisible()))
                .map(menu -> {
                    ProductContextNavRespVO nav = new ProductContextNavRespVO();
                    nav.setId(menu.getId());
                    nav.setName(menu.getName());
                    nav.setPath(menu.getPath());
                    nav.setIcon(menu.getIcon());
                    nav.setSort(menu.getSort());
                    return nav;
                })
                .collect(Collectors.toCollection(ArrayList::new));

        navs.sort(Comparator.comparing(ProductContextNavRespVO::getSort, Comparator.nullsLast(Integer::compareTo))
                .thenComparing(ProductContextNavRespVO::getId, Comparator.nullsLast(Long::compareTo)));
        return navs;
    }

    private List<String> buildContextButtons(List<ObjectMenuRespDTO> menus) {
        if (menus.isEmpty()) {
            return Collections.emptyList();
        }
        return menus.stream()
                .filter(menu -> MenuTypeEnum.BUTTON.getType().equals(menu.getType()))
                .map(ObjectMenuRespDTO::getPermission)
                .filter(StringUtils::hasText)
                .map(String::trim)
                .distinct()
                .sorted()
                .collect(Collectors.toList());
    }

    private Map<String, Long> buildProductStatusCounts(List<ObjectStatusModelDO> statusModels,
                                                       List<Map<String, Object>> rows) {
        Map<String, Long> statusCounts = new LinkedHashMap<>();
        if (statusModels == null || statusModels.isEmpty()) {
            return statusCounts;
        }
        statusModels.stream()
                .filter(statusModel -> Objects.equals(statusModel.getStatus(), 0))
                .filter(statusModel -> StringUtils.hasText(statusModel.getStatusCode()))
                .sorted(Comparator.comparing(ObjectStatusModelDO::getSort, Comparator.nullsLast(Integer::compareTo))
                        .thenComparing(ObjectStatusModelDO::getStatusCode))
                .forEach(statusModel -> statusCounts.put(statusModel.getStatusCode(), 0L));
        if (rows == null || rows.isEmpty()) {
            return statusCounts;
        }
        for (Map<String, Object> row : rows) {
            Object statusValue = row.getOrDefault("statusCode", row.get("status_code"));
            String statusCode = Objects.toString(statusValue, null);
            if (!statusCounts.containsKey(statusCode)) {
                continue;
            }
            Object countValue = row.getOrDefault("countValue", row.get("count_value"));
            if (countValue instanceof Number number) {
                statusCounts.put(statusCode, number.longValue());
            }
        }
        return statusCounts;
    }

    /**
     * 状态看板项：清单由状态机启用状态运行时推导，过滤/排序口径与 buildProductStatusCounts 一致（sort 升序）。
     * 产品域当前无「全部」视图，口径与项目域保持同构（无排除项），includeInAll 恒为 true（设计来源：
     * docs/superpowers/specs/2026-06-11-项目产品列表状态看板动态化-design.md 第二节）。
     */
    private List<ProductOverviewSummaryRespVO.StatusBoardItemVO> buildStatusBoardItems(
            List<ObjectStatusModelDO> statusModels, Map<String, Long> statusCounts) {
        return statusModels.stream()
                .filter(statusModel -> Objects.equals(statusModel.getStatus(), 0))
                .filter(statusModel -> StringUtils.hasText(statusModel.getStatusCode()))
                .sorted(Comparator.comparing(ObjectStatusModelDO::getSort, Comparator.nullsLast(Integer::compareTo))
                        .thenComparing(ObjectStatusModelDO::getStatusCode))
                .map(statusModel -> {
                    ProductOverviewSummaryRespVO.StatusBoardItemVO item = new ProductOverviewSummaryRespVO.StatusBoardItemVO();
                    item.setStatusCode(statusModel.getStatusCode());
                    item.setStatusName(statusModel.getStatusName());
                    item.setCount(statusCounts.getOrDefault(statusModel.getStatusCode(), 0L));
                    item.setSort(statusModel.getSort());
                    item.setTerminal(statusModel.getTerminalFlag());
                    item.setIncludeInAll(true);
                    return item;
                })
                .toList();
    }

    private void writeProductStatusLog(ProductDO product, String actionType, String fromStatus,
                                       String toStatus, String reason) {
        ProductStatusLogDO statusLog = new ProductStatusLogDO();
        statusLog.setProductId(product.getId());
        statusLog.setActionType(actionType);
        statusLog.setFromStatus(fromStatus);
        statusLog.setToStatus(toStatus);
        statusLog.setReason(defaultText(reason));
        statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        statusLog.setProductCodeSnapshot(product.getCode());
        statusLog.setProductNameSnapshot(product.getName());
        productStatusLogMapper.insert(statusLog);
    }

    private void writeBizAuditLog(ProductDO product, String actionType, String fromStatus, String toStatus,
                                  String fieldChanges, String reason) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(ProductObjectConstants.OBJECT_TYPE);
        auditLog.setBizId(product.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 void writeMemberInitAuditLog(UserObjectRoleDO member) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(ObjectActivityConstants.MEMBER_BIZ_TYPE);
        auditLog.setBizId(member.getId());
        auditLog.setActionType(ObjectActivityConstants.MEMBER_ACTION_ADD);
        auditLog.setFieldChanges(buildMemberFieldChanges(member));
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private void writeManagerInitAuditLog(Long productId, Long managerUserId) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(ProductObjectConstants.OBJECT_TYPE);
        auditLog.setBizId(productId);
        auditLog.setActionType(ObjectActivityConstants.PRODUCT_ACTION_CHANGE_MANAGER);
        auditLog.setFieldChanges(buildManagerFieldChanges(null, managerUserId));
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private ProductDO cloneProduct(ProductDO source) {
        ProductDO target = new ProductDO();
        target.setId(source.getId());
        target.setCode(source.getCode());
        target.setDirectionCode(source.getDirectionCode());
        target.setStatusCode(source.getStatusCode());
        target.setName(source.getName());
        target.setManagerUserId(source.getManagerUserId());
        target.setDescription(source.getDescription());
        target.setLastStatusReason(source.getLastStatusReason());
        return target;
    }

    private void applyProductBaseInfoUpdate(ProductDO product, String directionCode, String name, String description) {
        validateProductEditable(product, directionCode, name);
        validateProductNameUnique(product.getId(), name);

        ProductDO before = cloneProduct(product);
        product.setDirectionCode(directionCode);
        product.setName(name.trim());
        product.setDescription(normalizeNullableText(description));
        productMapper.updateById(product);

        writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_UPDATE, before.getStatusCode(), product.getStatusCode(),
                buildProductFieldChanges(before, product), null);
    }

    private String buildProductFieldChanges(ProductDO before, ProductDO after) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "code", valueOf(before, ProductDO::getCode), valueOf(after, ProductDO::getCode));
        appendFieldChange(fieldChanges, "directionCode", valueOf(before, ProductDO::getDirectionCode),
                valueOf(after, ProductDO::getDirectionCode));
        appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProductDO::getStatusCode),
                valueOf(after, ProductDO::getStatusCode));
        appendFieldChange(fieldChanges, "name", valueOf(before, ProductDO::getName), valueOf(after, ProductDO::getName));
        appendFieldChange(fieldChanges, "managerUserId", valueOf(before, ProductDO::getManagerUserId),
                valueOf(after, ProductDO::getManagerUserId));
        appendFieldChange(fieldChanges, "description", valueOf(before, ProductDO::getDescription),
                valueOf(after, ProductDO::getDescription));
        appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, ProductDO::getLastStatusReason),
                valueOf(after, ProductDO::getLastStatusReason));
        return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
    }

    private String buildMemberFieldChanges(UserObjectRoleDO member) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "userId", null, member.getUserId());
        appendFieldChange(fieldChanges, "roleId", null, member.getRoleId());
        appendFieldChange(fieldChanges, "status", null, member.getStatus());
        appendFieldChange(fieldChanges, "joinedTime", null, member.getJoinedTime());
        appendFieldChange(fieldChanges, "leftTime", null, member.getLeftTime());
        appendFieldChange(fieldChanges, "remark", null, member.getRemark());
        return JsonUtils.toJsonString(fieldChanges);
    }

    private String buildManagerFieldChanges(Long beforeManagerUserId, Long afterManagerUserId) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "managerUserId", beforeManagerUserId, afterManagerUserId);
        return JsonUtils.toJsonString(fieldChanges);
    }

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

    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 : "";
    }

}
