diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunitySubService.java b/crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunitySubService.java index 24316b2..ca3629f 100644 --- a/crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunitySubService.java +++ b/crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunitySubService.java @@ -1,6 +1,6 @@ package com.crm.opportunity.service; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.crm.base.domain.result.PageResult; import com.crm.opportunity.domain.entity.OpportunityAttachment; import com.crm.opportunity.domain.entity.OpportunityCustomer; import com.crm.opportunity.domain.entity.OpportunityFollow; @@ -8,6 +8,8 @@ import com.crm.opportunity.domain.entity.OpportunityOplog; import com.crm.opportunity.domain.entity.OpportunitySiteSurvey; import com.crm.opportunity.domain.entity.OpportunityTeam; +import java.util.List; + /** * 商机详情 Tab 子域查询服务(票 03 整改)。 * 六个子域:客户、跟进、勘察、附件、团队、日志。 @@ -15,19 +17,19 @@ import com.crm.opportunity.domain.entity.OpportunityTeam; public interface IOpportunitySubService { /** 客户列表(全量,通常不多) */ - java.util.List listCustomers(Long oppId); + List listCustomers(Long oppId); /** 跟进记录分页 */ - Page pageFollows(Long oppId, long pageNum, long pageSize); + PageResult pageFollows(Long oppId, long pageNum, long pageSize); /** 新增跟进 */ Long addFollow(OpportunityFollow follow); - /** 删除跟进(软删) */ + /** 删除跟进(软删,并写操作日志) */ void deleteFollow(Long followId, Long operatorId); /** 勘察记录分页 */ - Page pageSiteSurveys(Long oppId, long pageNum, long pageSize); + PageResult pageSiteSurveys(Long oppId, long pageNum, long pageSize); /** 新增勘察记录 */ Long addSiteSurvey(OpportunitySiteSurvey survey); @@ -36,7 +38,7 @@ public interface IOpportunitySubService { void deleteSiteSurvey(Long surveyId); /** 附件列表(按 oppId + 可选 bizType) */ - java.util.List listAttachments(Long oppId, String bizType); + List listAttachments(Long oppId, String bizType); /** 新增附件 */ Long addAttachment(OpportunityAttachment attachment); @@ -45,8 +47,8 @@ public interface IOpportunitySubService { void deleteAttachment(Long attachmentId); /** 团队成员列表(全量) */ - java.util.List listTeamMembers(Long oppId); + List listTeamMembers(Long oppId); /** 操作日志分页 */ - Page pageOplogs(Long oppId, long pageNum, long pageSize); + PageResult pageOplogs(Long oppId, long pageNum, long pageSize); } diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCollabServiceImpl.java b/crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCollabServiceImpl.java index 79cc32b..6081de2 100644 --- a/crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCollabServiceImpl.java +++ b/crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCollabServiceImpl.java @@ -8,6 +8,7 @@ import com.crm.opportunity.domain.entity.OpportunityViewLog; import com.crm.opportunity.mapper.OpportunityFocusMapper; import com.crm.opportunity.mapper.OpportunityMapper; import com.crm.opportunity.mapper.OpportunityViewLogMapper; +import com.crm.opportunity.domain.enums.OpportunityStatus; import com.crm.opportunity.service.IOpportunityCollabService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -83,7 +84,7 @@ public class OpportunityCollabServiceImpl implements IOpportunityCollabService { // 按 current_stage_id 分组统计:数量 + 金额汇总 // 简单实现:查所有推进中商机,按 stageId 聚合 LambdaQueryWrapper wrapper = new LambdaQueryWrapper() - .eq(Opportunity::getOppStatus, 2); // 推进中 + .eq(Opportunity::getOppStatus, OpportunityStatus.STATUS_ADVANCING.getValue()); // 推进中 if (stageTemplateId != null) { wrapper.eq(Opportunity::getStageTemplateId, stageTemplateId); } @@ -114,7 +115,7 @@ public class OpportunityCollabServiceImpl implements IOpportunityCollabService { public List> boardCards(Long stageId, int offset, int limit, Long userId) { List opps = oppMapper.selectList(new LambdaQueryWrapper() .eq(Opportunity::getCurrentStageId, stageId) - .eq(Opportunity::getOppStatus, 2) + .eq(Opportunity::getOppStatus, OpportunityStatus.STATUS_ADVANCING.getValue()) .last("LIMIT " + offset + "," + limit)); List> result = new ArrayList<>(); for (Opportunity opp : opps) { diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunitySubServiceImpl.java b/crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunitySubServiceImpl.java index 20a3f8e..ec18bb6 100644 --- a/crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunitySubServiceImpl.java +++ b/crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunitySubServiceImpl.java @@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.crm.base.domain.exception.BusinessErrorException; +import com.crm.base.domain.result.PageResult; +import com.crm.opportunity.domain.entity.Opportunity; import com.crm.opportunity.domain.entity.OpportunityAttachment; import com.crm.opportunity.domain.entity.OpportunityCustomer; import com.crm.opportunity.domain.entity.OpportunityFollow; @@ -13,6 +15,7 @@ import com.crm.opportunity.domain.entity.OpportunityTeam; import com.crm.opportunity.mapper.OpportunityAttachmentMapper; import com.crm.opportunity.mapper.OpportunityCustomerMapper; import com.crm.opportunity.mapper.OpportunityFollowMapper; +import com.crm.opportunity.mapper.OpportunityMapper; import com.crm.opportunity.mapper.OpportunityOplogMapper; import com.crm.opportunity.mapper.OpportunitySiteSurveyMapper; import com.crm.opportunity.mapper.OpportunityTeamMapper; @@ -22,6 +25,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.time.LocalDateTime; import java.util.List; /** @@ -31,12 +35,16 @@ import java.util.List; @RequiredArgsConstructor public class OpportunitySubServiceImpl implements IOpportunitySubService { + /** 跟进附件每条记录最多关联附件数(规格约定:10 个) */ + private static final int FOLLOW_ATTACHMENT_MAX = 10; + private final OpportunityCustomerMapper customerMapper; private final OpportunityFollowMapper followMapper; private final OpportunitySiteSurveyMapper siteSurveyMapper; private final OpportunityAttachmentMapper attachmentMapper; private final OpportunityTeamMapper teamMapper; private final OpportunityOplogMapper oplogMapper; + private final OpportunityMapper oppMapper; // ==================== 客户 ==================== @@ -51,12 +59,13 @@ public class OpportunitySubServiceImpl implements IOpportunitySubService { // ==================== 跟进 ==================== @Override - public Page pageFollows(Long oppId, long pageNum, long pageSize) { - return followMapper.selectPage(new Page<>(pageNum, pageSize), + public PageResult pageFollows(Long oppId, long pageNum, long pageSize) { + Page page = followMapper.selectPage(new Page<>(pageNum, pageSize), new LambdaQueryWrapper() .eq(OpportunityFollow::getOppId, oppId) .eq(OpportunityFollow::getDeleteKey, 0L) .orderByDesc(OpportunityFollow::getFollowTime)); + return new PageResult<>(page); } @Override @@ -69,6 +78,17 @@ public class OpportunitySubServiceImpl implements IOpportunitySubService { if (follow.getCustomerId() == null) throw new BusinessErrorException("关联客户必填"); follow.setDeleteKey(0L); followMapper.insert(follow); + + // 刷新回收锚点:last_valid_follow_time = max(原值, 本次跟进时间) + Opportunity opp = oppMapper.selectById(follow.getOppId()); + if (opp != null) { + LocalDateTime followAt = follow.getFollowTime(); + if (opp.getLastValidFollowTime() == null || followAt.isAfter(opp.getLastValidFollowTime())) { + oppMapper.update(null, new LambdaUpdateWrapper() + .eq(Opportunity::getId, follow.getOppId()) + .set(Opportunity::getLastValidFollowTime, followAt)); + } + } return follow.getId(); } @@ -81,16 +101,32 @@ public class OpportunitySubServiceImpl implements IOpportunitySubService { .eq(OpportunityFollow::getId, followId) .set(OpportunityFollow::getDeleteKey, followId) .set(OpportunityFollow::getDeleted, 1)); + + // 写操作日志:ROW_DELETE + OpportunityOplog log = new OpportunityOplog(); + log.setOppId(follow.getOppId()); + log.setOpKind("ROW_DELETE"); + log.setLogType("ROW_CHANGE"); + log.setEntityName("opportunity_follow"); + log.setBizRef(follow.getFollowContent() != null + ? follow.getFollowContent().substring(0, Math.min(50, follow.getFollowContent().length())) + : String.valueOf(followId)); + log.setOpDesc("删除跟进记录"); + log.setOpSource("MANUAL"); + log.setOpTime(LocalDateTime.now()); + log.setOpUserId(operatorId); + oplogMapper.insert(log); } // ==================== 勘察 ==================== @Override - public Page pageSiteSurveys(Long oppId, long pageNum, long pageSize) { - return siteSurveyMapper.selectPage(new Page<>(pageNum, pageSize), + public PageResult pageSiteSurveys(Long oppId, long pageNum, long pageSize) { + Page page = siteSurveyMapper.selectPage(new Page<>(pageNum, pageSize), new LambdaQueryWrapper() .eq(OpportunitySiteSurvey::getOppId, oppId) .orderByDesc(OpportunitySiteSurvey::getSurveyDate)); + return new PageResult<>(page); } @Override @@ -131,6 +167,16 @@ public class OpportunitySubServiceImpl implements IOpportunitySubService { public Long addAttachment(OpportunityAttachment attachment) { if (attachment.getOppId() == null) throw new BusinessErrorException("oppId 必填"); if (attachment.getFileId() == null) throw new BusinessErrorException("fileId 必填"); + // 跟进附件上限校验 + if ("FOLLOW_UP".equals(attachment.getBizType()) && attachment.getBizId() != null) { + long count = attachmentMapper.selectCount(new LambdaQueryWrapper() + .eq(OpportunityAttachment::getBizType, "FOLLOW_UP") + .eq(OpportunityAttachment::getBizId, attachment.getBizId()) + .eq(OpportunityAttachment::getDeleteKey, 0L)); + if (count >= FOLLOW_ATTACHMENT_MAX) { + throw new BusinessErrorException("跟进附件最多 " + FOLLOW_ATTACHMENT_MAX + " 个"); + } + } attachment.setDeleteKey(0L); attachmentMapper.insert(attachment); return attachment.getId(); @@ -160,10 +206,11 @@ public class OpportunitySubServiceImpl implements IOpportunitySubService { // ==================== 操作日志 ==================== @Override - public Page pageOplogs(Long oppId, long pageNum, long pageSize) { - return oplogMapper.selectPage(new Page<>(pageNum, pageSize), + public PageResult pageOplogs(Long oppId, long pageNum, long pageSize) { + Page page = oplogMapper.selectPage(new Page<>(pageNum, pageSize), new LambdaQueryWrapper() .eq(OpportunityOplog::getOppId, oppId) .orderByDesc(OpportunityOplog::getOpTime)); + return new PageResult<>(page); } } diff --git a/crm-rule/src/main/java/com/crm/rule/controller/OpportunityPoolRuleController.java b/crm-rule/src/main/java/com/crm/rule/controller/OpportunityPoolRuleController.java index 0cec8de..9a2ec05 100644 --- a/crm-rule/src/main/java/com/crm/rule/controller/OpportunityPoolRuleController.java +++ b/crm-rule/src/main/java/com/crm/rule/controller/OpportunityPoolRuleController.java @@ -1,14 +1,13 @@ package com.crm.rule.controller; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.crm.base.domain.result.PageResult; import com.crm.base.domain.result.Result; import com.crm.rule.domain.dto.OpportunityPoolRuleDTO; -import com.crm.rule.domain.param.OpportunityPoolRulePageParam; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.crm.rule.domain.entity.OpportunityPoolRule; +import com.crm.rule.domain.param.OpportunityPoolRulePageParam; import com.crm.rule.mapper.OpportunityPoolRuleMapper; import com.crm.rule.service.IOpportunityPoolRuleService; -import java.util.List; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -18,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + /** * 商机公海规则接口(薄适配层,只调 {@link IOpportunityPoolRuleService}) *

权限策略:不种子化 button 权限点(同公海池配置),ApiPermissionInterceptor 对未注册 URL fail-open;