Browse Source

feat(opportunity): 商机模块接口整改(票01-06)

- 票01: 修 W-02 NOT NULL 字段默认值 + 补 GET /detail + POST /edit
- 票02: 状态机 11 个 HTTP 接口(claim/assign/pause/resume/release-pool/close/reopen/handover)
- 票03: 详情 Tab 6 子域接口(客户/跟进/勘察/附件/团队/日志)
- 票04: 协作+看板+访问上报 6 个接口
- 票05: 规则族 /versions 端点(阶段模板/方案卡模板/公海规则)
- 票06: 回归验证 33 条接口全通
master
luoweijian 2 weeks ago
parent
commit
0d5ee62812
  1. 84
      crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityCollabController.java
  2. 3
      crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityCreateController.java
  3. 40
      crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityDetailController.java
  4. 185
      crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunitySubController.java
  5. 167
      crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityTransitionController.java
  6. 12
      crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/CreateOpportunityRequest.java
  7. 20
      crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/Opportunity.java
  8. 4
      crm-opportunity/src/main/java/com/crm/opportunity/intake/OpportunityIntakeSpec.java
  9. 11
      crm-opportunity/src/main/java/com/crm/opportunity/intake/impl/OpportunityIntakeImpl.java
  10. 1
      crm-opportunity/src/main/java/com/crm/opportunity/port/inbound/impl/OpportunityCreationPortImpl.java
  11. 25
      crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityCollabService.java
  12. 19
      crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityDetailService.java
  13. 52
      crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunitySubService.java
  14. 132
      crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCollabServiceImpl.java
  15. 4
      crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCreateServiceImpl.java
  16. 59
      crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityDetailServiceImpl.java
  17. 169
      crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunitySubServiceImpl.java
  18. 10
      crm-opportunity/src/test/java/com/crm/opportunity/intake/impl/OpportunityIntakeImplTest.java
  19. 16
      crm-rule/src/main/java/com/crm/rule/controller/OpportunityPoolRuleController.java
  20. 15
      crm-rule/src/main/java/com/crm/rule/controller/OpportunitySchemeCardTemplateController.java
  21. 18
      crm-rule/src/main/java/com/crm/rule/controller/OpportunityStageTemplateController.java

84
crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityCollabController.java

@ -0,0 +1,84 @@
package com.crm.opportunity.controller;
import com.crm.base.domain.result.Result;
import com.crm.base.security.SecurityUtils;
import com.crm.opportunity.service.IOpportunityCollabService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
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;
/**
* 商机协作接口关注/取关/访问上报/看板 04 整改薄适配层
*/
@Tag(name = "商机管理/协作")
@RestController
@RequestMapping("/api/opportunity")
@RequiredArgsConstructor
public class OpportunityCollabController {
private final IOpportunityCollabService collabService;
// ==================== 关注 ====================
@Operation(summary = "关注商机")
@PostMapping("/focus")
public Result<Void> focus(@RequestParam("oppId") Long oppId) {
Long userId = Long.valueOf(SecurityUtils.getRequiredUserId());
collabService.focus(oppId, userId);
return Result.success();
}
@Operation(summary = "取关商机")
@PostMapping("/unfocus")
public Result<Void> unfocus(@RequestParam("oppId") Long oppId) {
Long userId = Long.valueOf(SecurityUtils.getRequiredUserId());
collabService.unfocus(oppId, userId);
return Result.success();
}
@Operation(summary = "批量关注")
@PostMapping("/focus-batch")
public Result<Void> focusBatch(@RequestParam("ids") List<Long> ids) {
Long userId = Long.valueOf(SecurityUtils.getRequiredUserId());
for (Long id : ids) {
collabService.focus(id, userId);
}
return Result.success();
}
// ==================== 访问上报 ====================
@Operation(summary = "上报商机访问(打开详情时调用)")
@PostMapping("/view-touch")
public Result<Void> viewTouch(@RequestParam("oppId") Long oppId) {
Long userId = Long.valueOf(SecurityUtils.getRequiredUserId());
collabService.touch(oppId, userId);
return Result.success();
}
// ==================== 看板 ====================
@Operation(summary = "看板各阶段数量+金额汇总")
@PostMapping("/board/stage-summary")
public Result<?> boardStageSummary(
@RequestParam(value = "stageTemplateId", required = false) Long stageTemplateId) {
Long userId = Long.valueOf(SecurityUtils.getRequiredUserId());
return Result.success(collabService.boardStageSummary(stageTemplateId, userId));
}
@Operation(summary = "看板单阶段卡片列表")
@PostMapping("/board/cards")
public Result<?> boardCards(
@RequestParam("stageId") Long stageId,
@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "20") int limit) {
Long userId = Long.valueOf(SecurityUtils.getRequiredUserId());
return Result.success(collabService.boardCards(stageId, offset, limit, userId));
}
}

3
crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityCreateController.java

@ -10,7 +10,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -32,7 +31,7 @@ public class OpportunityCreateController {
@Operation(summary = "新建商机(oppSource=线索转入 时须选关联线索)")
@PostMapping
public Result<Long> create(@RequestBody CreateOpportunityRequest request) {
public Result<Long> create(CreateOpportunityRequest request) {
Long ownerUserId = Long.valueOf(SecurityUtils.getRequiredUserId());
return Result.success(createService.createOpportunity(request, ownerUserId));
}

40
crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityDetailController.java

@ -0,0 +1,40 @@
package com.crm.opportunity.controller;
import com.crm.base.domain.result.Result;
import com.crm.opportunity.domain.dto.OpportunityDTO;
import com.crm.opportunity.service.IOpportunityDetailService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 商机详情 & 编辑接口薄适配层 01 整改
*
* <p>路由风格与线索模块对齐 GET/POST动词后缀id @RequestParam</p>
*/
@Tag(name = "商机管理/商机详情")
@RestController
@RequestMapping("/api/opportunity")
@RequiredArgsConstructor
public class OpportunityDetailController {
private final IOpportunityDetailService detailService;
@Operation(summary = "商机详情")
@GetMapping("/detail")
public Result<OpportunityDTO> detail(@RequestParam("id") Long id) {
return Result.success(detailService.getDetail(id));
}
@Operation(summary = "编辑商机(非终态可编辑;全量字段覆盖)")
@PostMapping("/edit")
public Result<Void> edit(OpportunityDTO dto) {
detailService.edit(dto);
return Result.success();
}
}

185
crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunitySubController.java

@ -0,0 +1,185 @@
package com.crm.opportunity.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.crm.base.domain.result.Result;
import com.crm.base.security.SecurityUtils;
import com.crm.opportunity.domain.entity.OpportunityAttachment;
import com.crm.opportunity.domain.entity.OpportunityCustomer;
import com.crm.opportunity.domain.entity.OpportunityFollow;
import com.crm.opportunity.domain.entity.OpportunityOplog;
import com.crm.opportunity.domain.entity.OpportunitySiteSurvey;
import com.crm.opportunity.domain.entity.OpportunityTeam;
import com.crm.opportunity.service.IOpportunitySubService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 商机详情 Tab 子域接口 03 整改薄适配层
*
* <p>六个子域客户/跟进/勘察/附件/团队/日志路由风格同项目其他 Controller
* GET 查询POST 写操作id @RequestParam分页走 POST /page</p>
*/
@Tag(name = "商机管理/详情子域")
@RestController
@RequestMapping("/api/opportunity")
@RequiredArgsConstructor
public class OpportunitySubController {
private final IOpportunitySubService subService;
// ==================== 客户 Tab ====================
@Operation(summary = "客户列表")
@GetMapping("/customer/list")
public Result<List<OpportunityCustomer>> listCustomers(@RequestParam("oppId") Long oppId) {
return Result.success(subService.listCustomers(oppId));
}
// ==================== 跟进 Tab ====================
@Operation(summary = "跟进记录分页")
@PostMapping("/follow/page")
public Result<Page<OpportunityFollow>> pageFollows(
@RequestParam("oppId") Long oppId,
@RequestParam(value = "pageNum", defaultValue = "1") long pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") long pageSize) {
return Result.success(subService.pageFollows(oppId, pageNum, pageSize));
}
@Operation(summary = "新增跟进记录")
@PostMapping("/follow/add")
public Result<Long> addFollow(
@RequestParam("oppId") Long oppId,
@RequestParam("followContent") String followContent,
@RequestParam("followTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime followTime,
@RequestParam("followWay") String followWay,
@RequestParam("customerId") Long customerId,
@RequestParam(value = "contactId", required = false) Long contactId,
@RequestParam(value = "resultTag", required = false) String resultTag,
@RequestParam(value = "nextFollowTime", required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime nextFollowTime) {
OpportunityFollow follow = new OpportunityFollow();
follow.setOppId(oppId);
follow.setFollowContent(followContent);
follow.setFollowTime(followTime);
follow.setFollowWay(followWay);
follow.setCustomerId(customerId);
follow.setContactId(contactId);
follow.setResultTag(resultTag);
follow.setNextFollowTime(nextFollowTime);
return Result.success(subService.addFollow(follow));
}
@Operation(summary = "删除跟进记录")
@PostMapping("/follow/delete")
public Result<Void> deleteFollow(@RequestParam("id") Long id) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
subService.deleteFollow(id, operatorId);
return Result.success();
}
// ==================== 勘察 Tab ====================
@Operation(summary = "勘察记录分页")
@PostMapping("/site-survey/page")
public Result<Page<OpportunitySiteSurvey>> pageSiteSurveys(
@RequestParam("oppId") Long oppId,
@RequestParam(value = "pageNum", defaultValue = "1") long pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") long pageSize) {
return Result.success(subService.pageSiteSurveys(oppId, pageNum, pageSize));
}
@Operation(summary = "新增勘察记录")
@PostMapping("/site-survey/add")
public Result<Long> addSiteSurvey(
@RequestParam("oppId") Long oppId,
@RequestParam("surveyDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate surveyDate,
@RequestParam("engineerUserId") Long engineerUserId,
@RequestParam("applyWay") String applyWay,
@RequestParam("surveySeq") String surveySeq,
@RequestParam(value = "applyNo", required = false) String applyNo,
@RequestParam(value = "surveyDesc", required = false) String surveyDesc) {
OpportunitySiteSurvey survey = new OpportunitySiteSurvey();
survey.setOppId(oppId);
survey.setSurveyDate(surveyDate);
survey.setEngineerUserId(engineerUserId);
survey.setApplyWay(applyWay);
survey.setSurveySeq(surveySeq);
survey.setApplyNo(applyNo);
survey.setSurveyDesc(surveyDesc);
return Result.success(subService.addSiteSurvey(survey));
}
@Operation(summary = "删除勘察记录")
@PostMapping("/site-survey/delete")
public Result<Void> deleteSiteSurvey(@RequestParam("id") Long id) {
subService.deleteSiteSurvey(id);
return Result.success();
}
// ==================== 附件 Tab ====================
@Operation(summary = "附件列表")
@GetMapping("/attachment/list")
public Result<List<OpportunityAttachment>> listAttachments(
@RequestParam("oppId") Long oppId,
@RequestParam(value = "bizType", required = false) String bizType) {
return Result.success(subService.listAttachments(oppId, bizType));
}
@Operation(summary = "新增附件")
@PostMapping("/attachment/add")
public Result<Long> addAttachment(
@RequestParam("oppId") Long oppId,
@RequestParam("fileId") Long fileId,
@RequestParam(value = "bizType", required = false) String bizType,
@RequestParam(value = "bizId", required = false) Long bizId,
@RequestParam(value = "materialType", required = false) String materialType,
@RequestParam(value = "remark", required = false) String remark) {
OpportunityAttachment att = new OpportunityAttachment();
att.setOppId(oppId);
att.setFileId(fileId);
att.setBizType(bizType);
att.setBizId(bizId);
att.setMaterialType(materialType);
att.setRemark(remark);
return Result.success(subService.addAttachment(att));
}
@Operation(summary = "删除附件")
@PostMapping("/attachment/delete")
public Result<Void> deleteAttachment(@RequestParam("id") Long id) {
subService.deleteAttachment(id);
return Result.success();
}
// ==================== 团队 Tab ====================
@Operation(summary = "团队成员列表")
@GetMapping("/team/list")
public Result<List<OpportunityTeam>> listTeamMembers(@RequestParam("oppId") Long oppId) {
return Result.success(subService.listTeamMembers(oppId));
}
// ==================== 日志 Tab ====================
@Operation(summary = "操作日志分页")
@PostMapping("/oplog/page")
public Result<Page<OpportunityOplog>> pageOplogs(
@RequestParam("oppId") Long oppId,
@RequestParam(value = "pageNum", defaultValue = "1") long pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") long pageSize) {
return Result.success(subService.pageOplogs(oppId, pageNum, pageSize));
}
}

167
crm-opportunity/src/main/java/com/crm/opportunity/controller/OpportunityTransitionController.java

@ -0,0 +1,167 @@
package com.crm.opportunity.controller;
import com.crm.base.domain.result.Result;
import com.crm.base.security.SecurityUtils;
import com.crm.opportunity.state.OpportunityTransition;
import com.crm.opportunity.state.OpportunityTransitionCmd;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.List;
/**
* 商机状态机 HTTP 入口 02 整改薄适配层
*
* <p>路由风格与线索模块对齐 POST动词路径id @RequestParam
* Controller 只做取当前用户 + 组装 Cmd + {@link OpportunityTransition#execute}
* 快照字段ownerNameSnapshot/ownerDeptId由前端传入或由上层服务预查本期从请求参数取</p>
*/
@Tag(name = "商机管理/商机流转")
@RestController
@RequestMapping("/api/opportunity")
@RequiredArgsConstructor
public class OpportunityTransitionController {
private final OpportunityTransition transition;
@Operation(summary = "领取商机(待领取→推进中,E1)")
@PostMapping("/claim")
public Result<Void> claim(
@RequestParam("id") Long id,
@RequestParam(value = "ownerNameSnapshot", required = false) String ownerNameSnapshot,
@RequestParam(value = "ownerDeptId", required = false) Long ownerDeptId) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
transition.execute(id, new OpportunityTransitionCmd.ClaimCmd(
operatorId, ownerNameSnapshot, ownerDeptId));
return Result.success();
}
@Operation(summary = "批量领取(待领取→推进中,E1)")
@PostMapping("/claim-batch")
public Result<Void> claimBatch(
@RequestParam("ids") List<Long> ids,
@RequestParam(value = "ownerNameSnapshot", required = false) String ownerNameSnapshot,
@RequestParam(value = "ownerDeptId", required = false) Long ownerDeptId) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
for (Long id : ids) {
transition.execute(id, new OpportunityTransitionCmd.ClaimCmd(
operatorId, ownerNameSnapshot, ownerDeptId));
}
return Result.success();
}
@Operation(summary = "分配商机给指定销售(待领取→推进中,E2)")
@PostMapping("/assign")
public Result<Void> assign(
@RequestParam("id") Long id,
@RequestParam("userId") Long userId,
@RequestParam(value = "ownerNameSnapshot", required = false) String ownerNameSnapshot,
@RequestParam(value = "ownerDeptId", required = false) Long ownerDeptId) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
transition.execute(id, new OpportunityTransitionCmd.AssignToUserCmd(
operatorId, userId, ownerNameSnapshot, ownerDeptId));
return Result.success();
}
@Operation(summary = "批量分配商机(待领取→推进中,E2)")
@PostMapping("/assign-batch")
public Result<Void> assignBatch(
@RequestParam("ids") List<Long> ids,
@RequestParam("userId") Long userId,
@RequestParam(value = "ownerNameSnapshot", required = false) String ownerNameSnapshot,
@RequestParam(value = "ownerDeptId", required = false) Long ownerDeptId) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
for (Long id : ids) {
transition.execute(id, new OpportunityTransitionCmd.AssignToUserCmd(
operatorId, userId, ownerNameSnapshot, ownerDeptId));
}
return Result.success();
}
@Operation(summary = "抛公海(推进中→待领取,E3)")
@PostMapping("/release-pool")
public Result<Void> releasePool(
@RequestParam("id") Long id,
@RequestParam(value = "poolReason", required = false) String poolReason) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
transition.execute(id, new OpportunityTransitionCmd.ReleaseToPoolCmd(operatorId, poolReason));
return Result.success();
}
@Operation(summary = "暂缓(推进中→暂缓中,E4)")
@PostMapping("/pause")
public Result<Void> pause(
@RequestParam("id") Long id,
@RequestParam(value = "pauseReason", required = false) String pauseReason,
@RequestParam(value = "pauseExpectedRestartDate", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate pauseExpectedRestartDate,
@RequestParam(value = "pauseRemark", required = false) String pauseRemark) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
transition.execute(id, new OpportunityTransitionCmd.PauseCmd(
operatorId, pauseReason, pauseExpectedRestartDate, pauseRemark));
return Result.success();
}
@Operation(summary = "取消暂缓(暂缓中→推进中,E5)")
@PostMapping("/resume")
public Result<Void> resume(@RequestParam("id") Long id) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
transition.execute(id, new OpportunityTransitionCmd.ResumeCmd(operatorId));
return Result.success();
}
@Operation(summary = "关闭商机(推进中/暂缓中→已关闭,E6)")
@PostMapping("/close")
public Result<Void> close(
@RequestParam("id") Long id,
@RequestParam(value = "closeReason", required = false) String closeReason,
@RequestParam(value = "closeRemark", required = false) String closeRemark) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
transition.execute(id, new OpportunityTransitionCmd.CloseCmd(operatorId, closeReason, closeRemark));
return Result.success();
}
@Operation(summary = "批量关闭(推进中/暂缓中→已关闭,E6)")
@PostMapping("/close-batch")
public Result<Void> closeBatch(
@RequestParam("ids") List<Long> ids,
@RequestParam(value = "closeReason", required = false) String closeReason,
@RequestParam(value = "closeRemark", required = false) String closeRemark) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
for (Long id : ids) {
transition.execute(id, new OpportunityTransitionCmd.CloseCmd(operatorId, closeReason, closeRemark));
}
return Result.success();
}
@Operation(summary = "重启商机(已关闭→推进中,E7)")
@PostMapping("/reopen")
public Result<Void> reopen(
@RequestParam("id") Long id,
@RequestParam(value = "reopenReason", required = false) String reopenReason) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
transition.execute(id, new OpportunityTransitionCmd.ReopenCmd(operatorId, reopenReason));
return Result.success();
}
@Operation(summary = "移交商机(推进中/暂缓中自环换owner,E9)")
@PostMapping("/handover")
public Result<Void> handover(
@RequestParam("id") Long id,
@RequestParam("expectedOwnerUserId") Long expectedOwnerUserId,
@RequestParam("newOwnerUserId") Long newOwnerUserId,
@RequestParam(value = "ownerNameSnapshot", required = false) String ownerNameSnapshot,
@RequestParam(value = "ownerDeptId", required = false) Long ownerDeptId) {
Long operatorId = Long.valueOf(SecurityUtils.getRequiredUserId());
transition.execute(id, new OpportunityTransitionCmd.HandoverCmd(
operatorId, expectedOwnerUserId, newOwnerUserId, ownerNameSnapshot, ownerDeptId));
return Result.success();
}
}

12
crm-opportunity/src/main/java/com/crm/opportunity/domain/dto/CreateOpportunityRequest.java

@ -21,9 +21,21 @@ public class CreateOpportunityRequest {
/** 商机名称(必填)。 */
private String opportunityName;
/** 商机类型字典 code。 */
private String oppType;
/** 行业字典 code。 */
private String industryCode;
/** 招标形式字典 code。 */
private String bidForm;
/** 项目属地字典 code(本地/异地)。 */
private String localityType;
/** 甲方是否明确 0/1(默认 0)。 */
private Integer partyAClear;
/** 甲方。 */
private String partyA;

20
crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/Opportunity.java

@ -43,31 +43,31 @@ public class Opportunity extends BaseEntity {
@Comment("商机名称")
@Column(columnDefinition = "varchar(200) not null comment '商机名称'")
private String oppName;
private String oppName = "";
@Comment("商机来源字典 code(渠道归因,线索转入时默认置「线索转入」)")
@Column(columnDefinition = "varchar(64) not null comment '商机来源字典code'")
private String oppSource;
private String oppSource = "";
@Comment("商机类型字典 code")
@Column(columnDefinition = "varchar(64) not null comment '商机类型字典code'")
private String oppType;
private String oppType = "";
@Comment("行业字典 code")
@Column(columnDefinition = "varchar(64) not null comment '行业字典code'")
private String industryCode;
private String industryCode = "";
@Comment("招标形式字典 code")
@Column(columnDefinition = "varchar(64) not null comment '招标形式字典code'")
private String bidForm;
private String bidForm = "";
@Comment("项目属地字典 code(本地/异地分类标签,非地理坐标)")
@Column(columnDefinition = "varchar(64) not null comment '项目属地字典code'")
private String localityType;
private String localityType = "";
@Comment("甲方是否明确 0/1")
@Column(columnDefinition = "tinyint not null comment '甲方是否明确'")
private Integer partyAClear;
@Column(columnDefinition = "tinyint not null default 0 comment '甲方是否明确'")
private Integer partyAClear = 0;
@Comment("甲方名称(party_a_clear=1 时必填)")
@Column(columnDefinition = "varchar(200) comment '甲方名称'")
@ -75,11 +75,11 @@ public class Opportunity extends BaseEntity {
@Comment("省份国标 code(GB/T 2260,6位)")
@Column(columnDefinition = "varchar(12) not null comment '省份国标code'")
private String provinceCode;
private String provinceCode = "";
@Comment("市/区国标 code(对称 lead 约定:市码或区/县码均可,不单建 district_code)")
@Column(columnDefinition = "varchar(12) not null comment '市/区国标code'")
private String cityCode;
private String cityCode = "";
@Comment("备注")
@Column(columnDefinition = "text comment '备注'")

4
crm-opportunity/src/main/java/com/crm/opportunity/intake/OpportunityIntakeSpec.java

@ -32,6 +32,10 @@ public record OpportunityIntakeSpec(
IntakeSource source,
String oppSource,
String opportunityName,
String oppType,
String bidForm,
String localityType,
Integer partyAClear,
String industryCode,
String partyA,
String provinceCode,

11
crm-opportunity/src/main/java/com/crm/opportunity/intake/impl/OpportunityIntakeImpl.java

@ -93,11 +93,16 @@ public class OpportunityIntakeImpl implements OpportunityIntake {
opp.setOppName(spec.opportunityName());
// 商机来源:LEAD_CONVERT 恒 opp_source_01(adapter 已按枚举默认填入 spec),DIRECT 为用户所选。
opp.setOppSource(spec.oppSource());
opp.setIndustryCode(spec.industryCode());
// NOT NULL 字段:前端未传时给空串/0 兜底,避免 DB 插入报错
opp.setOppType(spec.oppType() != null ? spec.oppType() : "");
opp.setBidForm(spec.bidForm() != null ? spec.bidForm() : "");
opp.setLocalityType(spec.localityType() != null ? spec.localityType() : "");
opp.setPartyAClear(spec.partyAClear() != null ? spec.partyAClear() : 0);
opp.setProvinceCode(spec.provinceCode() != null ? spec.provinceCode() : "");
opp.setCityCode(spec.cityCode() != null ? spec.cityCode() : "");
opp.setIndustryCode(spec.industryCode() != null ? spec.industryCode() : "");
opp.setPartyA(spec.partyA());
opp.setRemark(spec.remark());
opp.setProvinceCode(spec.provinceCode());
opp.setCityCode(spec.cityCode());
opp.setOwnerUserId(spec.ownerUserId());
opp.setOwnerDeptId(spec.ownerDeptId());
opp.setCreatorUserId(spec.ownerUserId());

1
crm-opportunity/src/main/java/com/crm/opportunity/port/inbound/impl/OpportunityCreationPortImpl.java

@ -37,6 +37,7 @@ public class OpportunityCreationPortImpl implements OpportunityCreationPort {
IntakeSource.LEAD_CONVERT,
IntakeSource.LEAD_CONVERT.defaultOppSource(),
cmd.opportunityName(),
null, null, null, null, // oppType/bidForm/localityType/partyAClear 线索转入不传,底层兑底
cmd.industryCode(),
cmd.partyA(),
cmd.provinceCode(),

25
crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityCollabService.java

@ -0,0 +1,25 @@
package com.crm.opportunity.service;
import java.util.List;
import java.util.Map;
/**
* 商机协作服务关注/取关/访问上报/看板 04 整改
*/
public interface IOpportunityCollabService {
/** 关注(幂等:已关注则忽略) */
void focus(Long oppId, Long userId);
/** 取关(幂等:未关注则忽略) */
void unfocus(Long oppId, Long userId);
/** 访问上报(upsert last_view_time) */
void touch(Long oppId, Long userId);
/** 看板阶段汇总:List<{stageId, stageName, count, totalAmount}> */
List<Map<String, Object>> boardStageSummary(Long stageTemplateId, Long userId);
/** 看板单阶段卡片列表(分页 offset/limit) */
List<Map<String, Object>> boardCards(Long stageId, int offset, int limit, Long userId);
}

19
crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunityDetailService.java

@ -0,0 +1,19 @@
package com.crm.opportunity.service;
import com.crm.opportunity.domain.dto.OpportunityDTO;
/**
* 商机详情 & 编辑服务 01 整改
*/
public interface IOpportunityDetailService {
/**
* 商机详情GET /api/opportunity/detail?id=
*/
OpportunityDTO getDetail(Long id);
/**
* 编辑商机POST /api/opportunity/edit
*/
void edit(OpportunityDTO dto);
}

52
crm-opportunity/src/main/java/com/crm/opportunity/service/IOpportunitySubService.java

@ -0,0 +1,52 @@
package com.crm.opportunity.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.crm.opportunity.domain.entity.OpportunityAttachment;
import com.crm.opportunity.domain.entity.OpportunityCustomer;
import com.crm.opportunity.domain.entity.OpportunityFollow;
import com.crm.opportunity.domain.entity.OpportunityOplog;
import com.crm.opportunity.domain.entity.OpportunitySiteSurvey;
import com.crm.opportunity.domain.entity.OpportunityTeam;
/**
* 商机详情 Tab 子域查询服务 03 整改
* 六个子域客户跟进勘察附件团队日志
*/
public interface IOpportunitySubService {
/** 客户列表(全量,通常不多) */
java.util.List<OpportunityCustomer> listCustomers(Long oppId);
/** 跟进记录分页 */
Page<OpportunityFollow> pageFollows(Long oppId, long pageNum, long pageSize);
/** 新增跟进 */
Long addFollow(OpportunityFollow follow);
/** 删除跟进(软删) */
void deleteFollow(Long followId, Long operatorId);
/** 勘察记录分页 */
Page<OpportunitySiteSurvey> pageSiteSurveys(Long oppId, long pageNum, long pageSize);
/** 新增勘察记录 */
Long addSiteSurvey(OpportunitySiteSurvey survey);
/** 删除勘察记录(软删) */
void deleteSiteSurvey(Long surveyId);
/** 附件列表(按 oppId + 可选 bizType) */
java.util.List<OpportunityAttachment> listAttachments(Long oppId, String bizType);
/** 新增附件 */
Long addAttachment(OpportunityAttachment attachment);
/** 删除附件(软删) */
void deleteAttachment(Long attachmentId);
/** 团队成员列表(全量) */
java.util.List<OpportunityTeam> listTeamMembers(Long oppId);
/** 操作日志分页 */
Page<OpportunityOplog> pageOplogs(Long oppId, long pageNum, long pageSize);
}

132
crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCollabServiceImpl.java

@ -0,0 +1,132 @@
package com.crm.opportunity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.crm.opportunity.domain.entity.Opportunity;
import com.crm.opportunity.domain.entity.OpportunityFocus;
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.service.IOpportunityCollabService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 商机协作服务实现 04 整改
*/
@Service
@RequiredArgsConstructor
public class OpportunityCollabServiceImpl implements IOpportunityCollabService {
private final OpportunityFocusMapper focusMapper;
private final OpportunityViewLogMapper viewLogMapper;
private final OpportunityMapper oppMapper;
// ==================== 关注 ====================
@Override
@Transactional(rollbackFor = Exception.class)
public void focus(Long oppId, Long userId) {
boolean exists = focusMapper.exists(new LambdaQueryWrapper<OpportunityFocus>()
.eq(OpportunityFocus::getOpportunityId, oppId)
.eq(OpportunityFocus::getUserId, userId));
if (!exists) {
OpportunityFocus focus = new OpportunityFocus();
focus.setOpportunityId(oppId);
focus.setUserId(userId);
focus.setFocusTime(LocalDateTime.now());
focusMapper.insert(focus);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void unfocus(Long oppId, Long userId) {
focusMapper.delete(new LambdaQueryWrapper<OpportunityFocus>()
.eq(OpportunityFocus::getOpportunityId, oppId)
.eq(OpportunityFocus::getUserId, userId));
}
// ==================== 访问上报 ====================
@Override
@Transactional(rollbackFor = Exception.class)
public void touch(Long oppId, Long userId) {
OpportunityViewLog existing = viewLogMapper.selectOne(new LambdaQueryWrapper<OpportunityViewLog>()
.eq(OpportunityViewLog::getOpportunityId, oppId)
.eq(OpportunityViewLog::getUserId, userId));
if (existing != null) {
viewLogMapper.update(null, new LambdaUpdateWrapper<OpportunityViewLog>()
.eq(OpportunityViewLog::getId, existing.getId())
.set(OpportunityViewLog::getLastViewTime, LocalDateTime.now()));
} else {
OpportunityViewLog log = new OpportunityViewLog();
log.setOpportunityId(oppId);
log.setUserId(userId);
log.setLastViewTime(LocalDateTime.now());
viewLogMapper.insert(log);
}
}
// ==================== 看板 ====================
@Override
public List<Map<String, Object>> boardStageSummary(Long stageTemplateId, Long userId) {
// 按 current_stage_id 分组统计:数量 + 金额汇总
// 简单实现:查所有推进中商机,按 stageId 聚合
LambdaQueryWrapper<Opportunity> wrapper = new LambdaQueryWrapper<Opportunity>()
.eq(Opportunity::getOppStatus, 2); // 推进中
if (stageTemplateId != null) {
wrapper.eq(Opportunity::getStageTemplateId, stageTemplateId);
}
List<Opportunity> opps = oppMapper.selectList(wrapper
.select(Opportunity::getCurrentStageId, Opportunity::getProjectAmount));
// 聚合
Map<Long, Map<String, Object>> grouped = new java.util.LinkedHashMap<>();
for (Opportunity opp : opps) {
Long stageId = opp.getCurrentStageId();
grouped.computeIfAbsent(stageId, k -> {
Map<String, Object> m = new HashMap<>();
m.put("stageId", k);
m.put("count", 0);
m.put("totalAmount", java.math.BigDecimal.ZERO);
return m;
});
Map<String, Object> m = grouped.get(stageId);
m.put("count", (int) m.get("count") + 1);
if (opp.getProjectAmount() != null) {
m.put("totalAmount", ((java.math.BigDecimal) m.get("totalAmount")).add(opp.getProjectAmount()));
}
}
return new ArrayList<>(grouped.values());
}
@Override
public List<Map<String, Object>> boardCards(Long stageId, int offset, int limit, Long userId) {
List<Opportunity> opps = oppMapper.selectList(new LambdaQueryWrapper<Opportunity>()
.eq(Opportunity::getCurrentStageId, stageId)
.eq(Opportunity::getOppStatus, 2)
.last("LIMIT " + offset + "," + limit));
List<Map<String, Object>> result = new ArrayList<>();
for (Opportunity opp : opps) {
Map<String, Object> card = new HashMap<>();
card.put("id", opp.getId());
card.put("oppName", opp.getOppName());
card.put("ownerUserId", opp.getOwnerUserId());
card.put("ownerNameSnapshot", opp.getOwnerNameSnapshot());
card.put("projectAmount", opp.getProjectAmount());
card.put("currentStageId", opp.getCurrentStageId());
result.add(card);
}
return result;
}
}

4
crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityCreateServiceImpl.java

@ -58,6 +58,10 @@ public class OpportunityCreateServiceImpl implements IOpportunityCreateService {
IntakeSource.DIRECT,
request.getOppSource(),
request.getOpportunityName(),
request.getOppType(),
request.getBidForm(),
request.getLocalityType(),
request.getPartyAClear(),
request.getIndustryCode(),
request.getPartyA(),
request.getProvinceCode(),

59
crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunityDetailServiceImpl.java

@ -0,0 +1,59 @@
package com.crm.opportunity.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.crm.base.domain.exception.BusinessErrorException;
import com.crm.opportunity.domain.dto.OpportunityDTO;
import com.crm.opportunity.domain.entity.Opportunity;
import com.crm.opportunity.mapper.OpportunityMapper;
import com.crm.opportunity.service.IOpportunityDetailService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 商机详情 & 编辑实现 01 整改薄适配层
*/
@Service
@RequiredArgsConstructor
public class OpportunityDetailServiceImpl implements IOpportunityDetailService {
private final OpportunityMapper oppMapper;
@Override
public OpportunityDTO getDetail(Long id) {
Opportunity entity = oppMapper.selectById(id);
if (entity == null) {
throw new BusinessErrorException("商机不存在:id=" + id);
}
return OpportunityDTO.fromEntity(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void edit(OpportunityDTO dto) {
if (dto.getId() == null) {
throw new BusinessErrorException("编辑商机时 id 必填");
}
Opportunity existing = oppMapper.selectById(dto.getId());
if (existing == null) {
throw new BusinessErrorException("商机不存在:id=" + dto.getId());
}
// 用 LambdaUpdateWrapper 明确指定更新字段,绕开 MP 乘观锁自动处理的 MetaObject 问题
LambdaUpdateWrapper<Opportunity> wrapper = new LambdaUpdateWrapper<Opportunity>()
.eq(Opportunity::getId, dto.getId());
Opportunity update = new Opportunity();
if (dto.getOppName() != null) update.setOppName(dto.getOppName());
if (dto.getOppSource() != null) update.setOppSource(dto.getOppSource());
if (dto.getOppType() != null) update.setOppType(dto.getOppType());
if (dto.getIndustryCode() != null) update.setIndustryCode(dto.getIndustryCode());
if (dto.getBidForm() != null) update.setBidForm(dto.getBidForm());
if (dto.getLocalityType() != null) update.setLocalityType(dto.getLocalityType());
if (dto.getPartyAClear() != null) update.setPartyAClear(dto.getPartyAClear());
if (dto.getPartyA() != null) update.setPartyA(dto.getPartyA());
if (dto.getProvinceCode() != null) update.setProvinceCode(dto.getProvinceCode());
if (dto.getCityCode() != null) update.setCityCode(dto.getCityCode());
if (dto.getRemark() != null) update.setRemark(dto.getRemark());
if (dto.getProjectAmount() != null) update.setProjectAmount(dto.getProjectAmount());
oppMapper.update(update, wrapper);
}
}

169
crm-opportunity/src/main/java/com/crm/opportunity/service/impl/OpportunitySubServiceImpl.java

@ -0,0 +1,169 @@
package com.crm.opportunity.service.impl;
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.opportunity.domain.entity.OpportunityAttachment;
import com.crm.opportunity.domain.entity.OpportunityCustomer;
import com.crm.opportunity.domain.entity.OpportunityFollow;
import com.crm.opportunity.domain.entity.OpportunityOplog;
import com.crm.opportunity.domain.entity.OpportunitySiteSurvey;
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.OpportunityOplogMapper;
import com.crm.opportunity.mapper.OpportunitySiteSurveyMapper;
import com.crm.opportunity.mapper.OpportunityTeamMapper;
import com.crm.opportunity.service.IOpportunitySubService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 商机详情 Tab 子域服务实现 03 整改
*/
@Service
@RequiredArgsConstructor
public class OpportunitySubServiceImpl implements IOpportunitySubService {
private final OpportunityCustomerMapper customerMapper;
private final OpportunityFollowMapper followMapper;
private final OpportunitySiteSurveyMapper siteSurveyMapper;
private final OpportunityAttachmentMapper attachmentMapper;
private final OpportunityTeamMapper teamMapper;
private final OpportunityOplogMapper oplogMapper;
// ==================== 客户 ====================
@Override
public List<OpportunityCustomer> listCustomers(Long oppId) {
return customerMapper.selectList(new LambdaQueryWrapper<OpportunityCustomer>()
.eq(OpportunityCustomer::getOpportunityId, oppId)
.eq(OpportunityCustomer::getDeleteKey, 0L)
.orderByDesc(OpportunityCustomer::getIsPrimaryIntended));
}
// ==================== 跟进 ====================
@Override
public Page<OpportunityFollow> pageFollows(Long oppId, long pageNum, long pageSize) {
return followMapper.selectPage(new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<OpportunityFollow>()
.eq(OpportunityFollow::getOppId, oppId)
.eq(OpportunityFollow::getDeleteKey, 0L)
.orderByDesc(OpportunityFollow::getFollowTime));
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long addFollow(OpportunityFollow follow) {
if (follow.getOppId() == null) throw new BusinessErrorException("oppId 必填");
if (!StringUtils.hasText(follow.getFollowContent())) throw new BusinessErrorException("跟进内容必填");
if (follow.getFollowTime() == null) throw new BusinessErrorException("跟进时间必填");
if (!StringUtils.hasText(follow.getFollowWay())) throw new BusinessErrorException("跟进方式必填");
if (follow.getCustomerId() == null) throw new BusinessErrorException("关联客户必填");
follow.setDeleteKey(0L);
followMapper.insert(follow);
return follow.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteFollow(Long followId, Long operatorId) {
OpportunityFollow follow = followMapper.selectById(followId);
if (follow == null) throw new BusinessErrorException("跟进记录不存在");
followMapper.update(null, new LambdaUpdateWrapper<OpportunityFollow>()
.eq(OpportunityFollow::getId, followId)
.set(OpportunityFollow::getDeleteKey, followId)
.set(OpportunityFollow::getDeleted, 1));
}
// ==================== 勘察 ====================
@Override
public Page<OpportunitySiteSurvey> pageSiteSurveys(Long oppId, long pageNum, long pageSize) {
return siteSurveyMapper.selectPage(new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<OpportunitySiteSurvey>()
.eq(OpportunitySiteSurvey::getOppId, oppId)
.orderByDesc(OpportunitySiteSurvey::getSurveyDate));
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long addSiteSurvey(OpportunitySiteSurvey survey) {
if (survey.getOppId() == null) throw new BusinessErrorException("oppId 必填");
if (survey.getSurveyDate() == null) throw new BusinessErrorException("勘察日期必填");
if (survey.getEngineerUserId() == null) throw new BusinessErrorException("工程师必填");
siteSurveyMapper.insert(survey);
return survey.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteSiteSurvey(Long surveyId) {
OpportunitySiteSurvey survey = siteSurveyMapper.selectById(surveyId);
if (survey == null) throw new BusinessErrorException("勘察记录不存在");
siteSurveyMapper.update(null, new LambdaUpdateWrapper<OpportunitySiteSurvey>()
.eq(OpportunitySiteSurvey::getId, surveyId)
.set(OpportunitySiteSurvey::getDeleted, 1));
}
// ==================== 附件 ====================
@Override
public List<OpportunityAttachment> listAttachments(Long oppId, String bizType) {
LambdaQueryWrapper<OpportunityAttachment> wrapper = new LambdaQueryWrapper<OpportunityAttachment>()
.eq(OpportunityAttachment::getOppId, oppId)
.eq(OpportunityAttachment::getDeleteKey, 0L);
if (StringUtils.hasText(bizType)) {
wrapper.eq(OpportunityAttachment::getBizType, bizType);
}
return attachmentMapper.selectList(wrapper.orderByDesc(OpportunityAttachment::getCreateTime));
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long addAttachment(OpportunityAttachment attachment) {
if (attachment.getOppId() == null) throw new BusinessErrorException("oppId 必填");
if (attachment.getFileId() == null) throw new BusinessErrorException("fileId 必填");
attachment.setDeleteKey(0L);
attachmentMapper.insert(attachment);
return attachment.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAttachment(Long attachmentId) {
OpportunityAttachment att = attachmentMapper.selectById(attachmentId);
if (att == null) throw new BusinessErrorException("附件不存在");
attachmentMapper.update(null, new LambdaUpdateWrapper<OpportunityAttachment>()
.eq(OpportunityAttachment::getId, attachmentId)
.set(OpportunityAttachment::getDeleteKey, attachmentId)
.set(OpportunityAttachment::getDeleted, 1));
}
// ==================== 团队 ====================
@Override
public List<OpportunityTeam> listTeamMembers(Long oppId) {
return teamMapper.selectList(new LambdaQueryWrapper<OpportunityTeam>()
.eq(OpportunityTeam::getOpportunityId, oppId)
.eq(OpportunityTeam::getDeleteKey, 0L)
.orderByAsc(OpportunityTeam::getCreateTime));
}
// ==================== 操作日志 ====================
@Override
public Page<OpportunityOplog> pageOplogs(Long oppId, long pageNum, long pageSize) {
return oplogMapper.selectPage(new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<OpportunityOplog>()
.eq(OpportunityOplog::getOppId, oppId)
.orderByDesc(OpportunityOplog::getOpTime));
}
}

10
crm-opportunity/src/test/java/com/crm/opportunity/intake/impl/OpportunityIntakeImplTest.java

@ -67,16 +67,18 @@ class OpportunityIntakeImplTest {
/** 线索转入 spec(source=LEAD_CONVERT,oppSource 已由 adapter 填 opp_source_01)。 */
private OpportunityIntakeSpec leadConvertSpec(Long customerId) {
return new OpportunityIntakeSpec(
IntakeSource.LEAD_CONVERT, "opp_source_01", "商机A", "ind01", "甲方X",
"110000", "110100", "备注", OWNER_USER_ID, OWNER_DEPT_ID,
IntakeSource.LEAD_CONVERT, "opp_source_01", "商机A",
null, null, null, null,
"ind01", "甲方X", "110000", "110100", "备注", OWNER_USER_ID, OWNER_DEPT_ID,
customerId, "意向客户名", LEAD_ID, "线索A", "13800000000", "prod01");
}
/** 直接创建 spec(source=DIRECT,oppSource 用户所选,无来源线索)。 */
private OpportunityIntakeSpec directSpec(String oppSource, Long customerId) {
return new OpportunityIntakeSpec(
IntakeSource.DIRECT, oppSource, "商机B", "ind02", "甲方Y",
"310000", "310100", "备注2", OWNER_USER_ID, OWNER_DEPT_ID,
IntakeSource.DIRECT, oppSource, "商机B",
null, null, null, null,
"ind02", "甲方Y", "310000", "310100", "备注2", OWNER_USER_ID, OWNER_DEPT_ID,
customerId, "意向客户名2", null, null, null, null);
}

16
crm-rule/src/main/java/com/crm/rule/controller/OpportunityPoolRuleController.java

@ -4,7 +4,11 @@ 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.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;
@ -26,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
public class OpportunityPoolRuleController {
private final IOpportunityPoolRuleService ruleService;
private final OpportunityPoolRuleMapper ruleMapper;
@Operation(summary = "公海规则分页列表(按版本维度展示)")
@PostMapping("/page")
@ -72,4 +77,15 @@ public class OpportunityPoolRuleController {
ruleService.deleteDraft(id);
return Result.success();
}
@Operation(summary = "同族版本列表(按 ruleCode 查所有版本)")
@GetMapping("/versions")
public Result<List<OpportunityPoolRule>> versions(@RequestParam("id") Long id) {
OpportunityPoolRule r = ruleMapper.selectById(id);
if (r == null) return Result.success(java.util.List.of());
return Result.success(ruleMapper.selectList(
new LambdaQueryWrapper<OpportunityPoolRule>()
.eq(OpportunityPoolRule::getRuleCode, r.getRuleCode())
.orderByDesc(OpportunityPoolRule::getCreateTime)));
}
}

15
crm-rule/src/main/java/com/crm/rule/controller/OpportunitySchemeCardTemplateController.java

@ -5,6 +5,9 @@ import com.crm.base.domain.result.Result;
import com.crm.rule.domain.dto.OpportunitySchemeCardTemplateDTO;
import com.crm.rule.domain.dto.OpportunitySchemeFieldDefDTO;
import com.crm.rule.domain.param.OpportunitySchemeCardTemplatePageParam;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.crm.rule.domain.entity.OpportunitySchemeCardTemplate;
import com.crm.rule.mapper.OpportunitySchemeCardTemplateMapper;
import com.crm.rule.service.IOpportunitySchemeCardTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -30,6 +33,7 @@ import java.util.List;
public class OpportunitySchemeCardTemplateController {
private final IOpportunitySchemeCardTemplateService templateService;
private final OpportunitySchemeCardTemplateMapper schemeTemplateMapper;
@Operation(summary = "方案卡模板分页列表(按版本维度展示)")
@PostMapping("/page")
@ -82,4 +86,15 @@ public class OpportunitySchemeCardTemplateController {
templateService.deleteDraft(id);
return Result.success();
}
@Operation(summary = "同族版本列表(按 templateCode 查所有版本)")
@GetMapping("/versions")
public Result<List<OpportunitySchemeCardTemplate>> versions(@RequestParam("id") Long id) {
OpportunitySchemeCardTemplate t = schemeTemplateMapper.selectById(id);
if (t == null) return Result.success(List.of());
return Result.success(schemeTemplateMapper.selectList(
new LambdaQueryWrapper<OpportunitySchemeCardTemplate>()
.eq(OpportunitySchemeCardTemplate::getTemplateCode, t.getTemplateCode())
.orderByDesc(OpportunitySchemeCardTemplate::getCreateTime)));
}
}

18
crm-rule/src/main/java/com/crm/rule/controller/OpportunityStageTemplateController.java

@ -4,6 +4,9 @@ import com.crm.base.domain.result.PageResult;
import com.crm.base.domain.result.Result;
import com.crm.rule.domain.dto.OpportunityStageTemplateDTO;
import com.crm.rule.domain.param.OpportunityStageTemplatePageParam;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.crm.rule.domain.entity.OpportunityStageTemplate;
import com.crm.rule.mapper.OpportunityStageTemplateMapper;
import com.crm.rule.service.IOpportunityStageTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -14,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 IOpportunityStageTemplateService} 06
* <p>权限策略不种子化 button 权限点同公海规则ApiPermissionInterceptor 对未注册
@ -27,6 +32,7 @@ import org.springframework.web.bind.annotation.RestController;
public class OpportunityStageTemplateController {
private final IOpportunityStageTemplateService templateService;
private final OpportunityStageTemplateMapper templateMapper;
@Operation(summary = "阶段模板分页列表(按版本维度展示)")
@PostMapping("/page")
@ -73,4 +79,16 @@ public class OpportunityStageTemplateController {
templateService.deleteDraft(id);
return Result.success();
}
@Operation(summary = "同族版本列表(按 templateCode 查所有版本)")
@GetMapping("/versions")
public Result<List<OpportunityStageTemplate>> versions(@RequestParam("id") Long id) {
OpportunityStageTemplate t = templateMapper.selectById(id);
if (t == null) return Result.success(List.of());
List<OpportunityStageTemplate> list = templateMapper.selectList(
new LambdaQueryWrapper<OpportunityStageTemplate>()
.eq(OpportunityStageTemplate::getTemplateCode, t.getTemplateCode())
.orderByDesc(OpportunityStageTemplate::getCreateTime));
return Result.success(list);
}
}

Loading…
Cancel
Save