feat(opportunity): 新增全量商机查询内部接口

1. 新增OpportunityFullDTO封装全量商机精简字段
2. 实现listAllOpportunities接口及Mapper、SQL查询,返回全量商机不含数据权限过滤
3. 新增/api/opportunities/all内部接口,支持X-Internal-Secret鉴权
4. 配置放行接口路径到生产和开发环境配置文件
5. 补充完整接口文档说明
main
kangwenjing 2026-09-21 15:43:18 +08:00
parent 010d3052c8
commit 8477aac22f
10 changed files with 322 additions and 32 deletions

View File

@ -5,41 +5,13 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="修改定位信息 0323"> <list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="修改定位信息 0323">
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/OmsCallbackController.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/OmsCallbackController.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/OpportunityController.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/OpportunityController.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityDuplicateCheckDTO.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityDuplicateCheckDTO.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/mapper/LlmMcpMapper.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/mapper/LlmMcpMapper.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/security/McpDataPermissionInterceptor.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/security/McpDataPermissionInterceptor.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmCheckinSearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmCheckinSearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmDetailSearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmDetailSearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmDictOptionsToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmDictOptionsToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmEntityDetailToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmEntityDetailToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmExpansionSearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmExpansionSearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmFollowupSearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmFollowupSearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmMetadataToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmMetadataToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmOrgUserSearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmOrgUserSearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmReportCatalogToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmReportCatalogToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmReportQueryToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmReportQueryToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmTodoSearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmTodoSearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmUniversalSearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmUniversalSearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmWorkTodayStatusToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/CrmWorkTodayStatusToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/OpportunitySearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/OpportunitySearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/UserProfileToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/UserProfileToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/WorkReportSearchToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/WorkReportSearchToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/support/PermissionedMcpToolProvider.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/llm/tools/support/PermissionedMcpToolProvider.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/OpportunityService.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/OpportunityService.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/OpportunityService.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/OpportunityService.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/resources/application-prod.yml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/application-prod.yml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/backend/src/main/resources/application-prod.yml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/application-prod.yml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/resources/application.yml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/application.yml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/backend/src/main/resources/application.yml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/application.yml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/resources/mapper/llm/LlmMcpMapper.xml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/mapper/llm/LlmMcpMapper.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/test/java/com/unis/crm/llm/security/McpDataPermissionInterceptorTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/test/java/com/unis/crm/llm/security/McpDataPermissionInterceptorTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/lib/auth.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/lib/auth.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/pages/Opportunities.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/pages/Opportunities.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/pages/Work.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/pages/Work.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/sql/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/sql/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/sql/init_full_pg17.sql" beforeDir="false" afterPath="$PROJECT_DIR$/sql/init_full_pg17.sql" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -139,7 +111,8 @@
<workItem from="1789033867432" duration="17000" /> <workItem from="1789033867432" duration="17000" />
<workItem from="1789440226191" duration="937000" /> <workItem from="1789440226191" duration="937000" />
<workItem from="1789459774783" duration="30000" /> <workItem from="1789459774783" duration="30000" />
<workItem from="1789529046094" duration="3889000" /> <workItem from="1789529046094" duration="5499000" />
<workItem from="1789969706373" duration="858000" />
</task> </task>
<task id="LOCAL-00001" summary="修改定位信息 0323"> <task id="LOCAL-00001" summary="修改定位信息 0323">
<option name="closed" value="true" /> <option name="closed" value="true" />

View File

@ -2,18 +2,23 @@ package com.unis.crm.controller;
import com.unis.crm.common.ApiResponse; import com.unis.crm.common.ApiResponse;
import com.unis.crm.common.CurrentUserUtils; import com.unis.crm.common.CurrentUserUtils;
import com.unis.crm.common.UnauthorizedException;
import com.unis.crm.config.InternalAuthProperties;
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest; import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
import com.unis.crm.dto.opportunity.CreateOpportunityRequest; import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO; import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO;
import com.unis.crm.dto.opportunity.OpportunityDuplicateCheckDTO; import com.unis.crm.dto.opportunity.OpportunityDuplicateCheckDTO;
import com.unis.crm.dto.opportunity.OpportunityFullDTO;
import com.unis.crm.dto.opportunity.OpportunityItemDTO; import com.unis.crm.dto.opportunity.OpportunityItemDTO;
import com.unis.crm.dto.opportunity.OpportunityMetaDTO; import com.unis.crm.dto.opportunity.OpportunityMetaDTO;
import com.unis.crm.dto.opportunity.OpportunityOverviewDTO; import com.unis.crm.dto.opportunity.OpportunityOverviewDTO;
import com.unis.crm.dto.opportunity.PushOpportunityToOmsRequest; import com.unis.crm.dto.opportunity.PushOpportunityToOmsRequest;
import com.unis.crm.service.OpportunityService; import com.unis.crm.service.OpportunityService;
import com.unisbase.common.annotation.Log; import com.unisbase.common.annotation.Log;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -28,10 +33,16 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/api/opportunities") @RequestMapping("/api/opportunities")
public class OpportunityController { public class OpportunityController {
private final OpportunityService opportunityService; private static final String INTERNAL_UNAUTHORIZED_MESSAGE = "内部接口鉴权失败";
public OpportunityController(OpportunityService opportunityService) { private final OpportunityService opportunityService;
private final InternalAuthProperties internalAuthProperties;
public OpportunityController(
OpportunityService opportunityService,
InternalAuthProperties internalAuthProperties) {
this.opportunityService = opportunityService; this.opportunityService = opportunityService;
this.internalAuthProperties = internalAuthProperties;
} }
@GetMapping("/meta") @GetMapping("/meta")
@ -39,6 +50,36 @@ public class OpportunityController {
return ApiResponse.success(opportunityService.getMeta()); return ApiResponse.success(opportunityService.getMeta());
} }
/**
* /
* <p>使 X-Internal-Secret </p>
*/
@GetMapping("/all")
@Log(type = "商机集成", value = "查询全量商机")
public ApiResponse<List<OpportunityFullDTO>> listAll(HttpServletRequest httpServletRequest) {
validateInternalSecret(httpServletRequest);
return ApiResponse.success(opportunityService.listAllOpportunities());
}
private void validateInternalSecret(HttpServletRequest httpServletRequest) {
if (!internalAuthProperties.isEnabled()) {
return;
}
String expectedSecret = trimToNull(internalAuthProperties.getSecret());
String actualSecret = trimToNull(httpServletRequest.getHeader(internalAuthProperties.resolveHeaderName()));
if (expectedSecret == null || !Objects.equals(expectedSecret, actualSecret)) {
throw new UnauthorizedException(INTERNAL_UNAUTHORIZED_MESSAGE);
}
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
@GetMapping("/overview") @GetMapping("/overview")
public ApiResponse<OpportunityOverviewDTO> getOverview( public ApiResponse<OpportunityOverviewDTO> getOverview(
@RequestHeader("X-User-Id") Long userId, @RequestHeader("X-User-Id") Long userId,

View File

@ -0,0 +1,108 @@
package com.unis.crm.dto.opportunity;
/**
* DTO
*/
public class OpportunityFullDTO {
/** 商机编号 */
private String opportunityCode;
/** 商机名称 */
private String opportunityName;
/** 商机阶段编码 */
private String stageCode;
/** 商机阶段名称 */
private String stage;
/** 把握度等级A/B/C */
private String confidence;
/** 是否已推送OMS */
private Boolean pushedToOms;
/** 推送OMS时间 */
private String omsPushTime;
/** 创建时间 */
private String createdAt;
/** 商机负责人ID */
private Long ownerUserId;
/** 商机负责人姓名 */
private String ownerName;
public String getOpportunityCode() {
return opportunityCode;
}
public void setOpportunityCode(String opportunityCode) {
this.opportunityCode = opportunityCode;
}
public String getOpportunityName() {
return opportunityName;
}
public void setOpportunityName(String opportunityName) {
this.opportunityName = opportunityName;
}
public String getStageCode() {
return stageCode;
}
public void setStageCode(String stageCode) {
this.stageCode = stageCode;
}
public String getStage() {
return stage;
}
public void setStage(String stage) {
this.stage = stage;
}
public String getConfidence() {
return confidence;
}
public void setConfidence(String confidence) {
this.confidence = confidence;
}
public Boolean getPushedToOms() {
return pushedToOms;
}
public void setPushedToOms(Boolean pushedToOms) {
this.pushedToOms = pushedToOms;
}
public String getOmsPushTime() {
return omsPushTime;
}
public void setOmsPushTime(String omsPushTime) {
this.omsPushTime = omsPushTime;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public Long getOwnerUserId() {
return ownerUserId;
}
public void setOwnerUserId(Long ownerUserId) {
this.ownerUserId = ownerUserId;
}
public String getOwnerName() {
return ownerName;
}
public void setOwnerName(String ownerName) {
this.ownerName = ownerName;
}
}

View File

@ -7,6 +7,7 @@ import com.unis.crm.dto.opportunity.OpportunityCustomerSnapshotDTO;
import com.unis.crm.dto.opportunity.OpportunityDictOptionDTO; import com.unis.crm.dto.opportunity.OpportunityDictOptionDTO;
import com.unis.crm.dto.opportunity.OpportunityDuplicateCheckDTO; import com.unis.crm.dto.opportunity.OpportunityDuplicateCheckDTO;
import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO; import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO;
import com.unis.crm.dto.opportunity.OpportunityFullDTO;
import com.unis.crm.dto.opportunity.OpportunityIntegrationTargetDTO; import com.unis.crm.dto.opportunity.OpportunityIntegrationTargetDTO;
import com.unis.crm.dto.opportunity.OpportunityItemDTO; import com.unis.crm.dto.opportunity.OpportunityItemDTO;
import com.unis.crm.dto.opportunity.OpportunityOmsPushDataDTO; import com.unis.crm.dto.opportunity.OpportunityOmsPushDataDTO;
@ -59,6 +60,11 @@ public interface OpportunityMapper {
@Param("excludeStageCodes") List<String> excludeStageCodes, @Param("excludeStageCodes") List<String> excludeStageCodes,
@Param("stageCodes") List<String> stageCodes); @Param("stageCodes") List<String> stageCodes);
/**
* /
*/
List<OpportunityFullDTO> selectAllOpportunities();
OpportunityItemDTO selectOpportunityDetail( OpportunityItemDTO selectOpportunityDetail(
@Param("userId") Long userId, @Param("userId") Long userId,
@Param("opportunityId") Long opportunityId, @Param("opportunityId") Long opportunityId,

View File

@ -4,6 +4,7 @@ import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
import com.unis.crm.dto.opportunity.CreateOpportunityRequest; import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO; import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO;
import com.unis.crm.dto.opportunity.OpportunityDuplicateCheckDTO; import com.unis.crm.dto.opportunity.OpportunityDuplicateCheckDTO;
import com.unis.crm.dto.opportunity.OpportunityFullDTO;
import com.unis.crm.dto.opportunity.OpportunityItemDTO; import com.unis.crm.dto.opportunity.OpportunityItemDTO;
import com.unis.crm.dto.opportunity.OpportunityMetaDTO; import com.unis.crm.dto.opportunity.OpportunityMetaDTO;
import com.unis.crm.dto.opportunity.OpportunityOverviewDTO; import com.unis.crm.dto.opportunity.OpportunityOverviewDTO;
@ -15,6 +16,11 @@ public interface OpportunityService {
OpportunityMetaDTO getMeta(); OpportunityMetaDTO getMeta();
/**
* /
*/
List<OpportunityFullDTO> listAllOpportunities();
OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage); OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage);
OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage, boolean includeDetails); OpportunityOverviewDTO getOverview(Long userId, String keyword, String stage, boolean includeDetails);

View File

@ -14,6 +14,7 @@ import com.unis.crm.dto.opportunity.OpportunityDictOptionDTO;
import com.unis.crm.dto.opportunity.OpportunityDuplicateCheckDTO; import com.unis.crm.dto.opportunity.OpportunityDuplicateCheckDTO;
import com.unis.crm.dto.opportunity.OpportunityMetaDTO; import com.unis.crm.dto.opportunity.OpportunityMetaDTO;
import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO; import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO;
import com.unis.crm.dto.opportunity.OpportunityFullDTO;
import com.unis.crm.dto.opportunity.OpportunityIntegrationTargetDTO; import com.unis.crm.dto.opportunity.OpportunityIntegrationTargetDTO;
import com.unis.crm.dto.opportunity.OpportunityItemDTO; import com.unis.crm.dto.opportunity.OpportunityItemDTO;
import com.unis.crm.dto.opportunity.OpportunityOmsPushDataDTO; import com.unis.crm.dto.opportunity.OpportunityOmsPushDataDTO;
@ -207,6 +208,11 @@ public class OpportunityServiceImpl implements OpportunityService {
return item; return item;
} }
@Override
public List<OpportunityFullDTO> listAllOpportunities() {
return opportunityMapper.selectAllOpportunities();
}
@Override @Override
public OpportunityDuplicateCheckDTO checkDuplicateOpportunity(Long userId, String name, Long excludeId, List<String> excludeStageCodes) { public OpportunityDuplicateCheckDTO checkDuplicateOpportunity(Long userId, String name, Long excludeId, List<String> excludeStageCodes) {
if (isBlank(name)) { if (isBlank(name)) {

View File

@ -54,6 +54,7 @@ unisbase:
permit-all-urls: permit-all-urls:
- /actuator/health - /actuator/health
- /api/wecom/sso/** - /api/wecom/sso/**
- /api/opportunities/all
- /api/opportunities/integration/** - /api/opportunities/integration/**
- /api/oms/callback/order-return - /api/oms/callback/order-return
internal-auth: internal-auth:

View File

@ -56,6 +56,7 @@ unisbase:
permit-all-urls: permit-all-urls:
- /actuator/health - /actuator/health
- /api/wecom/sso/** - /api/wecom/sso/**
- /api/opportunities/all
- /api/opportunities/integration/** - /api/opportunities/integration/**
- /api/oms/callback/order-return - /api/oms/callback/order-return
internal-auth: internal-auth:

View File

@ -322,6 +322,53 @@
</if> </if>
</select> </select>
<select id="selectAllOpportunities" resultType="com.unis.crm.dto.opportunity.OpportunityFullDTO">
select
coalesce(o.opportunity_code, '') as opportunityCode,
coalesce(o.opportunity_name, '') as opportunityName,
coalesce(stage_dict.item_value, o.stage, '') as stageCode,
coalesce(
stage_dict.item_label,
case coalesce(o.stage, 'initial_contact')
when 'initial_contact' then '初步沟通'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '商务谈判'
when 'won' then '已成交'
when 'lost' then 'L-已丢单'
else coalesce(o.stage, '初步沟通')
end
) as stage,
coalesce(o.confidence_pct, '') as confidence,
coalesce(o.pushed_to_oms, false) as pushedToOms,
coalesce(to_char(o.oms_push_time, 'YYYY-MM-DD HH24:MI'), '') as omsPushTime,
coalesce(to_char(o.created_at, 'YYYY-MM-DD HH24:MI'), '') as createdAt,
o.owner_user_id as ownerUserId,
coalesce(nullif(btrim(u.display_name), ''), nullif(btrim(u.username), ''), '') as ownerName
from crm_opportunity o
left join sys_user u on u.user_id = o.owner_user_id
left join sys_dict_item stage_dict
on stage_dict.type_code = 'sj_xmjd'
and (
stage_dict.item_value = o.stage
or stage_dict.item_label = case coalesce(o.stage, 'initial_contact')
when 'initial_contact' then '初步沟通'
when 'solution_discussion' then '方案交流'
when 'bidding' then '招投标'
when 'business_negotiation' then '商务谈判'
when 'won' then '已成交'
when 'lost' then 'L-已丢单'
else o.stage
end
)
and (
stage_dict.status = 1
or coalesce(o.archived, false) = true
)
and coalesce(stage_dict.is_deleted, 0) = 0
order by o.created_at desc, o.id desc
</select>
<select id="selectOpportunityDetail" resultType="com.unis.crm.dto.opportunity.OpportunityItemDTO"> <select id="selectOpportunityDetail" resultType="com.unis.crm.dto.opportunity.OpportunityItemDTO">
select select
o.id, o.id,

View File

@ -0,0 +1,101 @@
# 全量商机查询接口文档
> 提供 CRM 全量商机精简字段清单,包含已归档/已签单商机,不分页。
## 基本信息
| 项目 | 内容 |
| --- | --- |
| 接口路径 | `GET /api/opportunities/all` |
| 调用方 | 内部系统 / 系统间集成(不面向前端用户) |
| 请求头鉴权 | `X-Internal-Secret`(内部接口密钥) |
| 数据范围 | 全量商机(含已归档/已签单),不做数据权限过滤 |
| 排序 | 按创建时间倒序,创建时间相同按 id 倒序 |
| 分页 | 无(一次性返回全部) |
## 鉴权方式
与商机集成更新接口 `PUT /api/opportunities/integration/update`、OMS 退单回调接口保持一致,使用 `unisbase.internal-auth` 配置的密钥校验。
- 请求头字段:`X-Internal-Secret`(可通过 `unisbase.internal-auth.header-name` 覆盖)
- 秘钥值:`unisbase.internal-auth.secret`
- 校验逻辑:
- 当 `unisbase.internal-auth.enabled=true` 时强制校验,头信息与配置密钥不一致则返回 `401``内部接口鉴权失败`
- 当 `enabled=false` 时跳过鉴权(仅用于本地开发)。
- 该路径已加入 `unisbase.security.permit-all-urls`**不需要** `Authorization: Bearer` 登录态。
## 请求
```
GET /api/opportunities/all
X-Internal-Secret: <配置的secret>
```
无查询参数、无请求体。
## 响应
统一响应结构 `ApiResponse<List<OpportunityFullDTO>>`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` | string | 状态码,`"0"` 表示成功,`"-1"` 表示失败 |
| `msg` | string | 提示信息,成功为 `success` |
| `data` | array | 商机列表 |
`data` 元素字段:
| 字段 | 类型 | 对应库表字段 | 说明 |
| --- | --- | --- | --- |
| `opportunityCode` | string | `crm_opportunity.opportunity_code` | 商机编号 |
| `opportunityName` | string | `crm_opportunity.opportunity_name` | 商机名称 |
| `stageCode` | string | `crm_opportunity.stage` | 商机阶段编码(取自字典 `sj_xmjd`,未匹配时回退原值) |
| `stage` | string | 字典 `sj_xmjd.item_label` | 商机阶段名称(未匹配字典时按内置映射回退) |
| `confidence` | string | `crm_opportunity.confidence_pct` | 把握度等级:`A` / `B` / `C` |
| `pushedToOms` | boolean | `crm_opportunity.pushed_to_oms` | 是否已推送 OMS |
| `omsPushTime` | string | `crm_opportunity.oms_push_time` | 推送 OMS 时间,格式 `yyyy-MM-dd HH:mm`,未推送为空串 |
| `createdAt` | string | `crm_opportunity.created_at` | 创建时间,格式 `yyyy-MM-dd HH:mm` |
| `ownerUserId` | long | `crm_opportunity.owner_user_id` | 商机负责人 ID |
| `ownerName` | string | `sys_user.display_name` / `username` | 商机负责人姓名 |
响应示例:
```json
{
"code": "0",
"msg": "success",
"data": [
{
"opportunityCode": "OPP-20260916-001",
"opportunityName": "某某医院云桌面项目",
"stageCode": "bidding",
"stage": "招投标",
"confidence": "A",
"pushedToOms": true,
"omsPushTime": "2026-09-16 10:30",
"createdAt": "2026-09-10 09:12",
"ownerUserId": 1001,
"ownerName": "张三"
}
]
}
```
## 实现说明
| 层 | 文件 |
| --- | --- |
| Controller | `backend/src/main/java/com/unis/crm/controller/OpportunityController.java``GET /all` |
| Service | `OpportunityService#listAllOpportunities` / `OpportunityServiceImpl` |
| Mapper | `OpportunityMapper#selectAllOpportunities` |
| SQL | `backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml` |
| DTO | `backend/src/main/java/com/unis/crm/dto/opportunity/OpportunityFullDTO.java` |
要点:
- 查询语句未加 `archived` 过滤,返回包含已归档/已签单在内的全部商机;
- 未拼接数据权限条件(`opportunityVisibilityCondition`),即不受当前用户数据可见范围限制,因此**仅允许内部系统调用**
- 鉴权由 `OpportunityController#validateInternalSecret` 完成,并在 `application.yml` / `application-prod.yml``unisbase.security.permit-all-urls` 中放开 `/api/opportunities/all`
- 已添加 `@Log(type = "商机集成", value = "查询全量商机")` 审计日志,记录调用留痕;
- 阶段名称通过 `sys_dict_item``type_code = 'sj_xmjd'`)关联获取,归档商机即使字典项被禁用也能取到名称;
- 时间字段统一由数据库格式化后返回字符串,未推送 OMS 时 `omsPushTime` 返回空串。