feat(llm): add channel analytics report feature

新增渠道统计分析报表功能,包含认证级别分布、合作意向、覆盖省市、年度营收分档统计,补充对应Mapper接口、数据权限配置与工具支持
main
kangwenjing 2026-09-15 16:16:29 +08:00
parent 28c336fd13
commit 52a35e5b20
6 changed files with 68 additions and 9 deletions

View File

@ -4,7 +4,21 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</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/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/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/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/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/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/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/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" />
</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" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -102,6 +116,7 @@
<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" /> <workItem from="1789440226191" duration="937000" />
<workItem from="1789459774783" duration="30000" />
</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

@ -224,6 +224,30 @@ public interface LlmMcpMapper {
@Param("limit") int limit, @Param("limit") int limit,
@Param("offset") int offset); @Param("offset") int offset);
List<Map<String, Object>> channelTierDistribution(
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("ownerUserId") Long ownerUserId,
@Param("tenantId") Long tenantId);
List<Map<String, Object>> channelIntentDistribution(
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("ownerUserId") Long ownerUserId,
@Param("tenantId") Long tenantId);
List<Map<String, Object>> channelCoverageSummary(
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("ownerUserId") Long ownerUserId,
@Param("tenantId") Long tenantId);
List<Map<String, Object>> channelRevenueTier(
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
@Param("ownerUserId") Long ownerUserId,
@Param("tenantId") Long tenantId);
Map<String, Object> selectWorkTodayStatus( Map<String, Object> selectWorkTodayStatus(
@Param("targetUserId") Long targetUserId, @Param("targetUserId") Long targetUserId,
@Param("tenantId") Long tenantId, @Param("tenantId") Long tenantId,

View File

@ -415,7 +415,8 @@ public class McpDataPermissionInterceptor implements Interceptor {
"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", "countChannelExpansions", "channelExpansionSummary", "searchChannelExpansions", "universalSearchChannelExpansions", "countChannelExpansions", "channelExpansionSummary", "searchChannelExpansions",
"selectChannelExpansionDetail", "selectChannelExpansionContacts", "selectChannelExpansionFollowups"); "selectChannelExpansionDetail", "selectChannelExpansionContacts", "selectChannelExpansionFollowups",
"channelTierDistribution", "channelIntentDistribution", "channelCoverageSummary", "channelRevenueTier");
add(policies, CrmDataVisibilityService.RESOURCE_WORK, "t.user_id", null, false, add(policies, CrmDataVisibilityService.RESOURCE_WORK, "t.user_id", null, false,
"universalSearchTodos", "countTodos", "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,

View File

@ -55,6 +55,7 @@ public class CrmReportCatalogToolProvider extends PermissionedMcpToolProvider {
Map.of("reportType", "daily_report_completion", "description", "日报应交、已交、未交、完成率"), Map.of("reportType", "daily_report_completion", "description", "日报应交、已交、未交、完成率"),
Map.of("reportType", "checkin_summary", "description", "外勤打卡统计"), Map.of("reportType", "checkin_summary", "description", "外勤打卡统计"),
Map.of("reportType", "expansion_summary", "description", "销售拓客与渠道拓客统计"), Map.of("reportType", "expansion_summary", "description", "销售拓客与渠道拓客统计"),
Map.of("reportType", "channel_analytics", "description", "渠道统计分析认证级别分布、合作意向、覆盖省市、年度营收分档SQL 全量聚合)"),
Map.of("reportType", "customer_summary", "description", "客户状态、来源、行业统计"), Map.of("reportType", "customer_summary", "description", "客户状态、来源、行业统计"),
Map.of("reportType", "todo_summary", "description", "待办状态与优先级统计"))); Map.of("reportType", "todo_summary", "description", "待办状态与优先级统计")));
result.put("security", "查询结果遵循系统 @DataScope 数据权限。"); result.put("security", "查询结果遵循系统 @DataScope 数据权限。");

View File

@ -59,7 +59,8 @@ public class CrmReportQueryToolProvider extends PermissionedMcpToolProvider {
"checkin_summary", "checkin_summary",
"expansion_summary", "expansion_summary",
"customer_summary", "customer_summary",
"todo_summary"))); "todo_summary",
"channel_analytics")));
queryProperties.put("startDate", stringProperty("开始日期 YYYY-MM-DD不传默认本月。")); queryProperties.put("startDate", stringProperty("开始日期 YYYY-MM-DD不传默认本月。"));
queryProperties.put("endDate", stringProperty("结束日期 YYYY-MM-DD不传默认今天最大跨度 366 天。")); queryProperties.put("endDate", stringProperty("结束日期 YYYY-MM-DD不传默认今天最大跨度 366 天。"));
queryProperties.put("ownerUserId", integerProperty("目标用户 ID实际可见范围仍按系统数据权限裁剪。")); queryProperties.put("ownerUserId", integerProperty("目标用户 ID实际可见范围仍按系统数据权限裁剪。"));
@ -92,6 +93,7 @@ public class CrmReportQueryToolProvider extends PermissionedMcpToolProvider {
case "expansion_summary" -> expansionSummary(context); case "expansion_summary" -> expansionSummary(context);
case "customer_summary" -> customerSummary(context); case "customer_summary" -> customerSummary(context);
case "todo_summary" -> todoSummary(context); case "todo_summary" -> todoSummary(context);
case "channel_analytics" -> channelAnalytics(context);
default -> throw new BusinessException("不支持的 reportType" + reportType); default -> throw new BusinessException("不支持的 reportType" + reportType);
}; };
@ -208,6 +210,22 @@ public class CrmReportQueryToolProvider extends PermissionedMcpToolProvider {
context.offset())); context.offset()));
} }
private List<Map<String, Object>> channelAnalytics(QueryContext context) {
List<Map<String, Object>> rows = new ArrayList<>();
Map<String, Object> row = new LinkedHashMap<>();
row.put("dimension", "tier");
row.put("tierDistribution", llmMcpMapper.channelTierDistribution(
context.startDate(), context.endDate(), context.ownerUserId(), context.tenantId()));
row.put("intentDistribution", llmMcpMapper.channelIntentDistribution(
context.startDate(), context.endDate(), context.ownerUserId(), context.tenantId()));
row.put("coverages", llmMcpMapper.channelCoverageSummary(
context.startDate(), context.endDate(), context.ownerUserId(), context.tenantId()));
row.put("revenueTier", llmMcpMapper.channelRevenueTier(
context.startDate(), context.endDate(), context.ownerUserId(), context.tenantId()));
rows.add(row);
return sanitizeRows(rows);
}
private QueryContext buildContext(Map<String, Object> arguments) { private QueryContext buildContext(Map<String, Object> arguments) {
LocalDate endDate = getDate(arguments, "endDate"); LocalDate endDate = getDate(arguments, "endDate");
LocalDate startDate = getDate(arguments, "startDate"); LocalDate startDate = getDate(arguments, "startDate");

View File

@ -2198,9 +2198,9 @@
select select
case case
when annual_revenue is null then '未填写' when annual_revenue is null then '未填写'
when annual_revenue < 100 then '<100' when annual_revenue &lt; 100 then '&lt;100万'
when annual_revenue < 500 then '100-500' when annual_revenue &lt; 500 then '100-500万'
when annual_revenue < 1000 then '500-1000' when annual_revenue &lt; 1000 then '500-1000万'
else '1000万+' else '1000万+'
end as "revenueTier", end as "revenueTier",
count(1)::bigint as "channelCount", count(1)::bigint as "channelCount",
@ -2211,9 +2211,9 @@
<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="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 group by case
when annual_revenue is null then '未填写' when annual_revenue is null then '未填写'
when annual_revenue < 100 then '<100' when annual_revenue &lt; 100 then '&lt;100万'
when annual_revenue < 500 then '100-500' when annual_revenue &lt; 500 then '100-500万'
when annual_revenue < 1000 then '500-1000' when annual_revenue &lt; 1000 then '500-1000万'
else '1000万+' else '1000万+'
end end
order by "channelCount" desc order by "channelCount" desc