Browse Source
- 票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
21 changed files with 1027 additions and 19 deletions
@ -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)); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue