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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -32,7 +31,7 @@ public class OpportunityCreateController {
@Operation(summary = "新建商机(oppSource=线索转入 时须选关联线索)") @Operation(summary = "新建商机(oppSource=线索转入 时须选关联线索)")
@PostMapping @PostMapping
public Result<Long> create(@RequestBody CreateOpportunityRequest request) { public Result<Long> create(CreateOpportunityRequest request) {
Long ownerUserId = Long.valueOf(SecurityUtils.getRequiredUserId()); Long ownerUserId = Long.valueOf(SecurityUtils.getRequiredUserId());
return Result.success(createService.createOpportunity(request, ownerUserId)); 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; private String opportunityName;
/** 商机类型字典 code。 */
private String oppType;
/** 行业字典 code。 */ /** 行业字典 code。 */
private String industryCode; private String industryCode;
/** 招标形式字典 code。 */
private String bidForm;
/** 项目属地字典 code(本地/异地)。 */
private String localityType;
/** 甲方是否明确 0/1(默认 0)。 */
private Integer partyAClear;
/** 甲方。 */ /** 甲方。 */
private String partyA; 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("商机名称") @Comment("商机名称")
@Column(columnDefinition = "varchar(200) not null comment '商机名称'") @Column(columnDefinition = "varchar(200) not null comment '商机名称'")
private String oppName; private String oppName = "";
@Comment("商机来源字典 code(渠道归因,线索转入时默认置「线索转入」)") @Comment("商机来源字典 code(渠道归因,线索转入时默认置「线索转入」)")
@Column(columnDefinition = "varchar(64) not null comment '商机来源字典code'") @Column(columnDefinition = "varchar(64) not null comment '商机来源字典code'")
private String oppSource; private String oppSource = "";
@Comment("商机类型字典 code") @Comment("商机类型字典 code")
@Column(columnDefinition = "varchar(64) not null comment '商机类型字典code'") @Column(columnDefinition = "varchar(64) not null comment '商机类型字典code'")
private String oppType; private String oppType = "";
@Comment("行业字典 code") @Comment("行业字典 code")
@Column(columnDefinition = "varchar(64) not null comment '行业字典code'") @Column(columnDefinition = "varchar(64) not null comment '行业字典code'")
private String industryCode; private String industryCode = "";
@Comment("招标形式字典 code") @Comment("招标形式字典 code")
@Column(columnDefinition = "varchar(64) not null comment '招标形式字典code'") @Column(columnDefinition = "varchar(64) not null comment '招标形式字典code'")
private String bidForm; private String bidForm = "";
@Comment("项目属地字典 code(本地/异地分类标签,非地理坐标)") @Comment("项目属地字典 code(本地/异地分类标签,非地理坐标)")
@Column(columnDefinition = "varchar(64) not null comment '项目属地字典code'") @Column(columnDefinition = "varchar(64) not null comment '项目属地字典code'")
private String localityType; private String localityType = "";
@Comment("甲方是否明确 0/1") @Comment("甲方是否明确 0/1")
@Column(columnDefinition = "tinyint not null comment '甲方是否明确'") @Column(columnDefinition = "tinyint not null default 0 comment '甲方是否明确'")
private Integer partyAClear; private Integer partyAClear = 0;
@Comment("甲方名称(party_a_clear=1 时必填)") @Comment("甲方名称(party_a_clear=1 时必填)")
@Column(columnDefinition = "varchar(200) comment '甲方名称'") @Column(columnDefinition = "varchar(200) comment '甲方名称'")
@ -75,11 +75,11 @@ public class Opportunity extends BaseEntity {
@Comment("省份国标 code(GB/T 2260,6位)") @Comment("省份国标 code(GB/T 2260,6位)")
@Column(columnDefinition = "varchar(12) not null comment '省份国标code'") @Column(columnDefinition = "varchar(12) not null comment '省份国标code'")
private String provinceCode; private String provinceCode = "";
@Comment("市/区国标 code(对称 lead 约定:市码或区/县码均可,不单建 district_code)") @Comment("市/区国标 code(对称 lead 约定:市码或区/县码均可,不单建 district_code)")
@Column(columnDefinition = "varchar(12) not null comment '市/区国标code'") @Column(columnDefinition = "varchar(12) not null comment '市/区国标code'")
private String cityCode; private String cityCode = "";
@Comment("备注") @Comment("备注")
@Column(columnDefinition = "text 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, IntakeSource source,
String oppSource, String oppSource,
String opportunityName, String opportunityName,
String oppType,
String bidForm,
String localityType,
Integer partyAClear,
String industryCode, String industryCode,
String partyA, String partyA,
String provinceCode, 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()); opp.setOppName(spec.opportunityName());
// 商机来源:LEAD_CONVERT 恒 opp_source_01(adapter 已按枚举默认填入 spec),DIRECT 为用户所选。 // 商机来源:LEAD_CONVERT 恒 opp_source_01(adapter 已按枚举默认填入 spec),DIRECT 为用户所选。
opp.setOppSource(spec.oppSource()); 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.setPartyA(spec.partyA());
opp.setRemark(spec.remark()); opp.setRemark(spec.remark());
opp.setProvinceCode(spec.provinceCode());
opp.setCityCode(spec.cityCode());
opp.setOwnerUserId(spec.ownerUserId()); opp.setOwnerUserId(spec.ownerUserId());
opp.setOwnerDeptId(spec.ownerDeptId()); opp.setOwnerDeptId(spec.ownerDeptId());
opp.setCreatorUserId(spec.ownerUserId()); 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,
IntakeSource.LEAD_CONVERT.defaultOppSource(), IntakeSource.LEAD_CONVERT.defaultOppSource(),
cmd.opportunityName(), cmd.opportunityName(),
null, null, null, null, // oppType/bidForm/localityType/partyAClear 线索转入不传,底层兑底
cmd.industryCode(), cmd.industryCode(),
cmd.partyA(), cmd.partyA(),
cmd.provinceCode(), 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, IntakeSource.DIRECT,
request.getOppSource(), request.getOppSource(),
request.getOpportunityName(), request.getOpportunityName(),
request.getOppType(),
request.getBidForm(),
request.getLocalityType(),
request.getPartyAClear(),
request.getIndustryCode(), request.getIndustryCode(),
request.getPartyA(), request.getPartyA(),
request.getProvinceCode(), 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)。 */ /** 线索转入 spec(source=LEAD_CONVERT,oppSource 已由 adapter 填 opp_source_01)。 */
private OpportunityIntakeSpec leadConvertSpec(Long customerId) { private OpportunityIntakeSpec leadConvertSpec(Long customerId) {
return new OpportunityIntakeSpec( return new OpportunityIntakeSpec(
IntakeSource.LEAD_CONVERT, "opp_source_01", "商机A", "ind01", "甲方X", IntakeSource.LEAD_CONVERT, "opp_source_01", "商机A",
"110000", "110100", "备注", OWNER_USER_ID, OWNER_DEPT_ID, null, null, null, null,
"ind01", "甲方X", "110000", "110100", "备注", OWNER_USER_ID, OWNER_DEPT_ID,
customerId, "意向客户名", LEAD_ID, "线索A", "13800000000", "prod01"); customerId, "意向客户名", LEAD_ID, "线索A", "13800000000", "prod01");
} }
/** 直接创建 spec(source=DIRECT,oppSource 用户所选,无来源线索)。 */ /** 直接创建 spec(source=DIRECT,oppSource 用户所选,无来源线索)。 */
private OpportunityIntakeSpec directSpec(String oppSource, Long customerId) { private OpportunityIntakeSpec directSpec(String oppSource, Long customerId) {
return new OpportunityIntakeSpec( return new OpportunityIntakeSpec(
IntakeSource.DIRECT, oppSource, "商机B", "ind02", "甲方Y", IntakeSource.DIRECT, oppSource, "商机B",
"310000", "310100", "备注2", OWNER_USER_ID, OWNER_DEPT_ID, null, null, null, null,
"ind02", "甲方Y", "310000", "310100", "备注2", OWNER_USER_ID, OWNER_DEPT_ID,
customerId, "意向客户名2", null, null, null, null); 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.base.domain.result.Result;
import com.crm.rule.domain.dto.OpportunityPoolRuleDTO; import com.crm.rule.domain.dto.OpportunityPoolRuleDTO;
import com.crm.rule.domain.param.OpportunityPoolRulePageParam; 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 com.crm.rule.service.IOpportunityPoolRuleService;
import java.util.List;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -26,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
public class OpportunityPoolRuleController { public class OpportunityPoolRuleController {
private final IOpportunityPoolRuleService ruleService; private final IOpportunityPoolRuleService ruleService;
private final OpportunityPoolRuleMapper ruleMapper;
@Operation(summary = "公海规则分页列表(按版本维度展示)") @Operation(summary = "公海规则分页列表(按版本维度展示)")
@PostMapping("/page") @PostMapping("/page")
@ -72,4 +77,15 @@ public class OpportunityPoolRuleController {
ruleService.deleteDraft(id); ruleService.deleteDraft(id);
return Result.success(); 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.OpportunitySchemeCardTemplateDTO;
import com.crm.rule.domain.dto.OpportunitySchemeFieldDefDTO; import com.crm.rule.domain.dto.OpportunitySchemeFieldDefDTO;
import com.crm.rule.domain.param.OpportunitySchemeCardTemplatePageParam; 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 com.crm.rule.service.IOpportunitySchemeCardTemplateService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -30,6 +33,7 @@ import java.util.List;
public class OpportunitySchemeCardTemplateController { public class OpportunitySchemeCardTemplateController {
private final IOpportunitySchemeCardTemplateService templateService; private final IOpportunitySchemeCardTemplateService templateService;
private final OpportunitySchemeCardTemplateMapper schemeTemplateMapper;
@Operation(summary = "方案卡模板分页列表(按版本维度展示)") @Operation(summary = "方案卡模板分页列表(按版本维度展示)")
@PostMapping("/page") @PostMapping("/page")
@ -82,4 +86,15 @@ public class OpportunitySchemeCardTemplateController {
templateService.deleteDraft(id); templateService.deleteDraft(id);
return Result.success(); 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.base.domain.result.Result;
import com.crm.rule.domain.dto.OpportunityStageTemplateDTO; import com.crm.rule.domain.dto.OpportunityStageTemplateDTO;
import com.crm.rule.domain.param.OpportunityStageTemplatePageParam; 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 com.crm.rule.service.IOpportunityStageTemplateService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; 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.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* 商机阶段模板接口薄适配层只调 {@link IOpportunityStageTemplateService} 06 * 商机阶段模板接口薄适配层只调 {@link IOpportunityStageTemplateService} 06
* <p>权限策略不种子化 button 权限点同公海规则ApiPermissionInterceptor 对未注册 * <p>权限策略不种子化 button 权限点同公海规则ApiPermissionInterceptor 对未注册
@ -27,6 +32,7 @@ import org.springframework.web.bind.annotation.RestController;
public class OpportunityStageTemplateController { public class OpportunityStageTemplateController {
private final IOpportunityStageTemplateService templateService; private final IOpportunityStageTemplateService templateService;
private final OpportunityStageTemplateMapper templateMapper;
@Operation(summary = "阶段模板分页列表(按版本维度展示)") @Operation(summary = "阶段模板分页列表(按版本维度展示)")
@PostMapping("/page") @PostMapping("/page")
@ -73,4 +79,16 @@ public class OpportunityStageTemplateController {
templateService.deleteDraft(id); templateService.deleteDraft(id);
return Result.success(); 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