feat(llm-tools): 为所有搜索工具添加总数和是否有更多数据返回
1. 为跟进、签到、客户、商机、待办、工作报告、拓展搜索工具新增总数统计和分页判断逻辑 2. 新增对应count查询的Mapper接口和XML实现 3. 完善数据权限拦截器对count方法的支持 4. 新增测试用例校验count语句不包含分页和排序main
parent
b5b01d4f79
commit
28c336fd13
|
|
@ -101,6 +101,7 @@
|
||||||
<workItem from="1787721570035" duration="18307000" />
|
<workItem from="1787721570035" duration="18307000" />
|
||||||
<workItem from="1788916111243" duration="625000" />
|
<workItem from="1788916111243" duration="625000" />
|
||||||
<workItem from="1789033867432" duration="17000" />
|
<workItem from="1789033867432" duration="17000" />
|
||||||
|
<workItem from="1789440226191" duration="937000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="修改定位信息 0323">
|
<task id="LOCAL-00001" summary="修改定位信息 0323">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,14 @@ public interface LlmMcpMapper {
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countWorkReports(
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("targetUserId") Long targetUserId,
|
||||||
|
@Param("tenantId") Long tenantId,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("status") String status);
|
||||||
|
|
||||||
List<Map<String, Object>> searchOpportunities(
|
List<Map<String, Object>> searchOpportunities(
|
||||||
@Param("ownerUserId") Long ownerUserId,
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
@Param("tenantId") Long tenantId,
|
@Param("tenantId") Long tenantId,
|
||||||
|
|
@ -34,6 +42,13 @@ public interface LlmMcpMapper {
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countOpportunities(
|
||||||
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
|
@Param("tenantId") Long tenantId,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("stage") String stage,
|
||||||
|
@Param("includeArchived") boolean includeArchived);
|
||||||
|
|
||||||
List<Map<String, Object>> universalSearchCustomers(
|
List<Map<String, Object>> universalSearchCustomers(
|
||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
@Param("ownerUserId") Long ownerUserId,
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
|
|
@ -226,6 +241,16 @@ public interface LlmMcpMapper {
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countTodos(
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("priority") String priority,
|
||||||
|
@Param("bizType") String bizType,
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
|
@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
List<Map<String, Object>> searchOrgUsers(
|
List<Map<String, Object>> searchOrgUsers(
|
||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
@Param("status") Integer status,
|
@Param("status") Integer status,
|
||||||
|
|
@ -257,6 +282,16 @@ public interface LlmMcpMapper {
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countCustomers(
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("source") String source,
|
||||||
|
@Param("industry") String industry,
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
|
@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
List<Map<String, Object>> searchCheckins(
|
List<Map<String, Object>> searchCheckins(
|
||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
@Param("status") String status,
|
@Param("status") String status,
|
||||||
|
|
@ -268,6 +303,15 @@ public interface LlmMcpMapper {
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countCheckins(
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("bizType") String bizType,
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
|
@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
List<Map<String, Object>> searchSalesExpansions(
|
List<Map<String, Object>> searchSalesExpansions(
|
||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
@Param("stage") String stage,
|
@Param("stage") String stage,
|
||||||
|
|
@ -279,6 +323,15 @@ public interface LlmMcpMapper {
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countSalesExpansions(
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("stage") String stage,
|
||||||
|
@Param("intentLevel") String intentLevel,
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
|
@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
List<Map<String, Object>> searchChannelExpansions(
|
List<Map<String, Object>> searchChannelExpansions(
|
||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
@Param("stage") String stage,
|
@Param("stage") String stage,
|
||||||
|
|
@ -290,6 +343,15 @@ public interface LlmMcpMapper {
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countChannelExpansions(
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("stage") String stage,
|
||||||
|
@Param("intentLevel") String intentLevel,
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
|
@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
List<Map<String, Object>> searchFollowups(
|
List<Map<String, Object>> searchFollowups(
|
||||||
@Param("bizType") String bizType,
|
@Param("bizType") String bizType,
|
||||||
@Param("keyword") String keyword,
|
@Param("keyword") String keyword,
|
||||||
|
|
@ -300,6 +362,14 @@ public interface LlmMcpMapper {
|
||||||
@Param("limit") int limit,
|
@Param("limit") int limit,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
long countFollowups(
|
||||||
|
@Param("bizType") String bizType,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("startDate") LocalDate startDate,
|
||||||
|
@Param("endDate") LocalDate endDate,
|
||||||
|
@Param("ownerUserId") Long ownerUserId,
|
||||||
|
@Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
Map<String, Object> selectCustomerDetail(@Param("id") Long id, @Param("tenantId") Long tenantId);
|
Map<String, Object> selectCustomerDetail(@Param("id") Long id, @Param("tenantId") Long tenantId);
|
||||||
|
|
||||||
Map<String, Object> selectOpportunityDetail(@Param("id") Long id, @Param("tenantId") Long tenantId);
|
Map<String, Object> selectOpportunityDetail(@Param("id") Long id, @Param("tenantId") Long tenantId);
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ public class McpDataPermissionInterceptor implements Interceptor {
|
||||||
validateParameters(methodName, boundSql.getParameterObject(), loginUser);
|
validateParameters(methodName, boundSql.getParameterObject(), loginUser);
|
||||||
|
|
||||||
CrmDataVisibilityService visibilityService = visibilityServiceProvider.getIfAvailable();
|
CrmDataVisibilityService visibilityService = visibilityServiceProvider.getIfAvailable();
|
||||||
if ("searchFollowups".equals(methodName)) {
|
if ("searchFollowups".equals(methodName) || "countFollowups".equals(methodName)) {
|
||||||
if (visibilityService == null) {
|
if (visibilityService == null) {
|
||||||
throw new BusinessException("MCP 数据权限服务不可用");
|
throw new BusinessException("MCP 数据权限服务不可用");
|
||||||
}
|
}
|
||||||
|
|
@ -293,6 +293,7 @@ public class McpDataPermissionInterceptor implements Interceptor {
|
||||||
static Set<String> policyMethodNames() {
|
static Set<String> policyMethodNames() {
|
||||||
java.util.LinkedHashSet<String> methodNames = new java.util.LinkedHashSet<>(POLICIES.keySet());
|
java.util.LinkedHashSet<String> methodNames = new java.util.LinkedHashSet<>(POLICIES.keySet());
|
||||||
methodNames.add("searchFollowups");
|
methodNames.add("searchFollowups");
|
||||||
|
methodNames.add("countFollowups");
|
||||||
methodNames.add("salesPerformance");
|
methodNames.add("salesPerformance");
|
||||||
return Set.copyOf(methodNames);
|
return Set.copyOf(methodNames);
|
||||||
}
|
}
|
||||||
|
|
@ -401,22 +402,22 @@ public class McpDataPermissionInterceptor implements Interceptor {
|
||||||
private static Map<String, QueryPolicy> buildPolicies() {
|
private static Map<String, QueryPolicy> buildPolicies() {
|
||||||
Map<String, QueryPolicy> policies = new LinkedHashMap<>();
|
Map<String, QueryPolicy> policies = new LinkedHashMap<>();
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_DAILY_REPORT, "r.user_id", null, false,
|
add(policies, CrmDataVisibilityService.RESOURCE_DAILY_REPORT, "r.user_id", null, false,
|
||||||
"searchWorkReports", "universalSearchWorkReports", "dashboardDailyReportMetric",
|
"searchWorkReports", "countWorkReports", "universalSearchWorkReports", "dashboardDailyReportMetric",
|
||||||
"selectWorkReportDetail", "selectWorkReportComments");
|
"selectWorkReportDetail", "selectWorkReportComments");
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_CHECKIN, "c.user_id", null, false,
|
add(policies, CrmDataVisibilityService.RESOURCE_CHECKIN, "c.user_id", null, false,
|
||||||
"universalSearchCheckins", "dashboardCheckinMetric", "checkinSummary",
|
"universalSearchCheckins", "countCheckins", "dashboardCheckinMetric", "checkinSummary",
|
||||||
"searchCheckins", "selectCheckinDetail");
|
"searchCheckins", "selectCheckinDetail");
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_CUSTOMER, "c.owner_user_id", null, false,
|
add(policies, CrmDataVisibilityService.RESOURCE_CUSTOMER, "c.owner_user_id", null, false,
|
||||||
"universalSearchCustomers", "dashboardCustomerMetric", "customerSummary",
|
"universalSearchCustomers", "countCustomers", "dashboardCustomerMetric", "customerSummary",
|
||||||
"searchCustomers", "selectCustomerDetail");
|
"searchCustomers", "selectCustomerDetail");
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_EXPANSION, "s.owner_user_id", null, false,
|
add(policies, CrmDataVisibilityService.RESOURCE_EXPANSION, "s.owner_user_id", null, false,
|
||||||
"universalSearchSalesExpansions", "salesExpansionSummary", "searchSalesExpansions",
|
"universalSearchSalesExpansions", "countSalesExpansions", "salesExpansionSummary", "searchSalesExpansions",
|
||||||
"selectSalesExpansionDetail", "selectSalesExpansionFollowups");
|
"selectSalesExpansionDetail", "selectSalesExpansionFollowups");
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_EXPANSION, "c.owner_user_id", null, false,
|
add(policies, CrmDataVisibilityService.RESOURCE_EXPANSION, "c.owner_user_id", null, false,
|
||||||
"universalSearchChannelExpansions", "channelExpansionSummary", "searchChannelExpansions",
|
"universalSearchChannelExpansions", "countChannelExpansions", "channelExpansionSummary", "searchChannelExpansions",
|
||||||
"selectChannelExpansionDetail", "selectChannelExpansionContacts", "selectChannelExpansionFollowups");
|
"selectChannelExpansionDetail", "selectChannelExpansionContacts", "selectChannelExpansionFollowups");
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_WORK, "t.user_id", null, false,
|
add(policies, CrmDataVisibilityService.RESOURCE_WORK, "t.user_id", null, false,
|
||||||
"universalSearchTodos", "searchTodos", "todoSummary", "selectTodoDetail");
|
"universalSearchTodos", "countTodos", "searchTodos", "todoSummary", "selectTodoDetail");
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_WORK, "l.operator_user_id", null, false,
|
add(policies, CrmDataVisibilityService.RESOURCE_WORK, "l.operator_user_id", null, false,
|
||||||
"universalSearchActivities");
|
"universalSearchActivities");
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_OPPORTUNITY, "o.owner_user_id",
|
add(policies, CrmDataVisibilityService.RESOURCE_OPPORTUNITY, "o.owner_user_id",
|
||||||
|
|
@ -428,7 +429,7 @@ public class McpDataPermissionInterceptor implements Interceptor {
|
||||||
|
|
||||||
add(policies, CrmDataVisibilityService.RESOURCE_OPPORTUNITY, "o.owner_user_id",
|
add(policies, CrmDataVisibilityService.RESOURCE_OPPORTUNITY, "o.owner_user_id",
|
||||||
"o.project_ownership_location", true,
|
"o.project_ownership_location", true,
|
||||||
"searchOpportunities", "universalSearchOpportunities", "dashboardNewOpportunityMetric",
|
"searchOpportunities", "countOpportunities", "universalSearchOpportunities", "dashboardNewOpportunityMetric",
|
||||||
"dashboardWonOpportunityMetric", "opportunityFunnel", "opportunityTrend",
|
"dashboardWonOpportunityMetric", "opportunityFunnel", "opportunityTrend",
|
||||||
"selectOpportunityDetail", "selectCustomerOpportunities", "selectOpportunityFollowups",
|
"selectOpportunityDetail", "selectCustomerOpportunities", "selectOpportunityFollowups",
|
||||||
"selectSalesExpansionOpportunities", "selectChannelExpansionOpportunities");
|
"selectSalesExpansionOpportunities", "selectChannelExpansionOpportunities");
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,17 @@ public class CrmCheckinSearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
@Override
|
@Override
|
||||||
protected Object handle(Map<String, Object> arguments) {
|
protected Object handle(Map<String, Object> arguments) {
|
||||||
QueryContext context = buildContext(arguments);
|
QueryContext context = buildContext(arguments);
|
||||||
|
long total = llmMcpMapper.countCheckins(
|
||||||
|
getString(arguments, "keyword"),
|
||||||
|
getString(arguments, "status"),
|
||||||
|
getString(arguments, "bizType"),
|
||||||
|
context.startDate(),
|
||||||
|
context.endDate(),
|
||||||
|
context.ownerUserId(),
|
||||||
|
context.tenantId());
|
||||||
Map<String, Object> result = wrap(context);
|
Map<String, Object> result = wrap(context);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("hasMore", (long) context.offset() + context.pageSize() < total);
|
||||||
result.put("rows", sanitizeRows(llmMcpMapper.searchCheckins(
|
result.put("rows", sanitizeRows(llmMcpMapper.searchCheckins(
|
||||||
getString(arguments, "keyword"),
|
getString(arguments, "keyword"),
|
||||||
getString(arguments, "status"),
|
getString(arguments, "status"),
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,18 @@ public class CrmDetailSearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
@Override
|
@Override
|
||||||
protected Object handle(Map<String, Object> arguments) {
|
protected Object handle(Map<String, Object> arguments) {
|
||||||
QueryContext context = buildContext(arguments);
|
QueryContext context = buildContext(arguments);
|
||||||
|
long total = llmMcpMapper.countCustomers(
|
||||||
|
getString(arguments, "keyword"),
|
||||||
|
getString(arguments, "status"),
|
||||||
|
getString(arguments, "source"),
|
||||||
|
getString(arguments, "industry"),
|
||||||
|
context.startDate(),
|
||||||
|
context.endDate(),
|
||||||
|
context.ownerUserId(),
|
||||||
|
context.tenantId());
|
||||||
Map<String, Object> result = wrap(context);
|
Map<String, Object> result = wrap(context);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("hasMore", (long) context.offset() + context.pageSize() < total);
|
||||||
result.put("rows", sanitizeRows(llmMcpMapper.searchCustomers(
|
result.put("rows", sanitizeRows(llmMcpMapper.searchCustomers(
|
||||||
getString(arguments, "keyword"),
|
getString(arguments, "keyword"),
|
||||||
getString(arguments, "status"),
|
getString(arguments, "status"),
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,40 @@ public class CrmExpansionSearchToolProvider extends PermissionedMcpToolProvider
|
||||||
if (!StringUtils.hasText(expansionType)) {
|
if (!StringUtils.hasText(expansionType)) {
|
||||||
expansionType = "all";
|
expansionType = "all";
|
||||||
}
|
}
|
||||||
|
long total = switch (expansionType) {
|
||||||
|
case "sales" -> llmMcpMapper.countSalesExpansions(
|
||||||
|
keyword,
|
||||||
|
stage,
|
||||||
|
intentLevel,
|
||||||
|
context.startDate(),
|
||||||
|
context.endDate(),
|
||||||
|
context.ownerUserId(),
|
||||||
|
context.tenantId());
|
||||||
|
case "channel" -> llmMcpMapper.countChannelExpansions(
|
||||||
|
keyword,
|
||||||
|
stage,
|
||||||
|
intentLevel,
|
||||||
|
context.startDate(),
|
||||||
|
context.endDate(),
|
||||||
|
context.ownerUserId(),
|
||||||
|
context.tenantId());
|
||||||
|
default -> llmMcpMapper.countSalesExpansions(
|
||||||
|
keyword,
|
||||||
|
stage,
|
||||||
|
intentLevel,
|
||||||
|
context.startDate(),
|
||||||
|
context.endDate(),
|
||||||
|
context.ownerUserId(),
|
||||||
|
context.tenantId())
|
||||||
|
+ llmMcpMapper.countChannelExpansions(
|
||||||
|
keyword,
|
||||||
|
stage,
|
||||||
|
intentLevel,
|
||||||
|
context.startDate(),
|
||||||
|
context.endDate(),
|
||||||
|
context.ownerUserId(),
|
||||||
|
context.tenantId());
|
||||||
|
};
|
||||||
List<Map<String, Object>> rows = switch (expansionType) {
|
List<Map<String, Object>> rows = switch (expansionType) {
|
||||||
case "sales" -> normalizeRows(llmMcpMapper.searchSalesExpansions(
|
case "sales" -> normalizeRows(llmMcpMapper.searchSalesExpansions(
|
||||||
keyword,
|
keyword,
|
||||||
|
|
@ -85,6 +119,8 @@ public class CrmExpansionSearchToolProvider extends PermissionedMcpToolProvider
|
||||||
default -> mergedRows(keyword, stage, intentLevel, context);
|
default -> mergedRows(keyword, stage, intentLevel, context);
|
||||||
};
|
};
|
||||||
Map<String, Object> result = wrap(context);
|
Map<String, Object> result = wrap(context);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("hasMore", (long) context.offset() + context.pageSize() < total);
|
||||||
result.put("rows", rows);
|
result.put("rows", rows);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,16 @@ public class CrmFollowupSearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
if (!StringUtils.hasText(bizType)) {
|
if (!StringUtils.hasText(bizType)) {
|
||||||
bizType = "all";
|
bizType = "all";
|
||||||
}
|
}
|
||||||
|
long total = llmMcpMapper.countFollowups(
|
||||||
|
bizType,
|
||||||
|
getString(arguments, "keyword"),
|
||||||
|
context.startDate(),
|
||||||
|
context.endDate(),
|
||||||
|
context.ownerUserId(),
|
||||||
|
context.tenantId());
|
||||||
Map<String, Object> result = wrap(context);
|
Map<String, Object> result = wrap(context);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("hasMore", (long) context.offset() + context.pageSize() < total);
|
||||||
result.put("rows", sanitizeRows(llmMcpMapper.searchFollowups(
|
result.put("rows", sanitizeRows(llmMcpMapper.searchFollowups(
|
||||||
bizType,
|
bizType,
|
||||||
getString(arguments, "keyword"),
|
getString(arguments, "keyword"),
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,16 @@ public class CrmTodoSearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
int page = getPage(arguments);
|
int page = getPage(arguments);
|
||||||
Long tenantId = tenantProvider.getCurrentTenantId();
|
Long tenantId = tenantProvider.getCurrentTenantId();
|
||||||
Long ownerUserId = getLong(arguments, "ownerUserId");
|
Long ownerUserId = getLong(arguments, "ownerUserId");
|
||||||
|
int offset = getOffset(page, pageSize);
|
||||||
|
long total = llmMcpMapper.countTodos(
|
||||||
|
getString(arguments, "keyword"),
|
||||||
|
getString(arguments, "status"),
|
||||||
|
getString(arguments, "priority"),
|
||||||
|
getString(arguments, "bizType"),
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
ownerUserId,
|
||||||
|
tenantId);
|
||||||
List<Map<String, Object>> rows = llmMcpMapper.searchTodos(
|
List<Map<String, Object>> rows = llmMcpMapper.searchTodos(
|
||||||
getString(arguments, "keyword"),
|
getString(arguments, "keyword"),
|
||||||
getString(arguments, "status"),
|
getString(arguments, "status"),
|
||||||
|
|
@ -80,7 +90,7 @@ public class CrmTodoSearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
ownerUserId,
|
ownerUserId,
|
||||||
tenantId,
|
tenantId,
|
||||||
pageSize,
|
pageSize,
|
||||||
getOffset(page, pageSize));
|
offset);
|
||||||
|
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("ownerUserId", ownerUserId);
|
result.put("ownerUserId", ownerUserId);
|
||||||
|
|
@ -89,6 +99,8 @@ public class CrmTodoSearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
result.put("endDate", endDate == null ? null : endDate.toString());
|
result.put("endDate", endDate == null ? null : endDate.toString());
|
||||||
result.put("page", page);
|
result.put("page", page);
|
||||||
result.put("pageSize", pageSize);
|
result.put("pageSize", pageSize);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("hasMore", (long) offset + pageSize < total);
|
||||||
result.put("rows", sanitizeRows(rows));
|
result.put("rows", sanitizeRows(rows));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,13 @@ public class OpportunitySearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
boolean includeArchived = getBoolean(arguments, "includeArchived", false);
|
boolean includeArchived = getBoolean(arguments, "includeArchived", false);
|
||||||
Long tenantId = tenantProvider.getCurrentTenantId();
|
Long tenantId = tenantProvider.getCurrentTenantId();
|
||||||
|
|
||||||
|
int offset = getOffset(page, pageSize);
|
||||||
|
long total = llmMcpMapper.countOpportunities(
|
||||||
|
ownerUserId,
|
||||||
|
tenantId,
|
||||||
|
keyword,
|
||||||
|
stage,
|
||||||
|
includeArchived);
|
||||||
List<Map<String, Object>> rows = sanitizeRows(llmMcpMapper.searchOpportunities(
|
List<Map<String, Object>> rows = sanitizeRows(llmMcpMapper.searchOpportunities(
|
||||||
ownerUserId,
|
ownerUserId,
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|
@ -70,12 +77,14 @@ public class OpportunitySearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
stage,
|
stage,
|
||||||
includeArchived,
|
includeArchived,
|
||||||
pageSize,
|
pageSize,
|
||||||
getOffset(page, pageSize)));
|
offset));
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("ownerUserId", ownerUserId);
|
result.put("ownerUserId", ownerUserId);
|
||||||
result.put("tenantId", tenantId);
|
result.put("tenantId", tenantId);
|
||||||
result.put("page", page);
|
result.put("page", page);
|
||||||
result.put("pageSize", pageSize);
|
result.put("pageSize", pageSize);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("hasMore", (long) offset + pageSize < total);
|
||||||
result.put("rows", rows);
|
result.put("rows", rows);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,15 @@ public class WorkReportSearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
String keyword = getString(arguments, "keyword");
|
String keyword = getString(arguments, "keyword");
|
||||||
String status = getString(arguments, "status");
|
String status = getString(arguments, "status");
|
||||||
Long tenantId = tenantProvider.getCurrentTenantId();
|
Long tenantId = tenantProvider.getCurrentTenantId();
|
||||||
|
int offset = getOffset(page, pageSize);
|
||||||
|
|
||||||
|
long total = llmMcpMapper.countWorkReports(
|
||||||
|
dateRange.startDate(),
|
||||||
|
dateRange.endDate(),
|
||||||
|
targetUserId,
|
||||||
|
tenantId,
|
||||||
|
keyword,
|
||||||
|
status);
|
||||||
List<Map<String, Object>> rows = sanitizeRows(llmMcpMapper.searchWorkReports(
|
List<Map<String, Object>> rows = sanitizeRows(llmMcpMapper.searchWorkReports(
|
||||||
dateRange.startDate(),
|
dateRange.startDate(),
|
||||||
dateRange.endDate(),
|
dateRange.endDate(),
|
||||||
|
|
@ -76,13 +84,15 @@ public class WorkReportSearchToolProvider extends PermissionedMcpToolProvider {
|
||||||
keyword,
|
keyword,
|
||||||
status,
|
status,
|
||||||
pageSize,
|
pageSize,
|
||||||
getOffset(page, pageSize)));
|
offset));
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("startDate", dateRange.startDate().toString());
|
result.put("startDate", dateRange.startDate().toString());
|
||||||
result.put("endDate", dateRange.endDate().toString());
|
result.put("endDate", dateRange.endDate().toString());
|
||||||
result.put("userId", targetUserId);
|
result.put("userId", targetUserId);
|
||||||
result.put("page", page);
|
result.put("page", page);
|
||||||
result.put("pageSize", pageSize);
|
result.put("pageSize", pageSize);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("hasMore", (long) offset + pageSize < total);
|
||||||
result.put("rows", rows);
|
result.put("rows", rows);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,36 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countWorkReports" resultType="long">
|
||||||
|
select count(*)
|
||||||
|
from work_daily_report r
|
||||||
|
left join sys_user u on u.user_id = r.user_id and coalesce(u.is_deleted, 0) = 0
|
||||||
|
where r.report_date between #{startDate} and #{endDate}
|
||||||
|
<if test="targetUserId != null">
|
||||||
|
and r.user_id = #{targetUserId}
|
||||||
|
</if>
|
||||||
|
<if test="tenantId != null">
|
||||||
|
and exists (
|
||||||
|
select 1
|
||||||
|
from sys_tenant_user tu
|
||||||
|
where tu.user_id = r.user_id
|
||||||
|
and tu.tenant_id = #{tenantId}
|
||||||
|
and coalesce(tu.is_deleted, 0) = 0
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(u.display_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(u.username, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(r.work_content, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(r.tomorrow_plan, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="status != null and status != ''">
|
||||||
|
and coalesce(r.status, 'submitted') = #{status}
|
||||||
|
</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="searchOpportunities" resultType="java.util.LinkedHashMap">
|
<select id="searchOpportunities" resultType="java.util.LinkedHashMap">
|
||||||
select
|
select
|
||||||
o.id as "id",
|
o.id as "id",
|
||||||
|
|
@ -161,6 +191,46 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countOpportunities" resultType="long">
|
||||||
|
select count(*)
|
||||||
|
from crm_opportunity o
|
||||||
|
left join crm_customer c on c.id = o.customer_id
|
||||||
|
left join sys_user u on u.user_id = o.owner_user_id and coalesce(u.is_deleted, 0) = 0
|
||||||
|
left join cnarea project_ownership_area
|
||||||
|
on project_ownership_area.level = 1
|
||||||
|
and project_ownership_area.area_code = o.project_ownership_location
|
||||||
|
where 1 = 1
|
||||||
|
<if test="ownerUserId != null">
|
||||||
|
and o.owner_user_id = #{ownerUserId}
|
||||||
|
</if>
|
||||||
|
<if test="!includeArchived">
|
||||||
|
and coalesce(o.archived, false) = false
|
||||||
|
</if>
|
||||||
|
<if test="tenantId != null">
|
||||||
|
and exists (
|
||||||
|
select 1
|
||||||
|
from sys_tenant_user tu
|
||||||
|
where tu.user_id = o.owner_user_id
|
||||||
|
and tu.tenant_id = #{tenantId}
|
||||||
|
and coalesce(tu.is_deleted, 0) = 0
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(o.opportunity_code, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(o.opportunity_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.customer_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(o.project_location, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(o.project_ownership_location, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(project_ownership_area.short_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(o.product_type, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="stage != null and stage != ''">
|
||||||
|
and o.stage = #{stage}
|
||||||
|
</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="universalSearch" resultType="java.util.LinkedHashMap">
|
<select id="universalSearch" resultType="java.util.LinkedHashMap">
|
||||||
with search_rows as (
|
with search_rows as (
|
||||||
select
|
select
|
||||||
|
|
@ -1159,6 +1229,28 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countTodos" resultType="long">
|
||||||
|
select count(*)
|
||||||
|
from work_todo t
|
||||||
|
left join sys_user u on u.user_id = t.user_id and coalesce(u.is_deleted, 0) = 0
|
||||||
|
where 1 = 1
|
||||||
|
<if test="ownerUserId != null">and t.user_id = #{ownerUserId}</if>
|
||||||
|
<if test="status != null and status != ''">and t.status = #{status}</if>
|
||||||
|
<if test="priority != null and priority != ''">and t.priority = #{priority}</if>
|
||||||
|
<if test="bizType != null and bizType != ''">and t.biz_type = #{bizType}</if>
|
||||||
|
<if test="startDate != null">and t.due_date::date >= #{startDate}</if>
|
||||||
|
<if test="endDate != null">and t.due_date::date <= #{endDate}</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = t.user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(t.title, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(t.biz_type, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(u.display_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(u.username, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="searchOrgUsers" resultType="java.util.LinkedHashMap">
|
<select id="searchOrgUsers" resultType="java.util.LinkedHashMap">
|
||||||
select
|
select
|
||||||
u.user_id as "userId",
|
u.user_id as "userId",
|
||||||
|
|
@ -1311,6 +1403,29 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countCustomers" resultType="long">
|
||||||
|
select count(*)
|
||||||
|
from crm_customer c
|
||||||
|
where 1 = 1
|
||||||
|
<if test="ownerUserId != null">and c.owner_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="startDate != null">and c.created_at::date >= #{startDate}</if>
|
||||||
|
<if test="endDate != null">and c.created_at::date <= #{endDate}</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = c.owner_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(c.customer_code, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.customer_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.industry, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.province, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.city, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.remark, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="status != null and status != ''">and c.status = #{status}</if>
|
||||||
|
<if test="source != null and source != ''">and c.source = #{source}</if>
|
||||||
|
<if test="industry != null and industry != ''">and c.industry = #{industry}</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="searchCheckins" resultType="java.util.LinkedHashMap">
|
<select id="searchCheckins" resultType="java.util.LinkedHashMap">
|
||||||
select
|
select
|
||||||
c.id as "id",
|
c.id as "id",
|
||||||
|
|
@ -1348,6 +1463,27 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countCheckins" resultType="long">
|
||||||
|
select count(*)
|
||||||
|
from work_checkin c
|
||||||
|
where 1 = 1
|
||||||
|
<if test="ownerUserId != null">and c.user_id = #{ownerUserId}</if>
|
||||||
|
<if test="startDate != null">and c.checkin_date >= #{startDate}</if>
|
||||||
|
<if test="endDate != null">and c.checkin_date <= #{endDate}</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = c.user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(c.user_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.dept_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.biz_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.location_text, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.remark, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="status != null and status != ''">and c.status = #{status}</if>
|
||||||
|
<if test="bizType != null and bizType != ''">and c.biz_type = #{bizType}</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="searchExpansions" resultType="java.util.LinkedHashMap">
|
<select id="searchExpansions" resultType="java.util.LinkedHashMap">
|
||||||
with expansion_rows as (
|
with expansion_rows as (
|
||||||
select
|
select
|
||||||
|
|
@ -1466,6 +1602,28 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countSalesExpansions" resultType="long">
|
||||||
|
select count(*)
|
||||||
|
from crm_sales_expansion s
|
||||||
|
where 1 = 1
|
||||||
|
<if test="ownerUserId != null">and s.owner_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="stage != null and stage != ''">and s.stage = #{stage}</if>
|
||||||
|
<if test="intentLevel != null and intentLevel != ''">and s.intent_level = #{intentLevel}</if>
|
||||||
|
<if test="startDate != null">and s.created_at::date >= #{startDate}</if>
|
||||||
|
<if test="endDate != null">and s.created_at::date <= #{endDate}</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(s.employee_no, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(s.candidate_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(s.mobile, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(s.target_dept, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(s.industry, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(s.remark, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = s.owner_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="searchChannelExpansions" resultType="java.util.LinkedHashMap">
|
<select id="searchChannelExpansions" resultType="java.util.LinkedHashMap">
|
||||||
select
|
select
|
||||||
'channel' as "expansionType",
|
'channel' as "expansionType",
|
||||||
|
|
@ -1504,6 +1662,30 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countChannelExpansions" resultType="long">
|
||||||
|
select count(*)
|
||||||
|
from crm_channel_expansion c
|
||||||
|
where 1 = 1
|
||||||
|
<if test="ownerUserId != null">and c.owner_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="stage != null and stage != ''">and c.stage = #{stage}</if>
|
||||||
|
<if test="intentLevel != null and intentLevel != ''">and c.intent_level = #{intentLevel}</if>
|
||||||
|
<if test="startDate != null">and c.created_at::date >= #{startDate}</if>
|
||||||
|
<if test="endDate != null">and c.created_at::date <= #{endDate}</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(c.channel_code, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.channel_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.province, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.city, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.channel_industry, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.contact_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.contact_mobile, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.remark, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = c.owner_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="searchFollowups" resultType="java.util.LinkedHashMap">
|
<select id="searchFollowups" resultType="java.util.LinkedHashMap">
|
||||||
with followup_rows as (
|
with followup_rows as (
|
||||||
select
|
select
|
||||||
|
|
@ -1592,6 +1774,76 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countFollowups" resultType="long">
|
||||||
|
with followup_rows as (
|
||||||
|
select
|
||||||
|
'opportunity' as "bizType",
|
||||||
|
f.id as "id",
|
||||||
|
f.opportunity_id as "bizId",
|
||||||
|
coalesce(o.opportunity_name, '') as "bizName",
|
||||||
|
f.followup_type as "followupType",
|
||||||
|
left(coalesce(f.content, ''), 800) as "content",
|
||||||
|
left(coalesce(f.next_action, ''), 300) as "nextAction",
|
||||||
|
f.followup_user_id as "userId",
|
||||||
|
f.followup_time as "followupTime",
|
||||||
|
'OPPORTUNITY' as "scopeResourceType",
|
||||||
|
o.owner_user_id as "scopeOwnerUserId",
|
||||||
|
o.project_ownership_location as "scopeAreaCode",
|
||||||
|
o.pre_sales_id as "scopePreSalesId",
|
||||||
|
o.pre_sales_name as "scopePreSalesName"
|
||||||
|
from crm_opportunity_followup f
|
||||||
|
join crm_opportunity o on o.id = f.opportunity_id
|
||||||
|
where #{bizType} in ('all', 'opportunity')
|
||||||
|
<if test="ownerUserId != null">and f.followup_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="startDate != null">and f.followup_time::date >= #{startDate}</if>
|
||||||
|
<if test="endDate != null">and f.followup_time::date <= #{endDate}</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(o.opportunity_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(f.content, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(f.next_action, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = f.followup_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
f.biz_type as "bizType",
|
||||||
|
f.id as "id",
|
||||||
|
f.biz_id as "bizId",
|
||||||
|
case when f.biz_type = 'sales' then coalesce(s.candidate_name, '') else coalesce(c.channel_name, '') end as "bizName",
|
||||||
|
f.followup_type as "followupType",
|
||||||
|
left(coalesce(f.content, ''), 800) as "content",
|
||||||
|
left(coalesce(f.next_action, f.next_plan, ''), 300) as "nextAction",
|
||||||
|
f.followup_user_id as "userId",
|
||||||
|
f.followup_time as "followupTime",
|
||||||
|
'EXPANSION' as "scopeResourceType",
|
||||||
|
case when f.biz_type = 'sales' then s.owner_user_id else c.owner_user_id end as "scopeOwnerUserId",
|
||||||
|
null::varchar as "scopeAreaCode",
|
||||||
|
null::bigint as "scopePreSalesId",
|
||||||
|
null::varchar as "scopePreSalesName"
|
||||||
|
from crm_expansion_followup f
|
||||||
|
left join crm_sales_expansion s on s.id = f.biz_id and f.biz_type = 'sales'
|
||||||
|
left join crm_channel_expansion c on c.id = f.biz_id and f.biz_type = 'channel'
|
||||||
|
where #{bizType} in ('all', 'sales', 'channel')
|
||||||
|
<if test="ownerUserId != null">and f.followup_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="startDate != null">and f.followup_time::date >= #{startDate}</if>
|
||||||
|
<if test="endDate != null">and f.followup_time::date <= #{endDate}</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (
|
||||||
|
coalesce(s.candidate_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(c.channel_name, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(f.content, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(f.next_action, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
or coalesce(f.next_plan, '') ilike concat('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = f.followup_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
)
|
||||||
|
select count(*) from followup_rows
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectCustomerDetail" resultType="java.util.LinkedHashMap">
|
<select id="selectCustomerDetail" resultType="java.util.LinkedHashMap">
|
||||||
select
|
select
|
||||||
c.*,
|
c.*,
|
||||||
|
|
@ -1885,4 +2137,86 @@
|
||||||
offset #{offset}
|
offset #{offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 渠道统计分析:按认证级别(tz_rzjb字典)分组,全量无分页 -->
|
||||||
|
<select id="channelTierDistribution" resultType="java.util.LinkedHashMap">
|
||||||
|
select
|
||||||
|
coalesce(nullif(btrim(ti.item_label), ''), '未分级') as "tierName",
|
||||||
|
coalesce(ti.item_value, '') as "tierCode",
|
||||||
|
count(c.id)::bigint as "channelCount",
|
||||||
|
count(case when c.intent_level = 'high' then 1 end)::bigint as "highIntentCount",
|
||||||
|
count(case when c.stage = 'won' then 1 end)::bigint as "wonCount",
|
||||||
|
count(case when coalesce(c.has_desktop_exp, false) = true then 1 end)::bigint as "desktopExpCount"
|
||||||
|
from crm_channel_expansion c
|
||||||
|
left join sys_dict_item ti
|
||||||
|
on ti.type_code = 'tz_rzjb'
|
||||||
|
and ti.item_value = c.certification_level
|
||||||
|
and coalesce(ti.is_deleted, 0) = 0
|
||||||
|
and coalesce(ti.status, 1) = 1
|
||||||
|
where c.created_at::date between #{startDate} and #{endDate}
|
||||||
|
<if test="ownerUserId != null">and c.owner_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = c.owner_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
group by coalesce(nullif(btrim(ti.item_label), ''), '未分级'), coalesce(ti.item_value, '')
|
||||||
|
order by "channelCount" desc, "tierName" asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 渠道统计分析:按合作意向(intent_level)分组,全量无分页 -->
|
||||||
|
<select id="channelIntentDistribution" resultType="java.util.LinkedHashMap">
|
||||||
|
select
|
||||||
|
coalesce(c.intent_level, 'unknown') as "intentKey",
|
||||||
|
case lower(coalesce(c.intent_level, 'unknown'))
|
||||||
|
when 'high' then '高意向'
|
||||||
|
when 'medium' then '中意向'
|
||||||
|
when 'low' then '低意向'
|
||||||
|
else '未标注'
|
||||||
|
end as "intentName",
|
||||||
|
count(1)::bigint as "channelCount"
|
||||||
|
from crm_channel_expansion c
|
||||||
|
where c.created_at::date between #{startDate} and #{endDate}
|
||||||
|
<if test="ownerUserId != null">and c.owner_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = c.owner_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
group by c.intent_level
|
||||||
|
order by "channelCount" desc, "intentName" asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 渠道统计分析:覆盖省份/城市,基于覆盖率从表 crm_channel_expansion_coverage,全量无分页 -->
|
||||||
|
<select id="channelCoverageSummary" resultType="java.util.LinkedHashMap">
|
||||||
|
select
|
||||||
|
coalesce(cov.province, '未知') as "province",
|
||||||
|
count(distinct c.id)::bigint as "channelCount",
|
||||||
|
count(distinct nullif(btrim(cov.city), ''))::bigint as "cityCount"
|
||||||
|
from crm_channel_expansion_coverage cov
|
||||||
|
join crm_channel_expansion c on c.id = cov.channel_id
|
||||||
|
where c.created_at::date between #{startDate} and #{endDate}
|
||||||
|
<if test="ownerUserId != null">and c.owner_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = c.owner_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
group by cov.province
|
||||||
|
order by "channelCount" desc, "province" asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 渠道统计分析:按年度营业额分档,全量无分页 -->
|
||||||
|
<select id="channelRevenueTier" resultType="java.util.LinkedHashMap">
|
||||||
|
select
|
||||||
|
case
|
||||||
|
when annual_revenue is null then '未填写'
|
||||||
|
when annual_revenue < 100 then '<100万'
|
||||||
|
when annual_revenue < 500 then '100-500万'
|
||||||
|
when annual_revenue < 1000 then '500-1000万'
|
||||||
|
else '1000万+'
|
||||||
|
end as "revenueTier",
|
||||||
|
count(1)::bigint as "channelCount",
|
||||||
|
coalesce(round(avg(annual_revenue)), 0)::bigint as "avgRevenue"
|
||||||
|
from crm_channel_expansion c
|
||||||
|
where c.created_at::date between #{startDate} and #{endDate}
|
||||||
|
<if test="ownerUserId != null">and c.owner_user_id = #{ownerUserId}</if>
|
||||||
|
<if test="tenantId != null">and exists (select 1 from sys_tenant_user tu where tu.user_id = c.owner_user_id and tu.tenant_id = #{tenantId} and coalesce(tu.is_deleted, 0) = 0)</if>
|
||||||
|
group by case
|
||||||
|
when annual_revenue is null then '未填写'
|
||||||
|
when annual_revenue < 100 then '<100万'
|
||||||
|
when annual_revenue < 500 then '100-500万'
|
||||||
|
when annual_revenue < 1000 then '500-1000万'
|
||||||
|
else '1000万+'
|
||||||
|
end
|
||||||
|
order by "channelCount" desc
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ class McpDataPermissionInterceptorTest {
|
||||||
CrmDataVisibilityService.RESOURCE_DAILY_REPORT, visibility,
|
CrmDataVisibilityService.RESOURCE_DAILY_REPORT, visibility,
|
||||||
CrmDataVisibilityService.RESOURCE_CHECKIN, visibility));
|
CrmDataVisibilityService.RESOURCE_CHECKIN, visibility));
|
||||||
} else {
|
} else {
|
||||||
String condition = "searchFollowups".equals(methodName)
|
String condition = ("searchFollowups".equals(methodName) || "countFollowups".equals(methodName))
|
||||||
? interceptor.buildFollowupVisibilityCondition(visibility, visibility, loginUser)
|
? interceptor.buildFollowupVisibilityCondition(visibility, visibility, loginUser)
|
||||||
: interceptor.buildVisibilityCondition(methodName, visibility, loginUser);
|
: interceptor.buildVisibilityCondition(methodName, visibility, loginUser);
|
||||||
rewritten = interceptor.rewriteSql(boundSql.getSql(), condition);
|
rewritten = interceptor.rewriteSql(boundSql.getSql(), condition);
|
||||||
|
|
@ -168,6 +168,35 @@ class McpDataPermissionInterceptorTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void everyCountStatementShouldNotPaginate() throws Exception {
|
||||||
|
String[] countMethods = {
|
||||||
|
"countWorkReports",
|
||||||
|
"countOpportunities",
|
||||||
|
"countTodos",
|
||||||
|
"countCustomers",
|
||||||
|
"countCheckins",
|
||||||
|
"countSalesExpansions",
|
||||||
|
"countChannelExpansions",
|
||||||
|
"countFollowups"};
|
||||||
|
Configuration configuration = new Configuration();
|
||||||
|
Path mapperPath = Path.of("src/main/resources/mapper/llm/LlmMcpMapper.xml");
|
||||||
|
try (InputStream inputStream = Files.newInputStream(mapperPath)) {
|
||||||
|
new XMLMapperBuilder(inputStream, configuration, mapperPath.toString(), configuration.getSqlFragments()).parse();
|
||||||
|
}
|
||||||
|
for (String methodName : countMethods) {
|
||||||
|
BoundSql boundSql = configuration.getMappedStatement(
|
||||||
|
"com.unis.crm.llm.mapper.LlmMcpMapper." + methodName)
|
||||||
|
.getBoundSql(allParameters());
|
||||||
|
String sql = boundSql.getSql().toLowerCase();
|
||||||
|
assertTrue(sql.startsWith("select count(*)") || sql.contains("select count(*)"),
|
||||||
|
methodName + " should count all rows");
|
||||||
|
assertTrue(!sql.contains(" limit ") && !sql.contains(" offset "),
|
||||||
|
methodName + " must not apply pagination to count");
|
||||||
|
assertTrue(!sql.contains("order by"), methodName + " must not order the count query");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void everyMcpMapperMethodShouldBeProtectedOrExplicitlyTenantSafe() {
|
void everyMcpMapperMethodShouldBeProtectedOrExplicitlyTenantSafe() {
|
||||||
Set<String> tenantSafeMethods = Set.of(
|
Set<String> tenantSafeMethods = Set.of(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue