增加新界面用于数据展示

This commit is contained in:
MerCry 2026-03-08 15:52:17 +08:00
parent 143a9e90be
commit 6c16ea1661
21 changed files with 5091 additions and 50 deletions

View File

@ -0,0 +1,191 @@
-- ==========================================
-- 流量看板V2 数据表和菜单配置
-- ==========================================
-- ----------------------------
-- 1. 数据表创建
-- ----------------------------
-- 客户统计数据表V2支持标签级成本行列转换存储
DROP TABLE IF EXISTS `customer_statistics_data_v2`;
CREATE TABLE `customer_statistics_data_v2` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`corp_id` VARCHAR(100) NOT NULL COMMENT '企业ID',
`cur_date` DATE NOT NULL COMMENT '统计日期',
-- 维度信息
`group_name` VARCHAR(50) NOT NULL COMMENT '组名N组、O组等',
`tag_name` VARCHAR(100) DEFAULT NULL COMMENT '标签名NULL表示组级汇总',
`tag_group_id` VARCHAR(100) DEFAULT NULL COMMENT '标签组ID关联wecom_tag_group',
`tag_id` VARCHAR(100) DEFAULT NULL COMMENT '标签ID关联wecom_tag',
-- 层级关系
`data_level` TINYINT DEFAULT 1 COMMENT '数据级别1-组级汇总2-标签级明细',
`parent_id` BIGINT(20) DEFAULT NULL COMMENT '父记录ID标签级数据对应组级记录的ID',
-- 成本数据(支持标签级成本)
`total_cost` DECIMAL(12,2) DEFAULT NULL COMMENT '总成本(手工录入)',
`single_cost` DECIMAL(12,2) DEFAULT NULL COMMENT '单条成本(计算得出)',
`order_cost` DECIMAL(12,2) DEFAULT NULL COMMENT '成单成本(计算得出)',
`cost_input_type` VARCHAR(20) DEFAULT NULL COMMENT '成本录入类型total-总成本single-单条成本',
-- 数量指标
`order_count` INT DEFAULT 0 COMMENT '成单数',
`customer_count` INT DEFAULT 0 COMMENT '进粉数',
`timely_order_count` INT DEFAULT 0 COMMENT '及时单数',
`non_timely_order_count` INT DEFAULT 0 COMMENT '非及时单数',
-- 比率指标
`conversion_rate` VARCHAR(10) DEFAULT '0%' COMMENT '转化率',
`timely_rate` VARCHAR(10) DEFAULT '0%' COMMENT '及时单占比',
`non_timely_rate` VARCHAR(10) DEFAULT '0%' COMMENT '非及时单占比',
-- 客户属性指标
`customer_attr_count` INT DEFAULT 0 COMMENT '客户属性数量',
`parent_count` INT DEFAULT 0 COMMENT '家长数量',
`student_count` INT DEFAULT 0 COMMENT '学生数量',
`teacher_count` INT DEFAULT 0 COMMENT '老师数量',
`unknown_attr_count` INT DEFAULT 0 COMMENT '未知属性数量',
`parent_rate` VARCHAR(10) DEFAULT '0%' COMMENT '家长占比',
`student_rate` VARCHAR(10) DEFAULT '0%' COMMENT '学生占比',
`teacher_rate` VARCHAR(10) DEFAULT '0%' COMMENT '老师占比',
`unknown_rate` VARCHAR(10) DEFAULT '0%' COMMENT '未知占比',
-- 出单率指标
`parent_order_count` INT DEFAULT 0 COMMENT '家长出单数量',
`student_order_count` INT DEFAULT 0 COMMENT '学生出单数量',
`teacher_order_count` INT DEFAULT 0 COMMENT '老师出单数量',
`unknown_order_count` INT DEFAULT 0 COMMENT '未知出单数量',
`parent_daily_count` INT DEFAULT 0 COMMENT '家长当日数量',
`student_daily_count` INT DEFAULT 0 COMMENT '学生当日数量',
`teacher_daily_count` INT DEFAULT 0 COMMENT '老师当日数量',
`unknown_daily_count` INT DEFAULT 0 COMMENT '未知当日数量',
`parent_daily_order_count` INT DEFAULT 0 COMMENT '家长当日出单数量',
`student_daily_order_count` INT DEFAULT 0 COMMENT '学生当日出单数量',
`teacher_daily_order_count` INT DEFAULT 0 COMMENT '老师当日出单数量',
`unknown_daily_order_count` INT DEFAULT 0 COMMENT '未知当日出单数量',
`parent_order_rate` VARCHAR(10) DEFAULT '0%' COMMENT '家长出单率',
`student_order_rate` VARCHAR(10) DEFAULT '0%' COMMENT '学生出单率',
`teacher_order_rate` VARCHAR(10) DEFAULT '0%' COMMENT '老师出单率',
`unknown_order_rate` VARCHAR(10) DEFAULT '0%' COMMENT '未知出单率',
-- 意向度指标
`intention_count` INT DEFAULT 0 COMMENT '意向度数量',
`active_quote_count` INT DEFAULT 0 COMMENT '主动报价数量',
`passive_quote_count` INT DEFAULT 0 COMMENT '被动报价数量',
`no_quote_count` INT DEFAULT 0 COMMENT '未开口报价数量',
`deleted_quote_count` INT DEFAULT 0 COMMENT '已删除报价数量',
`active_quote_rate` VARCHAR(10) DEFAULT '0%' COMMENT '主动报价占比',
`passive_quote_rate` VARCHAR(10) DEFAULT '0%' COMMENT '被动报价占比',
`no_quote_rate` VARCHAR(10) DEFAULT '0%' COMMENT '未开口报价占比',
`deleted_quote_rate` VARCHAR(10) DEFAULT '0%' COMMENT '已删除报价占比',
-- 年级指标
`grade_count` INT DEFAULT 0 COMMENT '年级数量',
`primary_count` INT DEFAULT 0 COMMENT '小学数量',
`middle_count` INT DEFAULT 0 COMMENT '初中数量',
`high_count` INT DEFAULT 0 COMMENT '高中数量',
`primary_rate` VARCHAR(10) DEFAULT '0%' COMMENT '小学占比',
`middle_rate` VARCHAR(10) DEFAULT '0%' COMMENT '初中占比',
`high_rate` VARCHAR(10) DEFAULT '0%' COMMENT '高中占比',
`sort_no` INT DEFAULT 0 COMMENT '排序号',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_corp_date_group_tag` (`corp_id`, `cur_date`, `group_name`, `tag_name`),
INDEX `idx_corp_date` (`corp_id`, `cur_date`),
INDEX `idx_group_name` (`group_name`),
INDEX `idx_data_level` (`data_level`),
INDEX `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户统计数据表V2支持标签级成本行列转换';
-- 成本录入记录表(用于追溯)
DROP TABLE IF EXISTS `cost_input_record_v2`;
CREATE TABLE `cost_input_record_v2` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`corp_id` VARCHAR(100) NOT NULL COMMENT '企业ID',
`cur_date` DATE NOT NULL COMMENT '统计日期',
`group_name` VARCHAR(50) NOT NULL COMMENT '组名',
`tag_name` VARCHAR(100) DEFAULT NULL COMMENT '标签名NULL表示组级',
`cost_type` VARCHAR(20) NOT NULL COMMENT 'total-总成本single-单条成本',
`input_value` DECIMAL(12,2) NOT NULL COMMENT '录入值',
`actual_total_cost` DECIMAL(12,2) NOT NULL COMMENT '实际总成本',
`input_by` VARCHAR(50) DEFAULT NULL COMMENT '录入人',
`input_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
INDEX `idx_corp_date_group` (`corp_id`, `cur_date`, `group_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成本录入记录表V2';
-- ----------------------------
-- 2. 菜单配置
-- 父菜单ID: 2000 (企业微信统计)
-- 新菜单ID从 2100 开始
-- ----------------------------
-- 流量看板V2 菜单(主菜单)
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2100, '流量看板V2', 2000, 0, 'customerStatisticsV2', 'wecom/customerStatisticsV2/index', NULL, 'CustomerStatisticsV2', 1, 0, 'C', '0', '0', 'wecom:customerStatisticsV2:list', 'chart', 'admin', NOW(), '', NULL, '流量看板V2菜单支持标签级成本');
-- 流量看板V2 - 查询按钮
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2101, '流量看板V2查询', 2100, 1, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerStatisticsV2:query', '#', 'admin', NOW(), '', NULL, '');
-- 流量看板V2 - 新增按钮
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2102, '流量看板V2新增', 2100, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerStatisticsV2:add', '#', 'admin', NOW(), '', NULL, '');
-- 流量看板V2 - 修改按钮
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2103, '流量看板V2修改', 2100, 3, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerStatisticsV2:edit', '#', 'admin', NOW(), '', NULL, '');
-- 流量看板V2 - 删除按钮
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2104, '流量看板V2删除', 2100, 4, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerStatisticsV2:remove', '#', 'admin', NOW(), '', NULL, '');
-- 流量看板V2 - 导出按钮
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2105, '流量看板V2导出', 2100, 5, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerStatisticsV2:export', '#', 'admin', NOW(), '', NULL, '');
-- 流量看板V2 - 成本录入按钮
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2106, '流量看板V2成本录入', 2100, 6, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerStatisticsV2:cost', '#', 'admin', NOW(), '', NULL, '');
-- 流量看板V2 - 重新计算按钮
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2107, '流量看板V2重新计算', 2100, 7, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerStatisticsV2:recalculate', '#', 'admin', NOW(), '', NULL, '');
-- 流量看板V2 - 树状数据查询
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES (2108, '流量看板V2树状查询', 2100, 8, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerStatisticsV2:tree', '#', 'admin', NOW(), '', NULL, '');
-- ----------------------------
-- 3. 菜单说明
-- ----------------------------
-- 菜单类型说明:
-- M: 目录
-- C: 菜单
-- F: 按钮
--
-- 菜单层级结构:
-- 2000 企业微信统计 (目录)
-- ├── 2001 客户列表数据
-- ├── 2002 客户联系统计
-- ├── 2003 流量看板数据 (原V1版本)
-- ├── 2004 销售看板数据
-- ├── 2100 流量看板V2 (新增,支持标签级成本)
-- │ ├── 2101 查询
-- │ ├── 2102 新增
-- │ ├── 2103 修改
-- │ ├── 2104 删除
-- │ ├── 2105 导出
-- │ ├── 2106 成本录入
-- │ ├── 2107 重新计算
-- │ └── 2108 树状查询
-- └── 3000 企业信息

View File

@ -0,0 +1,266 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 客户统计数据V2支持标签级成本行列转换存储
* 与V1的区别
* 1. V1行是指标列是组
* 2. V2行是组/标签列是指标
*/
@Data
@TableName("customer_statistics_data_v2")
public class CustomerStatisticsDataV2 implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String corpId;
private Date curDate;
/**
* 组名N组O组等
*/
private String groupName;
/**
* 标签名NULL表示组级汇总
*/
private String tagName;
/**
* 标签组ID关联wecom_tag_group
*/
private String tagGroupId;
/**
* 标签ID关联wecom_tag
*/
private String tagId;
/**
* 数据级别1-组级汇总2-标签级明细
*/
private Integer dataLevel;
/**
* 父记录ID标签级数据对应组级记录的ID
*/
private Long parentId;
// ==================== 成本数据 ====================
/**
* 总成本手工录入
*/
private BigDecimal totalCost;
/**
* 单条成本计算得出
*/
private BigDecimal singleCost;
/**
* 成单成本计算得出
*/
private BigDecimal orderCost;
/**
* 成本录入类型total-总成本single-单条成本
*/
private String costInputType;
// ==================== 数量指标 ====================
/**
* 成单数
*/
private Integer orderCount;
/**
* 进粉数
*/
private Integer customerCount;
/**
* 及时单数
*/
private Integer timelyOrderCount;
/**
* 非及时单数
*/
private Integer nonTimelyOrderCount;
// ==================== 比率指标 ====================
/**
* 转化率
*/
private String conversionRate;
/**
* 及时单占比
*/
private String timelyRate;
/**
* 非及时单占比
*/
private String nonTimelyRate;
// ==================== 客户属性指标 ====================
/**
* 客户属性数量
*/
private Integer customerAttrCount;
/**
* 家长数量
*/
private Integer parentCount;
/**
* 学生数量
*/
private Integer studentCount;
/**
* 老师数量
*/
private Integer teacherCount;
/**
* 未知属性数量
*/
private Integer unknownAttrCount;
/**
* 家长占比
*/
private String parentRate;
/**
* 学生占比
*/
private String studentRate;
/**
* 老师占比
*/
private String teacherRate;
/**
* 未知占比
*/
private String unknownRate;
// ==================== 出单率指标 ====================
private Integer parentOrderCount;
private Integer studentOrderCount;
private Integer teacherOrderCount;
private Integer unknownOrderCount;
private Integer parentDailyCount;
private Integer studentDailyCount;
private Integer teacherDailyCount;
private Integer unknownDailyCount;
private Integer parentDailyOrderCount;
private Integer studentDailyOrderCount;
private Integer teacherDailyOrderCount;
private Integer unknownDailyOrderCount;
private String parentOrderRate;
private String studentOrderRate;
private String teacherOrderRate;
private String unknownOrderRate;
// ==================== 意向度指标 ====================
private Integer intentionCount;
private Integer activeQuoteCount;
private Integer passiveQuoteCount;
private Integer noQuoteCount;
private Integer deletedQuoteCount;
private String activeQuoteRate;
private String passiveQuoteRate;
private String noQuoteRate;
private String deletedQuoteRate;
// ==================== 年级指标 ====================
private Integer gradeCount;
private Integer primaryCount;
private Integer middleCount;
private Integer highCount;
private String primaryRate;
private String middleRate;
private String highRate;
// ==================== 其他 ====================
private Integer sortNo;
private Date createTime;
private Date updateTime;
// ==================== 非持久化字段 ====================
/**
* 年份周数显示2026年第10周
*/
@TableField(exist = false)
private String yearWeek;
/**
* 年月显示2026年03月
*/
@TableField(exist = false)
private String yearMonth;
/**
* 日期范围显示2026-03-02 2026-03-08
*/
@TableField(exist = false)
private String dateRange;
/**
* 子标签列表用于树状展示
*/
@TableField(exist = false)
private List<CustomerStatisticsDataV2> children;
/**
* 是否为叶子节点标签级
*/
@TableField(exist = false)
private Boolean leaf;
/**
* 节点显示名称组名或标签名
*/
public String getDisplayName() {
if (tagName != null && !tagName.isEmpty()) {
return tagName;
}
return groupName;
}
/**
* 获取完整路径用于树状展示
*/
public String getFullPath() {
if (tagName != null && !tagName.isEmpty()) {
return groupName + "/" + tagName;
}
return groupName;
}
}

View File

@ -0,0 +1,34 @@
package com.ruoyi.excel.wecom.domain.dto;
import lombok.Data;
import java.util.List;
/**
* 标签树DTO
* 用于返回组-标签的树状结构
*/
@Data
public class TagTreeDTO {
/** 节点ID */
private String id;
/** 节点名称 */
private String label;
/** 节点类型group-组tag-标签 */
private String type;
/** 组名 */
private String groupName;
/** 标签名 */
private String tagName;
/** 子节点 */
private List<TagTreeDTO> children;
/** 数量统计 */
private Integer count;
}

View File

@ -48,6 +48,9 @@ import java.util.concurrent.atomic.AtomicInteger;
@Autowired @Autowired
private CorpInfoMapper corpInfoMapper; private CorpInfoMapper corpInfoMapper;
private List<String> finishFlag = Arrays.asList("已成交及时单9元+", "已成交非及时单9元+");
private List<String> timelyFinishFlag = Arrays.asList("已成交及时单9元+");
private List<String> noTimelyfinishFlag = Arrays.asList("已成交非及时单9元+");
/** /**
* 线程池配置 - 用于并行处理客户数据 * 线程池配置 - 用于并行处理客户数据
* 设置为4个线程适应4核8G服务器环境 * 设置为4个线程适应4核8G服务器环境
@ -773,7 +776,6 @@ import java.util.concurrent.atomic.AtomicInteger;
if (matchesQValue(data, date) && isTimelyOrder(data)) { if (matchesQValue(data, date) && isTimelyOrder(data)) {
stats.setTeacherDailyOrderCount(stats.getTeacherDailyOrderCount() + 1); stats.setTeacherDailyOrderCount(stats.getTeacherDailyOrderCount() + 1);
} }
}
} else { } else {
stats.setUnknownAttrCount(stats.getUnknownAttrCount() + 1); stats.setUnknownAttrCount(stats.getUnknownAttrCount() + 1);
stats.setUnkownOrderCount(stats.getUnkownOrderCount() + 1); stats.setUnkownOrderCount(stats.getUnkownOrderCount() + 1);
@ -784,6 +786,7 @@ import java.util.concurrent.atomic.AtomicInteger;
stats.setUnkownDailyOrderCount(stats.getUnkownDailyOrderCount() + 1); stats.setUnkownDailyOrderCount(stats.getUnkownDailyOrderCount() + 1);
} }
} }
}
// 4. 意向度统计 // 4. 意向度统计
String intention = data.getTagGroup15(); String intention = data.getTagGroup15();
@ -864,25 +867,29 @@ import java.util.concurrent.atomic.AtomicInteger;
// 1. 成单数 // 1. 成单数
//成单数 需要从历史的所有数据中获取 成交日期 = date的数据 //成单数 需要从历史的所有数据中获取 成交日期 = date的数据
Long finishCount = customerExportDataMapper.selectByFinishDate(corpId,curDate,GROUP_ATTR_MAP.get(groupName)); Long finishCount = customerExportDataMapper.selectByFinishDate(corpId,curDate,GROUP_ATTR_MAP.get(groupName),finishFlag);
setIndicatorValue(corpId,indicatorMap,curDate, "成单数(当日)", groupName, String.valueOf(finishCount),(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "成单数(当日)", groupName, String.valueOf(finishCount),(10*sortNo++));
// 2. 进粉数 // 2. 进粉数
setIndicatorValue(corpId,indicatorMap,curDate, "进粉数(当日)", groupName, String.valueOf(stats.getCustomerCount()),(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "进粉数(当日)", groupName, String.valueOf(stats.getCustomerCount()),(10*sortNo++));
// 3. 转化率 // 3. 转化率 = 成单数 / 进粉数使用finishCount而不是stats.getOrderCount()
String conversionRate = calculateRate(stats.getOrderCount(), stats.getCustomerCount()); String conversionRate = calculateRate(finishCount.intValue(), stats.getCustomerCount());
setIndicatorValue(corpId,indicatorMap,curDate, "转化率(当日)", groupName, conversionRate,(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "转化率(当日)", groupName, conversionRate,(10*sortNo++));
// 4. 及时单占比 // 4. 及时单占比 = 及时单数 / 成单数当日
String timelyRate = calculateRate(stats.getTimelyOrderCount(), stats.getCustomerCount()); // 及时单数量需要从历史数据中获取根据成交日期和订单状态
Long timelyCount = customerExportDataMapper.selectTimelyOrderCount(corpId, curDate, GROUP_ATTR_MAP.get(groupName),timelyFinishFlag);
String timelyRate = calculateRate(timelyCount.intValue(), finishCount.intValue());
setIndicatorValue(corpId,indicatorMap,curDate, "及时单占比(当日)", groupName, timelyRate,(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "及时单占比(当日)", groupName, timelyRate,(10*sortNo++));
setIndicatorValue(corpId,indicatorMap,curDate, "及时单数量(当日)", groupName, String.valueOf(stats.getTimelyOrderCount()),(10*sortNo++),true); setIndicatorValue(corpId,indicatorMap,curDate, "及时单数量(当日)", groupName, String.valueOf(timelyCount),(10*sortNo++),true);
// 5. 非及时单占比 // 5. 非及时单占比 = 非及时单数 / 成单数当日
String nonTimelyRate = calculateRate(stats.getNonTimelyOrderCount(), stats.getCustomerCount()); // 非及时单数量需要从历史数据中获取根据成交日期和订单状态
Long nonTimelyCount = customerExportDataMapper.selectNonTimelyOrderCount(corpId, curDate, GROUP_ATTR_MAP.get(groupName),noTimelyfinishFlag);
String nonTimelyRate = calculateRate(nonTimelyCount.intValue(), finishCount.intValue());
setIndicatorValue(corpId,indicatorMap,curDate, "非及时单占比(当日)", groupName, nonTimelyRate,(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "非及时单占比(当日)", groupName, nonTimelyRate,(10*sortNo++));
setIndicatorValue(corpId,indicatorMap,curDate, "非及时单数量(当日)", groupName, String.valueOf(stats.getNonTimelyOrderCount()),(10*sortNo++),true); setIndicatorValue(corpId,indicatorMap,curDate, "非及时单数量(当日)", groupName, String.valueOf(nonTimelyCount),(10*sortNo++),true);
// 6. 客户属性数量 // 6. 客户属性数量
setIndicatorValue(corpId,indicatorMap,curDate, "客户属性数量(当日)", groupName, String.valueOf(stats.getTotalCustomerAttr()),(10*sortNo++)); setIndicatorValue(corpId,indicatorMap,curDate, "客户属性数量(当日)", groupName, String.valueOf(stats.getTotalCustomerAttr()),(10*sortNo++));

View File

@ -0,0 +1,816 @@
package com.ruoyi.excel.wecom.helper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.excel.wecom.domain.CorpInfo;
import com.ruoyi.excel.wecom.domain.CustomerExportData;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsDataV2;
import com.ruoyi.excel.wecom.mapper.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 流量看板数据处理V2
* 支持标签级成本行列转换存储
* 与V1的区别
* 1. V1行是指标列是组
* 2. V2行是组/标签列是指标
*/
@Slf4j
@Component
public class HandleAllDataV2 {
@Autowired
private CustomerExportDataMapper customerExportDataMapper;
@Autowired
private CustomerStatisticsDataV2Mapper dataV2Mapper;
@Autowired
private WecomTagGroupMapper wecomTagGroupMapper;
@Autowired
private WecomTagMapper wecomTagMapper;
@Autowired
private CorpInfoMapper corpInfoMapper;
private List<String> finishFlag = Arrays.asList("已成交及时单9元+", "已成交非及时单9元+");
private List<String> timelyFinishFlag = Arrays.asList("已成交及时单9元+");
private List<String> noTimelyfinishFlag = Arrays.asList("已成交非及时单9元+");
/**
* 组名到字段名的映射使用括号内的名称
*/
private static final Map<String, String> GROUP_FIELD_MAP = new LinkedHashMap<>();
static {
GROUP_FIELD_MAP.put("投放", "tagGroup1");
GROUP_FIELD_MAP.put("公司孵化", "tagGroup2");
GROUP_FIELD_MAP.put("商务", "tagGroup3");
GROUP_FIELD_MAP.put("A1组", "tagGroup10");
GROUP_FIELD_MAP.put("B1组", "tagGroup11");
GROUP_FIELD_MAP.put("C1组", "tagGroup12");
GROUP_FIELD_MAP.put("D1组", "tagGroup13");
GROUP_FIELD_MAP.put("E1组", "tagGroup14");
GROUP_FIELD_MAP.put("自然流", "tagGroup16");
GROUP_FIELD_MAP.put("F1组", "tagGroup17");
GROUP_FIELD_MAP.put("G1组", "tagGroup18");
}
/**
* 组名到数据库字段名的映射用于SQL查询
*/
private static final Map<String, String> GROUP_ATTR_MAP = new LinkedHashMap<>();
static {
GROUP_ATTR_MAP.put("投放", "tag_group1");
GROUP_ATTR_MAP.put("公司孵化", "tag_group2");
GROUP_ATTR_MAP.put("商务", "tag_group3");
GROUP_ATTR_MAP.put("A1组", "tag_group10");
GROUP_ATTR_MAP.put("B1组", "tag_group11");
GROUP_ATTR_MAP.put("C1组", "tag_group12");
GROUP_ATTR_MAP.put("D1组", "tag_group13");
GROUP_ATTR_MAP.put("E1组", "tag_group14");
GROUP_ATTR_MAP.put("自然流", "tag_group16");
GROUP_ATTR_MAP.put("F1组", "tag_group17");
GROUP_ATTR_MAP.put("G1组", "tag_group18");
}
/**
* 线程池配置
*/
private final ExecutorService executorService = Executors.newFixedThreadPool(
4,
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "customer-data-v2-handler-" + threadNumber.getAndIncrement());
thread.setDaemon(false);
return thread;
}
}
);
/**
* 创建所有日期的流量看板数据V2
*/
public void createAllReportDataV2() {
List<CorpInfo> corpInfos = corpInfoMapper.selectCorpInfoList(new CorpInfo());
int batchSize = 10;
for (CorpInfo item : corpInfos) {
try {
String corpId = item.getCorpId();
List<Date> allDate = getAllDate(corpId);
for (int i = 0; i < allDate.size(); i += batchSize) {
int end = Math.min(i + batchSize, allDate.size());
List<Date> batchDates = allDate.subList(i, end);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (Date date : batchDates) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
createReportDataV2(corpId, date);
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
} catch (Exception e) {
throw new RuntimeException("多线程处理流量看板V2数据时发生错误: " + e.getMessage(), e);
}
}
}
/**
* 创建指定日期的流量看板数据V2
* @param corpId 企业ID
* @param date 统计日期
*/
@Transactional
public void createReportDataV2(String corpId, Date date) {
log.info("开始创建V2流量看板数据corpId={}, date={}", corpId, date);
// 1. 先删除当天已存在的数据
LambdaQueryWrapper<CustomerStatisticsDataV2> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(CustomerStatisticsDataV2::getCorpId, corpId)
.eq(CustomerStatisticsDataV2::getCurDate, date);
dataV2Mapper.delete(deleteWrapper);
// 2. 重新计算并插入当天数据
List<CustomerStatisticsDataV2> dataList = calculateStatisticsV2(corpId, date);
// 3. 批量插入
if (!dataList.isEmpty()) {
// 分批插入每批500条
int batchSize = 500;
for (int i = 0; i < dataList.size(); i += batchSize) {
int end = Math.min(i + batchSize, dataList.size());
List<CustomerStatisticsDataV2> batch = dataList.subList(i, end);
dataV2Mapper.batchInsert(batch);
}
}
log.info("V2流量看板数据创建完成corpId={}, date={}, 共{}条记录",
corpId, date, dataList.size());
}
/**
* 计算统计数据V2
* @param corpId 企业ID
* @param date 目标日期
* @return 统计结果列表
*/
private List<CustomerStatisticsDataV2> calculateStatisticsV2(String corpId, Date date) {
// 1. 初始化累加器按组和标签
GroupTagAccumulator accumulator = new GroupTagAccumulator();
// 2. 分页查询并累加统计
int pageSize = 1000;
int pageNum = 1;
LambdaQueryWrapper<CustomerExportData> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CustomerExportData::getCorpId, corpId)
.eq(CustomerExportData::getAddDate, date);
while (true) {
Page<CustomerExportData> page = new Page<>(pageNum, pageSize);
Page<CustomerExportData> pageData = customerExportDataMapper.selectPage(page, wrapper);
for (CustomerExportData data : pageData.getRecords()) {
processDataRecordV2(data, date, accumulator);
}
if (!pageData.hasNext()) {
break;
}
pageNum++;
}
// 3. 从累加器生成最终结果
return generateStatisticsResultsV2(corpId, date, accumulator);
}
/**
* 处理单条数据记录累加到组级和标签级统计
*/
private void processDataRecordV2(CustomerExportData data, Date date, GroupTagAccumulator accumulator) {
// 遍历所有组
for (Map.Entry<String, String> entry : GROUP_FIELD_MAP.entrySet()) {
String groupName = entry.getKey();
String fieldName = entry.getValue();
// 获取该组的标签值
String tagValue = getFieldValue(data, fieldName);
// 如果该组标签为空跳过
if (tagValue == null || tagValue.trim().isEmpty()) {
continue;
}
// 获取该组的统计器
GroupStatistics groupStats = accumulator.getGroupStats(groupName);
// 累加组级统计
accumulateGroupStatistics(data, date, groupStats);
// 解析标签值可能是逗号分隔的多个标签
String[] tags = tagValue.split(",");
for (String tag : tags) {
tag = tag.trim();
if (tag.isEmpty()) continue;
// 累加到标签级统计
TagStatistics tagStats = accumulator.getTagStats(groupName, tag);
accumulateGroupStatistics(data, date, tagStats);
}
}
}
/**
* 使用反射获取字段值带缓存
*/
private String getFieldValue(CustomerExportData data, String fieldName) {
try {
java.lang.reflect.Field field = CustomerExportData.class.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(data);
return value == null ? "" : value.toString();
} catch (Exception e) {
return "";
}
}
/**
* 累加单条数据的统计指标
*/
private void accumulateGroupStatistics(CustomerExportData data, Date date, BaseStatistics stats) {
// 1. 成单数统计
if (matchesQValue(data, date)) {
stats.setOrderCount(stats.getOrderCount() + 1);
String orderStatus = data.getTagGroup7();
if (orderStatus != null) {
String[] split = orderStatus.split(",");
String statusInfo = split[split.length - 1];
if (statusInfo.contains("已成交及时单9元+")) {
stats.setTimelyOrderCount(stats.getTimelyOrderCount() + 1);
} else if (statusInfo.contains("已成交非及时单9元+")) {
stats.setNonTimelyOrderCount(stats.getNonTimelyOrderCount() + 1);
}
}
}
// 来源筛选
if (!matchesSource(data) || !matchesDate(data, date)) {
return;
}
// 2. 进粉数
stats.setCustomerCount(stats.getCustomerCount() + 1);
// 3. 客户属性统计
String customerAttr = data.getTagGroup6();
if (customerAttr != null && !customerAttr.trim().isEmpty()) {
stats.setCustomerAttrCount(stats.getCustomerAttrCount() + 1);
if (customerAttr.contains("家长")) {
stats.setParentCount(stats.getParentCount() + 1);
stats.setParentOrderCount(stats.getParentOrderCount() + 1);
stats.setParentDailyCount(stats.getParentDailyCount() + 1);
if (matchesQValue(data, date) && isTimelyOrder(data)) {
stats.setParentDailyOrderCount(stats.getParentDailyOrderCount() + 1);
}
} else if (customerAttr.contains("学生")) {
stats.setStudentCount(stats.getStudentCount() + 1);
stats.setStudentOrderCount(stats.getStudentOrderCount() + 1);
stats.setStudentDailyCount(stats.getStudentDailyCount() + 1);
if (matchesQValue(data, date) && isTimelyOrder(data)) {
stats.setStudentDailyOrderCount(stats.getStudentDailyOrderCount() + 1);
}
} else if (customerAttr.contains("老师")) {
stats.setTeacherCount(stats.getTeacherCount() + 1);
stats.setTeacherOrderCount(stats.getTeacherOrderCount() + 1);
stats.setTeacherDailyCount(stats.getTeacherDailyCount() + 1);
if (matchesQValue(data, date) && isTimelyOrder(data)) {
stats.setTeacherDailyOrderCount(stats.getTeacherDailyOrderCount() + 1);
}
} else {
// 属性不为空但无法识别未知
stats.setUnknownAttrCount(stats.getUnknownAttrCount() + 1);
stats.setUnknownOrderCount(stats.getUnknownOrderCount() + 1);
stats.setUnknownDailyCount(stats.getUnknownDailyCount() + 1);
if (matchesQValue(data, date) && isTimelyOrder(data)) {
stats.setUnknownDailyOrderCount(stats.getUnknownDailyOrderCount() + 1);
}
}
}
// 注意属性为空的客户不统计在 customerAttrCount unknownAttrCount
// 4. 意向度统计
String intention = data.getTagGroup15();
if (intention != null && !intention.trim().isEmpty() && !"空白".equals(intention)) {
stats.setIntentionCount(stats.getIntentionCount() + 1);
if (intention.contains("主动报价")) {
stats.setActiveQuoteCount(stats.getActiveQuoteCount() + 1);
} else if (intention.contains("被动报价")) {
stats.setPassiveQuoteCount(stats.getPassiveQuoteCount() + 1);
} else if (intention.contains("未开口")) {
stats.setNoQuoteCount(stats.getNoQuoteCount() + 1);
} else if (intention.contains("已删除")) {
stats.setDeletedQuoteCount(stats.getDeletedQuoteCount() + 1);
}
}
// 5. 年级统计
String grade = data.getTagGroup5();
if (grade != null && !grade.trim().isEmpty() && !"空白".equals(grade)) {
stats.setGradeCount(stats.getGradeCount() + 1);
if (isPrimarySchool(grade)) {
stats.setPrimaryCount(stats.getPrimaryCount() + 1);
} else if (isMiddleSchool(grade)) {
stats.setMiddleCount(stats.getMiddleCount() + 1);
} else if (isHighSchool(grade)) {
stats.setHighCount(stats.getHighCount() + 1);
}
}
}
/**
* 从累加器生成最终统计结果V2
*/
private List<CustomerStatisticsDataV2> generateStatisticsResultsV2(
String corpId, Date date, GroupTagAccumulator accumulator) {
List<CustomerStatisticsDataV2> result = new ArrayList<>();
int sortNo = 0;
long tempId = 1; // 临时ID生成器
// 1. 生成组级数据
Map<String, Long> groupIdMap = new HashMap<>();
for (Map.Entry<String, GroupStatistics> entry : accumulator.getGroupStatsMap().entrySet()) {
String groupName = entry.getKey();
GroupStatistics stats = entry.getValue();
// 使用SQL查询获取成单数与V1保持一致
Long finishCount = customerExportDataMapper.selectByFinishDate(
corpId, date, GROUP_ATTR_MAP.get(groupName),finishFlag);
stats.setOrderCount(finishCount.intValue());
// 使用SQL查询获取及时单数量根据成交日期和订单状态
Long timelyCount = customerExportDataMapper.selectTimelyOrderCount(
corpId, date, GROUP_ATTR_MAP.get(groupName),timelyFinishFlag);
stats.setTimelyOrderCount(timelyCount.intValue());
// 使用SQL查询获取非及时单数量根据成交日期和订单状态
Long nonTimelyCount = customerExportDataMapper.selectNonTimelyOrderCount(
corpId, date, GROUP_ATTR_MAP.get(groupName),noTimelyfinishFlag);
stats.setNonTimelyOrderCount(nonTimelyCount.intValue());
CustomerStatisticsDataV2 groupData = createStatisticsDataV2(
corpId, date, groupName, null, stats, sortNo++);
groupData.setDataLevel(1);
groupData.setId(tempId); // 设置临时ID
result.add(groupData);
// 临时存储组ID用于标签级数据的parentId
groupIdMap.put(groupName, tempId);
tempId++;
}
// 2. 生成标签级数据
for (Map.Entry<String, Map<String, TagStatistics>> groupEntry :
accumulator.getTagStatsMap().entrySet()) {
String groupName = groupEntry.getKey();
Long parentId = groupIdMap.get(groupName);
for (Map.Entry<String, TagStatistics> tagEntry : groupEntry.getValue().entrySet()) {
String tagName = tagEntry.getKey();
TagStatistics stats = tagEntry.getValue();
// 使用SQL查询获取该标签的成单数根据标签值筛选
Long finishCount = customerExportDataMapper.selectByFinishDateAndTag(
corpId, date, GROUP_ATTR_MAP.get(groupName), tagName,finishFlag);
stats.setOrderCount(finishCount.intValue());
// 使用SQL查询获取该标签的及时单数量
Long timelyCount = customerExportDataMapper.selectTimelyOrderCountByTag(
corpId, date, GROUP_ATTR_MAP.get(groupName), tagName,timelyFinishFlag);
stats.setTimelyOrderCount(timelyCount.intValue());
// 使用SQL查询获取该标签的非及时单数量
Long nonTimelyCount = customerExportDataMapper.selectNonTimelyOrderCountByTag(
corpId, date, GROUP_ATTR_MAP.get(groupName), tagName,noTimelyfinishFlag);
stats.setNonTimelyOrderCount(nonTimelyCount.intValue());
CustomerStatisticsDataV2 tagData = createStatisticsDataV2(
corpId, date, groupName, tagName, stats, sortNo++);
tagData.setDataLevel(2);
tagData.setId(tempId); // 设置临时ID
tagData.setParentId(parentId);
result.add(tagData);
tempId++;
}
}
return result;
}
/**
* 创建统计数据V2对象
*/
private CustomerStatisticsDataV2 createStatisticsDataV2(
String corpId, Date date, String groupName, String tagName,
BaseStatistics stats, int sortNo) {
CustomerStatisticsDataV2 data = new CustomerStatisticsDataV2();
data.setCorpId(corpId);
data.setCurDate(date);
data.setGroupName(groupName);
data.setTagName(tagName);
data.setSortNo(sortNo);
// 数量指标
data.setOrderCount(stats.getOrderCount());
data.setCustomerCount(stats.getCustomerCount());
data.setTimelyOrderCount(stats.getTimelyOrderCount());
data.setNonTimelyOrderCount(stats.getNonTimelyOrderCount());
// 比率指标
// 转化率 = 成单数 / 进粉数
data.setConversionRate(calculateRate(stats.getOrderCount(), stats.getCustomerCount()));
// 及时单占比 = 及时单数 / 成单数当日
data.setTimelyRate(calculateRate(stats.getTimelyOrderCount(), stats.getOrderCount()));
// 非及时单占比 = 非及时单数 / 成单数当日
data.setNonTimelyRate(calculateRate(stats.getNonTimelyOrderCount(), stats.getOrderCount()));
// 客户属性指标
data.setCustomerAttrCount(stats.getCustomerAttrCount());
data.setParentCount(stats.getParentCount());
data.setStudentCount(stats.getStudentCount());
data.setTeacherCount(stats.getTeacherCount());
data.setUnknownAttrCount(stats.getUnknownAttrCount());
data.setParentRate(calculateRate(stats.getParentCount(), stats.getCustomerAttrCount()));
data.setStudentRate(calculateRate(stats.getStudentCount(), stats.getCustomerAttrCount()));
data.setTeacherRate(calculateRate(stats.getTeacherCount(), stats.getCustomerAttrCount()));
data.setUnknownRate(calculateRate(stats.getUnknownAttrCount(), stats.getCustomerAttrCount()));
// 出单率指标
data.setParentOrderCount(stats.getParentOrderCount());
data.setStudentOrderCount(stats.getStudentOrderCount());
data.setTeacherOrderCount(stats.getTeacherOrderCount());
data.setUnknownOrderCount(stats.getUnknownOrderCount());
data.setParentDailyCount(stats.getParentDailyCount());
data.setStudentDailyCount(stats.getStudentDailyCount());
data.setTeacherDailyCount(stats.getTeacherDailyCount());
data.setUnknownDailyCount(stats.getUnknownDailyCount());
data.setParentDailyOrderCount(stats.getParentDailyOrderCount());
data.setStudentDailyOrderCount(stats.getStudentDailyOrderCount());
data.setTeacherDailyOrderCount(stats.getTeacherDailyOrderCount());
data.setUnknownDailyOrderCount(stats.getUnknownDailyOrderCount());
data.setParentOrderRate(calculateRate(stats.getParentDailyOrderCount(), stats.getParentDailyCount()));
data.setStudentOrderRate(calculateRate(stats.getStudentDailyOrderCount(), stats.getStudentDailyCount()));
data.setTeacherOrderRate(calculateRate(stats.getTeacherDailyOrderCount(), stats.getTeacherDailyCount()));
data.setUnknownOrderRate(calculateRate(stats.getUnknownDailyOrderCount(), stats.getUnknownDailyCount()));
// 意向度指标
data.setIntentionCount(stats.getIntentionCount());
data.setActiveQuoteCount(stats.getActiveQuoteCount());
data.setPassiveQuoteCount(stats.getPassiveQuoteCount());
data.setNoQuoteCount(stats.getNoQuoteCount());
data.setDeletedQuoteCount(stats.getDeletedQuoteCount());
data.setActiveQuoteRate(calculateRate(stats.getActiveQuoteCount(), stats.getIntentionCount()));
data.setPassiveQuoteRate(calculateRate(stats.getPassiveQuoteCount(), stats.getIntentionCount()));
data.setNoQuoteRate(calculateRate(stats.getNoQuoteCount(), stats.getIntentionCount()));
data.setDeletedQuoteRate(calculateRate(stats.getDeletedQuoteCount(), stats.getIntentionCount()));
// 年级指标
data.setGradeCount(stats.getGradeCount());
data.setPrimaryCount(stats.getPrimaryCount());
data.setMiddleCount(stats.getMiddleCount());
data.setHighCount(stats.getHighCount());
data.setPrimaryRate(calculateRate(stats.getPrimaryCount(), stats.getGradeCount()));
data.setMiddleRate(calculateRate(stats.getMiddleCount(), stats.getGradeCount()));
data.setHighRate(calculateRate(stats.getHighCount(), stats.getGradeCount()));
return data;
}
/**
* 计算百分比
*/
private String calculateRate(int count, int total) {
if (total == 0) {
return "0%";
}
BigDecimal rate = new BigDecimal(count)
.multiply(new BigDecimal(100))
.divide(new BigDecimal(total), 2, RoundingMode.HALF_UP);
return rate.toString() + "%";
}
/**
* 检查数据是否匹配日期
*/
private boolean matchesDate(CustomerExportData data, Date date) {
if (data.getAddDate() != null && date != null) {
return date.compareTo(data.getAddDate()) == 0;
}
return false;
}
/**
* 检查数据来源是否匹配
*/
private boolean matchesSource(CustomerExportData data) {
if (data.getSource() != null &&
data.getSource().contains("管理员") &&
data.getSource().contains("分配")) {
return false;
}
return true;
}
/**
* 检查成交日期是否匹配
*/
private boolean matchesQValue(CustomerExportData data, Date curDate) {
String orderDate = data.getTagGroup4();
if (orderDate == null || orderDate.trim().isEmpty()) {
return false;
}
try {
String[] dates = orderDate.trim().split(",");
String lastDateStr = dates[dates.length - 1].trim();
Calendar orderCal = Calendar.getInstance();
orderCal.setTime(curDate);
orderCal.set(Calendar.HOUR_OF_DAY, 0);
orderCal.set(Calendar.MINUTE, 0);
orderCal.set(Calendar.SECOND, 0);
orderCal.set(Calendar.MILLISECOND, 0);
boolean parsed = false;
// 格式1: 2026/2/24周二
java.util.regex.Pattern fullDatePattern = java.util.regex.Pattern.compile("(\\d{4})[/\\-](\\d{1,2})[/\\-](\\d{1,2})");
java.util.regex.Matcher fullDateMatcher = fullDatePattern.matcher(lastDateStr);
if (fullDateMatcher.find()) {
int year = Integer.parseInt(fullDateMatcher.group(1));
int month = Integer.parseInt(fullDateMatcher.group(2));
int day = Integer.parseInt(fullDateMatcher.group(3));
orderCal.set(Calendar.YEAR, year);
orderCal.set(Calendar.MONTH, month - 1);
orderCal.set(Calendar.DAY_OF_MONTH, day);
parsed = true;
}
// 格式2: 1.7-小雅初中公众号K 2.3
if (!parsed) {
java.util.regex.Pattern monthDayPattern = java.util.regex.Pattern.compile("^(\\d{1,2})[.\\-/](\\d{1,2})");
java.util.regex.Matcher monthDayMatcher = monthDayPattern.matcher(lastDateStr);
if (monthDayMatcher.find()) {
int month = Integer.parseInt(monthDayMatcher.group(1));
int day = Integer.parseInt(monthDayMatcher.group(2));
orderCal.set(Calendar.MONTH, month - 1);
orderCal.set(Calendar.DAY_OF_MONTH, day);
Calendar tempCal = Calendar.getInstance();
tempCal.setTime(curDate);
tempCal.set(Calendar.MONTH, month - 1);
tempCal.set(Calendar.DAY_OF_MONTH, day);
tempCal.set(Calendar.HOUR_OF_DAY, 0);
tempCal.set(Calendar.MINUTE, 0);
tempCal.set(Calendar.SECOND, 0);
tempCal.set(Calendar.MILLISECOND, 0);
if (tempCal.getTime().before(curDate)) {
orderCal.add(Calendar.YEAR, 1);
}
parsed = true;
}
}
if (!parsed) {
return false;
}
Calendar targetCal = Calendar.getInstance();
targetCal.setTime(curDate);
targetCal.set(Calendar.HOUR_OF_DAY, 0);
targetCal.set(Calendar.MINUTE, 0);
targetCal.set(Calendar.SECOND, 0);
targetCal.set(Calendar.MILLISECOND, 0);
return orderCal.getTimeInMillis() == targetCal.getTimeInMillis();
} catch (Exception e) {
return false;
}
}
/**
* 判断是否为及时单
*/
private boolean isTimelyOrder(CustomerExportData data) {
String orderStatus = data.getTagGroup7();
if (orderStatus == null || orderStatus.trim().isEmpty()) {
return false;
}
String[] split = orderStatus.split(",");
String statusInfo = split[split.length - 1];
return statusInfo.contains("已成交及时单9元+");
}
/**
* 判断是否为小学
*/
private boolean isPrimarySchool(String grade) {
return grade.contains("小学") || grade.contains("一年级") || grade.contains("二年级") ||
grade.contains("三年级") || grade.contains("四年级") || grade.contains("五年级") ||
grade.contains("六年级");
}
/**
* 判断是否为初中
*/
private boolean isMiddleSchool(String grade) {
return grade.contains("初中") || grade.contains("初一") ||
grade.contains("初二") || grade.contains("初三");
}
/**
* 判断是否为高中
*/
private boolean isHighSchool(String grade) {
return grade.contains("高中") || grade.contains("高一") ||
grade.contains("高二") || grade.contains("高三");
}
/**
* 获取所有日期
*/
private List<Date> getAllDate(String corpId) {
return customerExportDataMapper.getDistinctDate(corpId);
}
/**
* 关闭线程池
*/
public void shutdown() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
// ==================== 内部类定义 ====================
/**
* 基础统计类
*/
public static class BaseStatistics {
// 数量指标
private int orderCount;
private int customerCount;
private int timelyOrderCount;
private int nonTimelyOrderCount;
// 客户属性指标
private int customerAttrCount;
private int parentCount;
private int studentCount;
private int teacherCount;
private int unknownAttrCount;
// 出单率指标
private int parentOrderCount;
private int studentOrderCount;
private int teacherOrderCount;
private int unknownOrderCount;
private int parentDailyCount;
private int studentDailyCount;
private int teacherDailyCount;
private int unknownDailyCount;
private int parentDailyOrderCount;
private int studentDailyOrderCount;
private int teacherDailyOrderCount;
private int unknownDailyOrderCount;
// 意向度指标
private int intentionCount;
private int activeQuoteCount;
private int passiveQuoteCount;
private int noQuoteCount;
private int deletedQuoteCount;
// 年级指标
private int gradeCount;
private int primaryCount;
private int middleCount;
private int highCount;
// Getters and Setters
public int getOrderCount() { return orderCount; }
public void setOrderCount(int orderCount) { this.orderCount = orderCount; }
public int getCustomerCount() { return customerCount; }
public void setCustomerCount(int customerCount) { this.customerCount = customerCount; }
public int getTimelyOrderCount() { return timelyOrderCount; }
public void setTimelyOrderCount(int timelyOrderCount) { this.timelyOrderCount = timelyOrderCount; }
public int getNonTimelyOrderCount() { return nonTimelyOrderCount; }
public void setNonTimelyOrderCount(int nonTimelyOrderCount) { this.nonTimelyOrderCount = nonTimelyOrderCount; }
public int getCustomerAttrCount() { return customerAttrCount; }
public void setCustomerAttrCount(int customerAttrCount) { this.customerAttrCount = customerAttrCount; }
public int getParentCount() { return parentCount; }
public void setParentCount(int parentCount) { this.parentCount = parentCount; }
public int getStudentCount() { return studentCount; }
public void setStudentCount(int studentCount) { this.studentCount = studentCount; }
public int getTeacherCount() { return teacherCount; }
public void setTeacherCount(int teacherCount) { this.teacherCount = teacherCount; }
public int getUnknownAttrCount() { return unknownAttrCount; }
public void setUnknownAttrCount(int unknownAttrCount) { this.unknownAttrCount = unknownAttrCount; }
public int getParentOrderCount() { return parentOrderCount; }
public void setParentOrderCount(int parentOrderCount) { this.parentOrderCount = parentOrderCount; }
public int getStudentOrderCount() { return studentOrderCount; }
public void setStudentOrderCount(int studentOrderCount) { this.studentOrderCount = studentOrderCount; }
public int getTeacherOrderCount() { return teacherOrderCount; }
public void setTeacherOrderCount(int teacherOrderCount) { this.teacherOrderCount = teacherOrderCount; }
public int getUnknownOrderCount() { return unknownOrderCount; }
public void setUnknownOrderCount(int unknownOrderCount) { this.unknownOrderCount = unknownOrderCount; }
public int getParentDailyCount() { return parentDailyCount; }
public void setParentDailyCount(int parentDailyCount) { this.parentDailyCount = parentDailyCount; }
public int getStudentDailyCount() { return studentDailyCount; }
public void setStudentDailyCount(int studentDailyCount) { this.studentDailyCount = studentDailyCount; }
public int getTeacherDailyCount() { return teacherDailyCount; }
public void setTeacherDailyCount(int teacherDailyCount) { this.teacherDailyCount = teacherDailyCount; }
public int getUnknownDailyCount() { return unknownDailyCount; }
public void setUnknownDailyCount(int unknownDailyCount) { this.unknownDailyCount = unknownDailyCount; }
public int getParentDailyOrderCount() { return parentDailyOrderCount; }
public void setParentDailyOrderCount(int parentDailyOrderCount) { this.parentDailyOrderCount = parentDailyOrderCount; }
public int getStudentDailyOrderCount() { return studentDailyOrderCount; }
public void setStudentDailyOrderCount(int studentDailyOrderCount) { this.studentDailyOrderCount = studentDailyOrderCount; }
public int getTeacherDailyOrderCount() { return teacherDailyOrderCount; }
public void setTeacherDailyOrderCount(int teacherDailyOrderCount) { this.teacherDailyOrderCount = teacherDailyOrderCount; }
public int getUnknownDailyOrderCount() { return unknownDailyOrderCount; }
public void setUnknownDailyOrderCount(int unknownDailyOrderCount) { this.unknownDailyOrderCount = unknownDailyOrderCount; }
public int getIntentionCount() { return intentionCount; }
public void setIntentionCount(int intentionCount) { this.intentionCount = intentionCount; }
public int getActiveQuoteCount() { return activeQuoteCount; }
public void setActiveQuoteCount(int activeQuoteCount) { this.activeQuoteCount = activeQuoteCount; }
public int getPassiveQuoteCount() { return passiveQuoteCount; }
public void setPassiveQuoteCount(int passiveQuoteCount) { this.passiveQuoteCount = passiveQuoteCount; }
public int getNoQuoteCount() { return noQuoteCount; }
public void setNoQuoteCount(int noQuoteCount) { this.noQuoteCount = noQuoteCount; }
public int getDeletedQuoteCount() { return deletedQuoteCount; }
public void setDeletedQuoteCount(int deletedQuoteCount) { this.deletedQuoteCount = deletedQuoteCount; }
public int getGradeCount() { return gradeCount; }
public void setGradeCount(int gradeCount) { this.gradeCount = gradeCount; }
public int getPrimaryCount() { return primaryCount; }
public void setPrimaryCount(int primaryCount) { this.primaryCount = primaryCount; }
public int getMiddleCount() { return middleCount; }
public void setMiddleCount(int middleCount) { this.middleCount = middleCount; }
public int getHighCount() { return highCount; }
public void setHighCount(int highCount) { this.highCount = highCount; }
}
/**
* 组级统计
*/
public static class GroupStatistics extends BaseStatistics {}
/**
* 标签级统计
*/
public static class TagStatistics extends BaseStatistics {}
/**
* 组和标签累加器
*/
public static class GroupTagAccumulator {
private final Map<String, GroupStatistics> groupStatsMap = new LinkedHashMap<>();
private final Map<String, Map<String, TagStatistics>> tagStatsMap = new LinkedHashMap<>();
public GroupStatistics getGroupStats(String groupName) {
return groupStatsMap.computeIfAbsent(groupName, k -> new GroupStatistics());
}
public TagStatistics getTagStats(String groupName, String tagName) {
Map<String, TagStatistics> tagMap = tagStatsMap.computeIfAbsent(groupName, k -> new LinkedHashMap<>());
return tagMap.computeIfAbsent(tagName, k -> new TagStatistics());
}
public Map<String, GroupStatistics> getGroupStatsMap() {
return groupStatsMap;
}
public Map<String, Map<String, TagStatistics>> getTagStatsMap() {
return tagStatsMap;
}
}
}

View File

@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Param;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 客户导出数据Mapper * 客户导出数据Mapper
@ -59,7 +60,69 @@ public interface CustomerExportDataMapper extends BaseMapper<CustomerExportData>
); );
Long selectByFinishDate( Long selectByFinishDate(
@Param("corpId") String corpId,@Param("date") Date date,@Param("attr") String attr); @Param("corpId") String corpId,@Param("date") Date date,
@Param("attr") String attr,@Param("successFlags") List<String> successFlags);
/**
* 查询及时单数量根据成交日期和订单状态
* @param corpId 企业ID
* @param date 成交日期
* @param attr 组字段名
* @return 及时单数量
*/
Long selectTimelyOrderCount(
@Param("corpId") String corpId, @Param("date") Date date,
@Param("attr") String attr,@Param("successFlags") List<String> successFlags);
/**
* 查询非及时单数量根据成交日期和订单状态
* @param corpId 企业ID
* @param date 成交日期
* @param attr 组字段名
* @return 非及时单数量
*/
Long selectNonTimelyOrderCount(
@Param("corpId") String corpId, @Param("date") Date date,
@Param("attr") String attr,@Param("successFlags") List<String> successFlags);
/**
* 查询成单数根据成交日期组字段和标签值
* @param corpId 企业ID
* @param date 成交日期
* @param attr 组字段名
* @param tagValue 标签值
* @return 成单数
*/
Long selectByFinishDateAndTag(
@Param("corpId") String corpId, @Param("date") Date date,
@Param("attr") String attr, @Param("tagValue") String tagValue,
@Param("successFlags") List<String> successFlags);
/**
* 查询及时单数量根据成交日期订单状态和标签值
* @param corpId 企业ID
* @param date 成交日期
* @param attr 组字段名
* @param tagValue 标签值
* @return 及时单数量
*/
Long selectTimelyOrderCountByTag(
@Param("corpId") String corpId, @Param("date") Date date,
@Param("attr") String attr, @Param("tagValue") String tagValue,
@Param("successFlags") List<String> successFlags);
/**
* 查询非及时单数量根据成交日期订单状态和标签值
* @param corpId 企业ID
* @param date 成交日期
* @param attr 组字段名
* @param tagValue 标签值
* @return 非及时单数量
*/
Long selectNonTimelyOrderCountByTag(
@Param("corpId") String corpId, @Param("date") Date date,
@Param("attr") String attr, @Param("tagValue") String tagValue,
@Param("successFlags") List<String> successFlags);
/** /**
* 统计客户导出数据VO数量(用于异步导出) * 统计客户导出数据VO数量(用于异步导出)
@ -94,5 +157,88 @@ public interface CustomerExportDataMapper extends BaseMapper<CustomerExportData>
@Param("offset") int offset, @Param("offset") int offset,
@Param("limit") int limit @Param("limit") int limit
); );
/**
* 按日期范围查询成单数根据finish_date
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param attr 组字段名如tag_group11
* @return 成单数
*/
Long selectOrderCountByFinishDateRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("attr") String attr,
@Param("successFlags") List<String> successFlags);
/**
* 按日期范围查询及时单数量根据finish_date
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param attr 组字段名
* @return 及时单数量
*/
Long selectTimelyOrderCountByDateRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("attr") String attr,
@Param("successFlags") List<String> successFlags);
/**
* 按日期范围查询非及时单数量根据finish_date
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param attr 组字段名
* @return 非及时单数量
*/
Long selectNonTimelyOrderCountByDateRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("attr") String attr,
@Param("successFlags") List<String> successFlags);
/**
* 批量查询所有组的成单数根据finish_date
* 一次查询返回所有组的成单数避免N+1查询问题
* @param corpId 企业ID
* @param startDate 开始日期可为null表示不限制
* @param endDate 结束日期可为null表示不限制
* @return Map格式key=组字段名value=成单数
*/
Map<String, Object> selectOrderCountBatchByFinishDateRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("successFlags") List<String> successFlags);
/**
* 批量查询所有组的及时单数量根据finish_date
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return Map格式key=组字段名value=及时单数量
*/
Map<String, Object> selectTimelyOrderCountBatchByDateRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,@Param("successFlags") List<String> successFlags);
/**
* 批量查询所有组的非及时单数量根据finish_date
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return Map格式key=组字段名value=非及时单数量
*/
Map<String, Object> selectNonTimelyOrderCountBatchByDateRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,@Param("successFlags") List<String> successFlags);
} }

View File

@ -53,6 +53,21 @@ public interface CustomerStatisticsDataMapper extends BaseMapper<CustomerStatist
@Param("indicatorName") String indicatorName @Param("indicatorName") String indicatorName
); );
/**
* 按日期范围查询周数据修复跨年周问题
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param indicatorName 指标名称
* @return 客户统计数据列表
*/
List<CustomerStatisticsData> selectDailyDataByWeekRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("indicatorName") String indicatorName
);
List<CustomerStatisticsData> selectDailyDataByMonth( List<CustomerStatisticsData> selectDailyDataByMonth(
@Param("corpId") String corpId, @Param("corpId") String corpId,
@Param("yearMonth") String yearMonth, @Param("yearMonth") String yearMonth,

View File

@ -0,0 +1,134 @@
package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsDataV2;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* 客户统计数据V2 Mapper接口
* 支持标签级成本行列转换存储
*/
@Mapper
public interface CustomerStatisticsDataV2Mapper extends BaseMapper<CustomerStatisticsDataV2> {
/**
* 根据企业ID日期组名标签名查询数据
*/
CustomerStatisticsDataV2 selectByCorpDateGroupTag(
@Param("corpId") String corpId,
@Param("curDate") Date curDate,
@Param("groupName") String groupName,
@Param("tagName") String tagName
);
/**
* 查询组级数据列表
*/
List<CustomerStatisticsDataV2> selectGroupLevelList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate
);
/**
* 查询标签级数据列表
*/
List<CustomerStatisticsDataV2> selectTagLevelList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("groupName") String groupName
);
/**
* 根据企业ID日期组名查询组级数据
*/
CustomerStatisticsDataV2 selectGroupLevelByCorpDateGroup(
@Param("corpId") String corpId,
@Param("curDate") Date curDate,
@Param("groupName") String groupName
);
/**
* 根据企业ID日期组名查询标签级数据列表
*/
List<CustomerStatisticsDataV2> selectTagLevelByCorpDateGroup(
@Param("corpId") String corpId,
@Param("curDate") Date curDate,
@Param("groupName") String groupName
);
/**
* 批量插入数据
*/
int batchInsert(@Param("list") List<CustomerStatisticsDataV2> list);
/**
* 删除指定日期范围的数据
*/
int deleteByDateRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate
);
/**
* 查询树状结构数据+标签
*/
List<CustomerStatisticsDataV2> selectTreeData(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate
);
/**
* 根据筛选条件查询数据列表支持按组标签筛选
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param groupName 组名可选
* @param tagName 标签名可选
* @return 数据列表
*/
List<CustomerStatisticsDataV2> selectListByFilter(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("groupName") String groupName,
@Param("tagName") String tagName
);
/**
* 按日期范围聚合查询支持按组标签筛选
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param groupName 组名可选
* @param tagName 标签名可选
* @return 聚合后的数据列表
*/
List<CustomerStatisticsDataV2> selectAggregatedByDateRange(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("groupName") String groupName,
@Param("tagName") String tagName
);
/**
* 查询所有数据并聚合支持按组标签筛选
* @param corpId 企业ID
* @param groupName 组名可选
* @param tagName 标签名可选
* @return 聚合后的数据列表
*/
List<CustomerStatisticsDataV2> selectAllAggregated(
@Param("corpId") String corpId,
@Param("groupName") String groupName,
@Param("tagName") String tagName
);
}

View File

@ -0,0 +1,147 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsDataV2;
import com.ruoyi.excel.wecom.domain.dto.TagTreeDTO;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 客户统计数据V2 Service接口
* 支持标签级成本行列转换存储
*/
public interface ICustomerStatisticsDataV2Service {
/**
* 查询客户统计数据V2列表
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 客户统计数据V2列表
*/
List<CustomerStatisticsDataV2> selectCustomerStatisticsDataV2List(
String corpId, Date startDate, Date endDate);
/**
* 查询树状结构数据+标签
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 树状结构数据列表
*/
List<CustomerStatisticsDataV2> selectTreeData(
String corpId, Date startDate, Date endDate);
/**
* 查询标签树只返回组-标签结构不返回统计数据
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 标签树列表
*/
List<TagTreeDTO> selectTagTree(String corpId, Date startDate, Date endDate);
/**
* 查询客户统计数据V2列表支持按组标签筛选
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param groupName 组名可选
* @param tagName 标签名可选
* @return 客户统计数据V2列表
*/
List<CustomerStatisticsDataV2> selectCustomerStatisticsDataV2List(
String corpId, Date startDate, Date endDate, String groupName, String tagName);
/**
* 根据ID查询客户统计数据V2
* @param id 主键ID
* @return 客户统计数据V2
*/
CustomerStatisticsDataV2 selectCustomerStatisticsDataV2ById(Long id);
/**
* 新增客户统计数据V2
* @param data 客户统计数据V2
* @return 结果
*/
int insertCustomerStatisticsDataV2(CustomerStatisticsDataV2 data);
/**
* 修改客户统计数据V2
* @param data 客户统计数据V2
* @return 结果
*/
int updateCustomerStatisticsDataV2(CustomerStatisticsDataV2 data);
/**
* 批量删除客户统计数据V2
* @param ids 需要删除的数据ID
* @return 结果
*/
int deleteCustomerStatisticsDataV2ByIds(Long[] ids);
/**
* 录入成本支持组级和标签级
* @param corpId 企业ID
* @param date 日期
* @param groupName 组名
* @param tagName 标签名null表示组级
* @param costValue 成本值
* @param inputType 录入类型total-总成本single-单条成本
* @return 结果
*/
int inputCost(String corpId, Date date, String groupName, String tagName,
BigDecimal costValue, String inputType);
/**
* 重新计算指定日期的统计数据
* @param corpId 企业ID
* @param date 日期
* @return 结果
*/
int recalculateStatistics(String corpId, Date date);
/**
* 重新计算指定日期范围的统计数据
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 结果
*/
int recalculateStatisticsRange(String corpId, Date startDate, Date endDate);
/**
* 按周聚合查询
* @param corpId 企业ID
* @param year 年份
* @param week 周数
* @param groupName 组名可选
* @param tagName 标签名可选
* @return 聚合后的数据列表
*/
List<CustomerStatisticsDataV2> selectByWeekAggregation(
String corpId, Integer year, Integer week, String groupName, String tagName);
/**
* 按月聚合查询
* @param corpId 企业ID
* @param yearMonth 年月格式yyyy-MM
* @param groupName 组名可选
* @param tagName 标签名可选
* @return 聚合后的数据列表
*/
List<CustomerStatisticsDataV2> selectByMonthAggregation(
String corpId, String yearMonth, String groupName, String tagName);
/**
* 查询所有数据的聚合
* @param corpId 企业ID
* @param groupName 组名可选
* @param tagName 标签名可选
* @return 聚合后的数据列表
*/
List<CustomerStatisticsDataV2> selectAllAggregation(
String corpId, String groupName, String tagName);
}

View File

@ -19,6 +19,7 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.WeekFields; import java.time.temporal.WeekFields;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -245,8 +246,131 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
@Override @Override
public List<CustomerStatisticsDataVO> selectByWeekAggregation(String corpId, Integer year, Integer week, String indicatorName) { public List<CustomerStatisticsDataVO> selectByWeekAggregation(String corpId, Integer year, Integer week, String indicatorName) {
List<CustomerStatisticsData> dailyDataList = customerStatisticsDataMapper.selectDailyDataByWeek(corpId, year, week, indicatorName); log.info("========== V1周聚合查询开始 ==========");
return aggregateDataList(dailyDataList, year, week, null); log.info("参数: corpId={}, year={}, week={}, indicatorName={}", corpId, year, week, indicatorName);
// 计算周范围修复跨年问题
Date[] weekRange = calculateWeekRangeFixed(year, week);
if (weekRange == null) {
// 如果周范围无效跨年返回空列表
log.info("周范围无效(跨年),返回空列表");
return new ArrayList<>();
}
Date startDate = weekRange[0];
Date endDate = weekRange[1];
log.info("周日期范围: {} 至 {}", startDate, endDate);
// 使用日期范围查询修复跨年周问题
List<CustomerStatisticsData> dailyDataList = customerStatisticsDataMapper.selectDailyDataByWeekRange(corpId, startDate, endDate, indicatorName);
log.info("查询到原始数据: {}条", dailyDataList != null ? dailyDataList.size() : 0);
if (dailyDataList != null && !dailyDataList.isEmpty()) {
// 按指标分组统计
Map<String, Long> indicatorCount = dailyDataList.stream()
.collect(Collectors.groupingBy(CustomerStatisticsData::getIndicatorName, Collectors.counting()));
log.info("指标分布: {}", indicatorCount);
// 查看第一条数据的详细信息
CustomerStatisticsData first = dailyDataList.get(0);
log.info("第一条数据: date={}, indicator={}, ntfGroup={}, ofhGroup={}, wa1Group={}, xb1Group={}",
first.getCurDate(), first.getIndicatorName(),
first.getNtfGroup(), first.getOfhGroup(), first.getWa1Group(), first.getXb1Group());
}
List<CustomerStatisticsDataVO> result = aggregateDataList(dailyDataList, year, week, null);
log.info("聚合后结果: {}条", result != null ? result.size() : 0);
log.info("========== V1周聚合查询结束 ==========");
return result;
}
/**
* 计算周范围修复跨年问题
* 规则
* 1. 周的第一天是周一不是周日
* 2. 第一周从1月1日开始到1月1日所在周的周日结束
* - 如果1月1日是周日第一周只有1天1月1日当天
* 3. 最后一周从最后一周的周一12月31日往前推开始到12月31日结束
* - 如果12月31日是周一最后一周只有1天12月31日当天
* 4. 其他周按正常的周一到周日计算
*/
private Date[] calculateWeekRangeFixed(int year, int week) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, Calendar.JANUARY);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// 获取1月1日是星期几1=周日2=周一...7=周六
int jan1DayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
// 转换为0=周日1=周一...6=周六
int jan1Weekday = (jan1DayOfWeek == Calendar.SUNDAY) ? 0 : (jan1DayOfWeek - 1);
// 计算12月31日
Calendar dec31Cal = Calendar.getInstance();
dec31Cal.set(Calendar.YEAR, year);
dec31Cal.set(Calendar.MONTH, Calendar.DECEMBER);
dec31Cal.set(Calendar.DAY_OF_MONTH, 31);
dec31Cal.set(Calendar.HOUR_OF_DAY, 0);
dec31Cal.set(Calendar.MINUTE, 0);
dec31Cal.set(Calendar.SECOND, 0);
dec31Cal.set(Calendar.MILLISECOND, 0);
// 第一周从1月1日开始
if (week == 1) {
Date startDate = calendar.getTime();
// 计算第一周的结束日期周日
// 如果1月1日是周日(0)则第一周只有1天当天
// 否则计算到本周日
int daysToSunday;
if (jan1Weekday == 0) {
daysToSunday = 0; // 1月1日是周日第一周只有1天
} else {
daysToSunday = 7 - jan1Weekday; // 到本周日
}
calendar.add(Calendar.DAY_OF_MONTH, daysToSunday);
Date endDate = calendar.getTime();
return new Date[]{startDate, endDate};
}
// 计算第一周结束日期1月1日所在周的周日
Calendar firstWeekEndCal = (Calendar) calendar.clone();
int firstWeekDays;
if (jan1Weekday == 0) {
firstWeekDays = 0; // 1月1日是周日第一周只有1天
} else {
firstWeekDays = 7 - jan1Weekday; // 到本周日
}
firstWeekEndCal.add(Calendar.DAY_OF_MONTH, firstWeekDays);
// 计算第二周开始日期第一周结束后的周一
Calendar secondWeekStartCal = (Calendar) firstWeekEndCal.clone();
secondWeekStartCal.add(Calendar.DAY_OF_MONTH, 1);
// 计算目标周的开始和结束
// 从第二周开始每周都是周一到周日
Calendar targetWeekStartCal = (Calendar) secondWeekStartCal.clone();
targetWeekStartCal.add(Calendar.WEEK_OF_YEAR, week - 2);
// 如果开始日期已经跨年了大于12月31日则返回null
if (targetWeekStartCal.after(dec31Cal)) {
return null;
}
Date startDate = targetWeekStartCal.getTime();
Calendar targetWeekEndCal = (Calendar) targetWeekStartCal.clone();
targetWeekEndCal.add(Calendar.DAY_OF_MONTH, 6);
Date endDate = targetWeekEndCal.getTime();
// 如果目标周的结束超过了12月31日则调整到12月31日
if (targetWeekEndCal.after(dec31Cal)) {
endDate = dec31Cal.getTime();
}
return new Date[]{startDate, endDate};
} }
@Override @Override
@ -422,11 +546,19 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
return result; return result;
} }
// 针对成单数和进粉数添加详细日志
boolean isTargetIndicator = indicatorName.equals("成单数(当日)") || indicatorName.equals("进粉数(当日)");
if (isTargetIndicator) {
log.info("========== 开始聚合指标: {} ==========", indicatorName);
}
for (String field : groupFields) { for (String field : groupFields) {
BigDecimal sum = BigDecimal.ZERO; BigDecimal sum = BigDecimal.ZERO;
boolean hasValue = false; boolean hasValue = false;
for (CustomerStatisticsData data : dailyList) { for (CustomerStatisticsData data : dailyList) {
String value = getFieldValue(data, field); String value = getFieldValue(data, field);
if (value != null && !value.trim().isEmpty()) { if (value != null && !value.trim().isEmpty()) {
String cleanValue = value.replace("%", "").trim(); String cleanValue = value.replace("%", "").trim();
if (indicatorName.equals("总成本(当日)") && !cleanValue.equals("需手工填写")) { if (indicatorName.equals("总成本(当日)") && !cleanValue.equals("需手工填写")) {
@ -438,21 +570,32 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
} }
if (cleanValue.matches("-?\\d+(\\.\\d+)?")) { if (cleanValue.matches("-?\\d+(\\.\\d+)?")) {
try { try {
sum = sum.add(new BigDecimal(cleanValue)); BigDecimal decimalValue = new BigDecimal(cleanValue);
sum = sum.add(decimalValue);
hasValue = true; hasValue = true;
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
if (isTargetIndicator) {
log.info(" 解析失败: {}", cleanValue);
}
} }
} }
} }
} }
if (hasValue) { if (hasValue) {
setFieldValue(result, field, sum.setScale(2, RoundingMode.HALF_UP).toString()); setFieldValue(result, field, sum.setScale(2, RoundingMode.HALF_UP).toString());
if (isTargetIndicator) {
log.info("指标 {} 字段 {} 最终聚合值: {}", indicatorName, field, sum.setScale(2, RoundingMode.HALF_UP)); log.info("指标 {} 字段 {} 最终聚合值: {}", indicatorName, field, sum.setScale(2, RoundingMode.HALF_UP));
}
} else if (indicatorName.equals("总成本(当日)")) { } else if (indicatorName.equals("总成本(当日)")) {
setFieldValue(result, field, "0"); setFieldValue(result, field, "0");
} }
} }
if (isTargetIndicator) {
log.info("========== 结束聚合指标: {} ==========", indicatorName);
}
return result; return result;
} }
@ -469,6 +612,8 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
CustomerStatisticsDataVO keHuShuXingData = indicatorMap.get("客户属性数量(当日)"); CustomerStatisticsDataVO keHuShuXingData = indicatorMap.get("客户属性数量(当日)");
CustomerStatisticsDataVO xueShengZhanBiData = indicatorMap.get("学生占比(当日)"); CustomerStatisticsDataVO xueShengZhanBiData = indicatorMap.get("学生占比(当日)");
CustomerStatisticsDataVO weiZhiZhanBiData = indicatorMap.get("未知占比(当日)"); CustomerStatisticsDataVO weiZhiZhanBiData = indicatorMap.get("未知占比(当日)");
CustomerStatisticsDataVO laoshiZhanBiData = indicatorMap.get("老师占比(当日)");
CustomerStatisticsDataVO zhuDongBaoJiaZhanBiData = indicatorMap.get("主动报价占比(当日)"); CustomerStatisticsDataVO zhuDongBaoJiaZhanBiData = indicatorMap.get("主动报价占比(当日)");
CustomerStatisticsDataVO beiDongBaoJiaZhanBiData = indicatorMap.get("被动报价占比(当日)"); CustomerStatisticsDataVO beiDongBaoJiaZhanBiData = indicatorMap.get("被动报价占比(当日)");
CustomerStatisticsDataVO weiKaiKouBaoJiaZhanBiData = indicatorMap.get("未开口报价占比(当日)"); CustomerStatisticsDataVO weiKaiKouBaoJiaZhanBiData = indicatorMap.get("未开口报价占比(当日)");
@ -486,6 +631,7 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
CustomerStatisticsDataVO feiJiShiDanShuLiangData = indicatorMap.get("非及时单数量(当日)"); CustomerStatisticsDataVO feiJiShiDanShuLiangData = indicatorMap.get("非及时单数量(当日)");
CustomerStatisticsDataVO jiaZhangShuLiangData = indicatorMap.get("家长数量(当日)"); CustomerStatisticsDataVO jiaZhangShuLiangData = indicatorMap.get("家长数量(当日)");
CustomerStatisticsDataVO xueShengShuLiangData = indicatorMap.get("学生数量(当日)"); CustomerStatisticsDataVO xueShengShuLiangData = indicatorMap.get("学生数量(当日)");
CustomerStatisticsDataVO laoshiShuLiangData = indicatorMap.get("老师数量(当日)");
CustomerStatisticsDataVO weiZhiShuLiangData = indicatorMap.get("未知数量(当日)"); CustomerStatisticsDataVO weiZhiShuLiangData = indicatorMap.get("未知数量(当日)");
CustomerStatisticsDataVO zhuDongBaoJiaShuLiangData = indicatorMap.get("主动报价数量(当日)"); CustomerStatisticsDataVO zhuDongBaoJiaShuLiangData = indicatorMap.get("主动报价数量(当日)");
CustomerStatisticsDataVO beiDongBaoJiaShuLiangData = indicatorMap.get("被动报价数量(当日)"); CustomerStatisticsDataVO beiDongBaoJiaShuLiangData = indicatorMap.get("被动报价数量(当日)");
@ -497,6 +643,11 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
CustomerStatisticsDataVO jiaZhangChuDanShuLiangData = indicatorMap.get("家长出单数量(当日)"); CustomerStatisticsDataVO jiaZhangChuDanShuLiangData = indicatorMap.get("家长出单数量(当日)");
CustomerStatisticsDataVO xueShengChuDanShuLiangData = indicatorMap.get("学生出单数量(当日)"); CustomerStatisticsDataVO xueShengChuDanShuLiangData = indicatorMap.get("学生出单数量(当日)");
CustomerStatisticsDataVO jiazhangChuDanLvData = indicatorMap.get("家长出单率(当日)");
CustomerStatisticsDataVO jiazhangJishiDanShuLiangData = indicatorMap.get("家长即时单数量(当日)");
CustomerStatisticsDataVO xueshengChuDanLvData = indicatorMap.get("学生出单率(当日)");
CustomerStatisticsDataVO xueshengJishiDanShuLiangData = indicatorMap.get("学生即时单数量(当日)");
String[] groupFields = {"ntfGroup", "ofhGroup", "pswGroup", "wa1Group", "xb1Group", String[] groupFields = {"ntfGroup", "ofhGroup", "pswGroup", "wa1Group", "xb1Group",
"yc1Group", "zd1Group", "aaGroup", "acGroup", "adGroup", "aeGroup"}; "yc1Group", "zd1Group", "aaGroup", "acGroup", "adGroup", "aeGroup"};
@ -583,16 +734,16 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
} }
} }
if (jiShiDanZhanBiData != null && jiShiDanShuLiangData != null && jinFenShuData != null) { if (jiShiDanZhanBiData != null && jiShiDanShuLiangData != null && chengDanShuData != null) {
for (String field : groupFields) { for (String field : groupFields) {
String jiShiDanStr = getFieldValue(jiShiDanShuLiangData, field); String jiShiDanStr = getFieldValue(jiShiDanShuLiangData, field);
String jinFenShuStr = getFieldValue(jinFenShuData, field); String chengDanShuStr = getFieldValue(chengDanShuData, field);
if (jiShiDanStr != null && jinFenShuStr != null) { if (jiShiDanStr != null && chengDanShuStr != null) {
try { try {
BigDecimal jiShiDan = new BigDecimal(jiShiDanStr); BigDecimal jiShiDan = new BigDecimal(jiShiDanStr);
BigDecimal jinFenShu = new BigDecimal(jinFenShuStr); BigDecimal chengDanShu = new BigDecimal(chengDanShuStr);
if (jinFenShu.compareTo(BigDecimal.ZERO) > 0) { if (chengDanShu.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal zhanBi = jiShiDan.divide(jinFenShu, 4, RoundingMode.HALF_UP) BigDecimal zhanBi = jiShiDan.divide(chengDanShu, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100")) .multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP); .setScale(2, RoundingMode.HALF_UP);
setFieldValue(jiShiDanZhanBiData, field, zhanBi + "%"); setFieldValue(jiShiDanZhanBiData, field, zhanBi + "%");
@ -605,16 +756,16 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
} }
} }
if (feiJiShiDanZhanBiData != null && feiJiShiDanShuLiangData != null && jinFenShuData != null) { if (feiJiShiDanZhanBiData != null && feiJiShiDanShuLiangData != null && chengDanShuData != null) {
for (String field : groupFields) { for (String field : groupFields) {
String feiJiShiDanStr = getFieldValue(feiJiShiDanShuLiangData, field); String feiJiShiDanStr = getFieldValue(feiJiShiDanShuLiangData, field);
String jinFenShuStr = getFieldValue(jinFenShuData, field); String chengDanShuStr = getFieldValue(chengDanShuData, field);
if (feiJiShiDanStr != null && jinFenShuStr != null) { if (feiJiShiDanStr != null && chengDanShuStr != null) {
try { try {
BigDecimal feiJiShiDan = new BigDecimal(feiJiShiDanStr); BigDecimal feiJiShiDan = new BigDecimal(feiJiShiDanStr);
BigDecimal jinFenShu = new BigDecimal(jinFenShuStr); BigDecimal chengDanShu = new BigDecimal(chengDanShuStr);
if (jinFenShu.compareTo(BigDecimal.ZERO) > 0) { if (chengDanShu.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal zhanBi = feiJiShiDan.divide(jinFenShu, 4, RoundingMode.HALF_UP) BigDecimal zhanBi = feiJiShiDan.divide(chengDanShu, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100")) .multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP); .setScale(2, RoundingMode.HALF_UP);
setFieldValue(feiJiShiDanZhanBiData, field, zhanBi + "%"); setFieldValue(feiJiShiDanZhanBiData, field, zhanBi + "%");
@ -627,16 +778,18 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
} }
} }
if (keHuShuXingData != null && jiaZhangShuLiangData != null && xueShengShuLiangData != null && weiZhiShuLiangData != null) { if (keHuShuXingData != null && jiaZhangShuLiangData != null && laoshiShuLiangData != null && xueShengShuLiangData != null && weiZhiShuLiangData != null) {
for (String field : groupFields) { for (String field : groupFields) {
String jiaZhangStr = getFieldValue(jiaZhangShuLiangData, field); String jiaZhangStr = getFieldValue(jiaZhangShuLiangData, field);
String xueShengStr = getFieldValue(xueShengShuLiangData, field); String xueShengStr = getFieldValue(xueShengShuLiangData, field);
String laoShiStr = getFieldValue(laoshiShuLiangData, field);
String weiZhiStr = getFieldValue(weiZhiShuLiangData, field); String weiZhiStr = getFieldValue(weiZhiShuLiangData, field);
try { try {
BigDecimal jiaZhang = jiaZhangStr != null ? new BigDecimal(jiaZhangStr) : BigDecimal.ZERO; BigDecimal jiaZhang = jiaZhangStr != null ? new BigDecimal(jiaZhangStr) : BigDecimal.ZERO;
BigDecimal xueSheng = xueShengStr != null ? new BigDecimal(xueShengStr) : BigDecimal.ZERO; BigDecimal xueSheng = xueShengStr != null ? new BigDecimal(xueShengStr) : BigDecimal.ZERO;
BigDecimal laoshi = laoShiStr != null ? new BigDecimal(laoShiStr) : BigDecimal.ZERO;
BigDecimal weiZhi = weiZhiStr != null ? new BigDecimal(weiZhiStr) : BigDecimal.ZERO; BigDecimal weiZhi = weiZhiStr != null ? new BigDecimal(weiZhiStr) : BigDecimal.ZERO;
BigDecimal total = jiaZhang.add(xueSheng).add(weiZhi); BigDecimal total = jiaZhang.add(xueSheng).add(weiZhi).add(laoshi);
setFieldValue(keHuShuXingData, field, total.setScale(0, RoundingMode.HALF_UP).toString()); setFieldValue(keHuShuXingData, field, total.setScale(0, RoundingMode.HALF_UP).toString());
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
} }
@ -709,6 +862,29 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
} }
} }
if (laoshiZhanBiData != null && laoshiShuLiangData != null && keHuShuXingData != null) {
for (String field : groupFields) {
String laoshiStr = getFieldValue(laoshiShuLiangData, field);
String keHuShuXingStr = getFieldValue(keHuShuXingData, field);
if (laoshiStr != null && keHuShuXingStr != null) {
try {
BigDecimal laoshi = new BigDecimal(laoshiStr);
BigDecimal keHuShuXing = new BigDecimal(keHuShuXingStr);
if (keHuShuXing.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal zhanBi = laoshi.divide(keHuShuXing, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP);
setFieldValue(laoshiZhanBiData, field, zhanBi + "%");
} else {
setFieldValue(laoshiZhanBiData, field, "0%");
}
} catch (NumberFormatException e) {
}
}
}
}
if (zhuDongBaoJiaZhanBiData != null && zhuDongBaoJiaShuLiangData != null && yiXiangDuShuLiangData != null) { if (zhuDongBaoJiaZhanBiData != null && zhuDongBaoJiaShuLiangData != null && yiXiangDuShuLiangData != null) {
for (String field : groupFields) { for (String field : groupFields) {
String zhuDongStr = getFieldValue(zhuDongBaoJiaShuLiangData, field); String zhuDongStr = getFieldValue(zhuDongBaoJiaShuLiangData, field);
@ -863,7 +1039,52 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
} }
} }
if (jiaZhangChuDanZhanBiData != null && jiaZhangChuDanShuLiangData != null && jiaZhangShuLiangData != null) { if (jiazhangChuDanLvData != null && jiaZhangShuLiangData != null && jiazhangJishiDanShuLiangData != null) {
for (String field : groupFields) {
String jiazhangJishidanStr = getFieldValue(jiazhangJishiDanShuLiangData, field);
String jiazhangShuLiangStr = getFieldValue(jiaZhangShuLiangData, field);
if (jiazhangJishidanStr != null && jiazhangShuLiangStr != null) {
try {
BigDecimal jiasZhangJishidan = new BigDecimal(jiazhangJishidanStr);
BigDecimal jiazhangshuliang = new BigDecimal(jiazhangShuLiangStr);
if (jiazhangshuliang.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal zhanBi = jiasZhangJishidan.divide(jiazhangshuliang, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP);
setFieldValue(jiazhangChuDanLvData, field, zhanBi + "%");
} else {
setFieldValue(jiazhangChuDanLvData, field, "0%");
}
} catch (NumberFormatException e) {
}
}
}
}
if (xueshengChuDanLvData != null && xueShengShuLiangData != null && xueshengJishiDanShuLiangData != null) {
for (String field : groupFields) {
String xueshengJishidanStr = getFieldValue(xueshengJishiDanShuLiangData, field);
String xueshengShuLiangStr = getFieldValue(xueShengShuLiangData, field);
if (xueshengJishidanStr != null && xueshengShuLiangStr != null) {
try {
BigDecimal xueshengJishidan = new BigDecimal(xueshengJishidanStr);
BigDecimal xueshengshuliang = new BigDecimal(xueshengShuLiangStr);
if (xueshengshuliang.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal zhanBi = xueshengJishidan.divide(xueshengshuliang, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"))
.setScale(2, RoundingMode.HALF_UP);
setFieldValue(xueshengChuDanLvData, field, zhanBi + "%");
} else {
setFieldValue(xueshengChuDanLvData, field, "0%");
}
} catch (NumberFormatException e) {
}
}
}
}
/* if (jiaZhangChuDanZhanBiData != null && jiaZhangChuDanShuLiangData != null && jiaZhangShuLiangData != null) {
for (String field : groupFields) { for (String field : groupFields) {
String jiaZhangChuDanStr = getFieldValue(jiaZhangChuDanShuLiangData, field); String jiaZhangChuDanStr = getFieldValue(jiaZhangChuDanShuLiangData, field);
String jiaZhangStr = getFieldValue(jiaZhangShuLiangData, field); String jiaZhangStr = getFieldValue(jiaZhangShuLiangData, field);
@ -905,7 +1126,7 @@ public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDat
} }
} }
} }
} }*/
} }
private BigDecimal parseCostValue(String value) { private BigDecimal parseCostValue(String value) {

View File

@ -0,0 +1,718 @@
package com.ruoyi.excel.wecom.service.impl;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsDataV2;
import com.ruoyi.excel.wecom.domain.dto.TagTreeDTO;
import com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper;
import com.ruoyi.excel.wecom.mapper.CustomerStatisticsDataV2Mapper;
import com.ruoyi.excel.wecom.service.ICustomerStatisticsDataV2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
/**
* 客户统计数据V2 Service业务层处理
* 支持标签级成本行列转换存储
*/
@Slf4j
@Service
public class CustomerStatisticsDataV2ServiceImpl implements ICustomerStatisticsDataV2Service {
@Autowired
private CustomerStatisticsDataV2Mapper dataV2Mapper;
@Autowired
private CustomerExportDataMapper exportDataMapper;
private List<String> finishFlag = Arrays.asList("已成交及时单9元+", "已成交非及时单9元+");
private List<String> timelyFinishFlag = Arrays.asList("已成交及时单9元+");
private List<String> noTimelyfinishFlag = Arrays.asList("已成交非及时单9元+");
/**
* 组名到数据库字段的映射
*/
private static final Map<String, String> GROUP_NAME_TO_ATTR_MAP = new LinkedHashMap<>();
static {
GROUP_NAME_TO_ATTR_MAP.put("投放", "tag_group1");
GROUP_NAME_TO_ATTR_MAP.put("公司孵化", "tag_group2");
GROUP_NAME_TO_ATTR_MAP.put("商务", "tag_group3");
GROUP_NAME_TO_ATTR_MAP.put("A1组", "tag_group10");
GROUP_NAME_TO_ATTR_MAP.put("B1组", "tag_group11");
GROUP_NAME_TO_ATTR_MAP.put("C1组", "tag_group12");
GROUP_NAME_TO_ATTR_MAP.put("D1组", "tag_group13");
GROUP_NAME_TO_ATTR_MAP.put("E1组", "tag_group14");
GROUP_NAME_TO_ATTR_MAP.put("自然流", "tag_group16");
GROUP_NAME_TO_ATTR_MAP.put("F1组", "tag_group17");
GROUP_NAME_TO_ATTR_MAP.put("G1组", "tag_group18");
}
@Override
public List<CustomerStatisticsDataV2> selectCustomerStatisticsDataV2List(
String corpId, Date startDate, Date endDate) {
return dataV2Mapper.selectGroupLevelList(corpId, startDate, endDate);
}
@Override
public List<CustomerStatisticsDataV2> selectCustomerStatisticsDataV2List(
String corpId, Date startDate, Date endDate, String groupName, String tagName) {
return dataV2Mapper.selectListByFilter(corpId, startDate, endDate, groupName, tagName);
}
@Override
public List<TagTreeDTO> selectTagTree(String corpId, Date startDate, Date endDate) {
log.info("selectTagTree called with corpId={}, startDate={}, endDate={}", corpId, startDate, endDate);
// 查询所有数据提取组-标签结构
List<CustomerStatisticsDataV2> allData = dataV2Mapper.selectTreeData(corpId, startDate, endDate);
log.info("selectTreeData returned {} records", allData.size());
// 按组名分组
Map<String, List<CustomerStatisticsDataV2>> groupDataMap = allData.stream()
.filter(d -> d.getGroupName() != null)
.collect(Collectors.groupingBy(CustomerStatisticsDataV2::getGroupName));
log.info("groupDataMap has {} groups", groupDataMap.size());
List<TagTreeDTO> result = new ArrayList<>();
for (Map.Entry<String, List<CustomerStatisticsDataV2>> entry : groupDataMap.entrySet()) {
String groupName = entry.getKey();
List<CustomerStatisticsDataV2> groupDataList = entry.getValue();
// 创建组节点
TagTreeDTO groupNode = new TagTreeDTO();
groupNode.setId("group_" + groupName);
groupNode.setLabel(groupName);
groupNode.setType("group");
groupNode.setGroupName(groupName);
groupNode.setChildren(new ArrayList<>());
// 提取该组下的所有标签
Set<String> tagSet = new LinkedHashSet<>();
for (CustomerStatisticsDataV2 data : groupDataList) {
if (data.getTagName() != null && !data.getTagName().isEmpty()) {
tagSet.add(data.getTagName());
}
}
// 创建标签节点
for (String tagName : tagSet) {
TagTreeDTO tagNode = new TagTreeDTO();
tagNode.setId("tag_" + groupName + "_" + tagName);
tagNode.setLabel(tagName);
tagNode.setType("tag");
tagNode.setGroupName(groupName);
tagNode.setTagName(tagName);
tagNode.setCount(1);
groupNode.getChildren().add(tagNode);
}
groupNode.setCount(groupNode.getChildren().size());
result.add(groupNode);
}
// 按组名排序
result.sort(Comparator.comparing(TagTreeDTO::getLabel));
return result;
}
@Override
public List<CustomerStatisticsDataV2> selectTreeData(
String corpId, Date startDate, Date endDate) {
// 1. 查询所有数据组级+标签级
List<CustomerStatisticsDataV2> allData = dataV2Mapper.selectTreeData(
corpId, startDate, endDate);
// 2. 构建树状结构
Map<Long, CustomerStatisticsDataV2> groupMap = new LinkedHashMap<>();
List<CustomerStatisticsDataV2> result = new ArrayList<>();
// 先处理组级数据
for (CustomerStatisticsDataV2 data : allData) {
if (data.getDataLevel() != null && data.getDataLevel() == 1) {
data.setChildren(new ArrayList<>());
data.setLeaf(false);
groupMap.put(data.getId(), data);
result.add(data);
}
}
// 再处理标签级数据挂载到对应组下
for (CustomerStatisticsDataV2 data : allData) {
if (data.getDataLevel() != null && data.getDataLevel() == 2) {
data.setLeaf(true);
CustomerStatisticsDataV2 parent = groupMap.get(data.getParentId());
if (parent != null) {
parent.getChildren().add(data);
}
}
}
return result;
}
@Override
public CustomerStatisticsDataV2 selectCustomerStatisticsDataV2ById(Long id) {
return dataV2Mapper.selectById(id);
}
@Override
public int insertCustomerStatisticsDataV2(CustomerStatisticsDataV2 data) {
return dataV2Mapper.insert(data);
}
@Override
public int updateCustomerStatisticsDataV2(CustomerStatisticsDataV2 data) {
return dataV2Mapper.updateById(data);
}
@Override
public int deleteCustomerStatisticsDataV2ByIds(Long[] ids) {
int count = 0;
for (Long id : ids) {
count += dataV2Mapper.deleteById(id);
}
return count;
}
@Override
@Transactional
public int inputCost(String corpId, Date date, String groupName, String tagName,
BigDecimal costValue, String inputType) {
try {
// 1. 查询目标记录
CustomerStatisticsDataV2 data = dataV2Mapper.selectByCorpDateGroupTag(
corpId, date, groupName, tagName);
if (data == null) {
throw new RuntimeException("未找到对应的统计数据:" + groupName +
(tagName != null ? "/" + tagName : ""));
}
// 2. 计算实际总成本
BigDecimal actualTotalCost;
boolean isSingleInput = "single".equals(inputType);
if (isSingleInput) {
// 单条成本 × 进粉数 = 总成本
actualTotalCost = costValue.multiply(
new BigDecimal(data.getCustomerCount()));
data.setSingleCost(costValue);
data.setTotalCost(actualTotalCost); // 更新总成本
} else {
actualTotalCost = costValue;
data.setTotalCost(costValue);
}
data.setCostInputType(inputType);
// 3. 计算派生成本指标单条成本录入时保留用户输入的单条成本值
calculateDerivedCosts(data, actualTotalCost, isSingleInput);
// 4. 更新记录
dataV2Mapper.updateById(data);
// 5. 如果是标签级成本需要重新计算组级汇总
if (tagName != null && !tagName.isEmpty()) {
recalculateGroupCost(corpId, date, groupName);
}
log.info("成本录入成功corpId={}, date={}, group={}, tag={}, cost={}",
corpId, date, groupName, tagName, costValue);
return 1;
} catch (Exception e) {
log.error("成本录入失败:" + e.getMessage(), e);
throw new RuntimeException("成本录入失败: " + e.getMessage(), e);
}
}
/**
* 计算派生成本指标
* @param data 数据对象
* @param totalCost 总成本
* @param preserveSingleCost 是否保留原有的单条成本值单条成本录入模式时为true
*/
private void calculateDerivedCosts(CustomerStatisticsDataV2 data, BigDecimal totalCost, boolean preserveSingleCost) {
// 初始化成本为0
if (!preserveSingleCost) {
data.setSingleCost(BigDecimal.ZERO);
}
data.setOrderCost(BigDecimal.ZERO);
// 单条成本 = 总成本 / 进粉数仅在非单条成本录入模式下计算
if (!preserveSingleCost && data.getCustomerCount() != null && data.getCustomerCount() > 0) {
BigDecimal singleCost = totalCost.divide(
new BigDecimal(data.getCustomerCount()), 2, RoundingMode.HALF_UP);
data.setSingleCost(singleCost);
}
// 成单成本 = 总成本 / 成单数
if (data.getOrderCount() != null && data.getOrderCount() > 0) {
BigDecimal orderCost = totalCost.divide(
new BigDecimal(data.getOrderCount()), 2, RoundingMode.HALF_UP);
data.setOrderCost(orderCost);
}
}
/**
* 计算派生成本指标默认不保留单条成本
*/
private void calculateDerivedCosts(CustomerStatisticsDataV2 data, BigDecimal totalCost) {
calculateDerivedCosts(data, totalCost, false);
}
/**
* 重新计算组级成本汇总
*/
private void recalculateGroupCost(String corpId, Date date, String groupName) {
// 1. 查询该组下所有标签级数据
List<CustomerStatisticsDataV2> tagDataList = dataV2Mapper
.selectTagLevelByCorpDateGroup(corpId, date, groupName);
// 2. 汇总标签成本
BigDecimal totalCostSum = BigDecimal.ZERO;
int totalCustomerCount = 0;
for (CustomerStatisticsDataV2 tagData : tagDataList) {
if (tagData.getTotalCost() != null) {
totalCostSum = totalCostSum.add(tagData.getTotalCost());
}
if (tagData.getCustomerCount() != null) {
totalCustomerCount += tagData.getCustomerCount();
}
}
// 3. 更新组级记录
CustomerStatisticsDataV2 groupData = dataV2Mapper
.selectGroupLevelByCorpDateGroup(corpId, date, groupName);
if (groupData != null) {
groupData.setTotalCost(totalCostSum);
if (totalCustomerCount > 0) {
BigDecimal avgSingleCost = totalCostSum.divide(
new BigDecimal(totalCustomerCount), 2, RoundingMode.HALF_UP);
groupData.setSingleCost(avgSingleCost);
}
calculateDerivedCosts(groupData, totalCostSum);
dataV2Mapper.updateById(groupData);
log.info("组级成本重新计算完成group={}, totalCost={}", groupName, totalCostSum);
}
}
@Override
public int recalculateStatistics(String corpId, Date date) {
// 删除旧数据
dataV2Mapper.deleteByDateRange(corpId, date, date);
// 重新计算逻辑在 HandleAllDataV2 中实现
log.info("统计数据已删除等待重新计算corpId={}, date={}", corpId, date);
return 1;
}
@Override
public int recalculateStatisticsRange(String corpId, Date startDate, Date endDate) {
// 删除旧数据
dataV2Mapper.deleteByDateRange(corpId, startDate, endDate);
// 重新计算逻辑在 HandleAllDataV2 中实现
log.info("统计数据已删除等待重新计算corpId={}, startDate={}, endDate={}",
corpId, startDate, endDate);
return 1;
}
@Override
public List<CustomerStatisticsDataV2> selectByWeekAggregation(
String corpId, Integer year, Integer week, String groupName, String tagName) {
log.info("周聚合查询: corpId={}, year={}, week={}, groupName={}, tagName={}", corpId, year, week, groupName, tagName);
// 计算周的开始和结束日期使用与V1一致的MySQL WEEK函数逻辑
Date[] weekRange = calculateWeekRangeByMySQL(year, week);
if (weekRange == null) {
// 如果周范围无效跨年返回空列表
log.info("周范围无效(跨年),返回空列表");
return new ArrayList<>();
}
Date startDate = weekRange[0];
Date endDate = weekRange[1];
log.info("周日期范围: {} 至 {}", formatDate(startDate), formatDate(endDate));
// 查询该周的数据并聚合
List<CustomerStatisticsDataV2> result = dataV2Mapper.selectAggregatedByDateRange(corpId, startDate, endDate, groupName, tagName);
log.info("周聚合查询结果: {}条记录", result.size());
// 从原始数据表按finish_date重新查询成单数解决跨日成交问题
recalculateOrderCountFromOriginalData(corpId, startDate, endDate, result);
// 重新计算比率指标SQL只聚合数量比率需要重新计算
for (CustomerStatisticsDataV2 data : result) {
recalculateRates(data);
log.info("记录: groupName={}, tagName={}, dataLevel={}, customerCount={}, orderCount={}, conversionRate={}",
data.getGroupName(), data.getTagName(), data.getDataLevel(), data.getCustomerCount(),
data.getOrderCount(), data.getConversionRate());
}
// 设置显示字段
String yearWeek = year + "年第" + week + "";
String dateRange = formatDate(startDate) + "" + formatDate(endDate);
for (CustomerStatisticsDataV2 data : result) {
data.setYearWeek(yearWeek);
data.setDateRange(dateRange);
}
return result;
}
@Override
public List<CustomerStatisticsDataV2> selectByMonthAggregation(
String corpId, String yearMonth, String groupName, String tagName) {
// 解析年月
String[] parts = yearMonth.split("-");
int year = Integer.parseInt(parts[0]);
int month = Integer.parseInt(parts[1]);
// 计算月的开始和结束日期
Date[] monthRange = calculateMonthRange(year, month);
Date startDate = monthRange[0];
Date endDate = monthRange[1];
// 查询该月的数据并聚合
List<CustomerStatisticsDataV2> result = dataV2Mapper.selectAggregatedByDateRange(corpId, startDate, endDate, groupName, tagName);
// 从原始数据表按finish_date重新查询成单数解决跨日成交问题
recalculateOrderCountFromOriginalData(corpId, startDate, endDate, result);
// 重新计算比率指标
for (CustomerStatisticsDataV2 data : result) {
recalculateRates(data);
}
// 设置显示字段
String yearMonthDisplay = year + "" + String.format("%02d", month) + "";
String dateRange = formatDate(startDate) + "" + formatDate(endDate);
for (CustomerStatisticsDataV2 data : result) {
data.setYearMonth(yearMonthDisplay);
data.setDateRange(dateRange);
}
return result;
}
@Override
public List<CustomerStatisticsDataV2> selectAllAggregation(
String corpId, String groupName, String tagName) {
// 查询所有数据并聚合
List<CustomerStatisticsDataV2> result = dataV2Mapper.selectAllAggregated(corpId, groupName, tagName);
// 从原始数据表按finish_date重新查询成单数解决跨日成交问题
// 对于全部数据不限制日期范围
recalculateOrderCountFromOriginalData(corpId, null, null, result);
// 重新计算比率指标
for (CustomerStatisticsDataV2 data : result) {
recalculateRates(data);
}
// 设置显示字段
for (CustomerStatisticsDataV2 data : result) {
data.setYearWeek("全部数据");
data.setYearMonth("全部数据");
data.setDateRange("全部数据");
}
return result;
}
/**
* 从原始数据表按finish_date重新查询成单数
* 解决跨日成交问题成单数应该按finish_date统计而不是按add_date
* 优化使用批量查询避免N+1问题
*/
private void recalculateOrderCountFromOriginalData(String corpId, Date startDate, Date endDate,
List<CustomerStatisticsDataV2> dataList) {
if (dataList == null || dataList.isEmpty()) {
return;
}
// 批量查询所有组的成单数一次SQL查询
Map<String, Object> orderCountMap = exportDataMapper.selectOrderCountBatchByFinishDateRange(corpId, startDate, endDate,finishFlag);
if (orderCountMap == null) {
orderCountMap = new HashMap<>();
}
// 批量查询所有组的及时单数量一次SQL查询
Map<String, Object> timelyCountMap = exportDataMapper.selectTimelyOrderCountBatchByDateRange(corpId, startDate, endDate,timelyFinishFlag);
if (timelyCountMap == null) {
timelyCountMap = new HashMap<>();
}
// 批量查询所有组的非及时单数量一次SQL查询
Map<String, Object> nonTimelyCountMap = exportDataMapper.selectNonTimelyOrderCountBatchByDateRange(corpId, startDate, endDate,noTimelyfinishFlag);
if (nonTimelyCountMap == null) {
nonTimelyCountMap = new HashMap<>();
}
// 遍历数据从批量查询结果中获取对应的值
for (CustomerStatisticsDataV2 data : dataList) {
String groupName = data.getGroupName();
if (groupName == null || groupName.isEmpty()) {
continue;
}
String attr = GROUP_NAME_TO_ATTR_MAP.get(groupName);
if (attr == null) {
continue;
}
// 从批量查询结果中获取成单数
Object orderCountObj = orderCountMap.get(attr);
int orderCount = 0;
if (orderCountObj != null) {
if (orderCountObj instanceof Number) {
orderCount = ((Number) orderCountObj).intValue();
} else {
try {
orderCount = Integer.parseInt(orderCountObj.toString());
} catch (NumberFormatException e) {
log.warn("无法解析成单数: {}", orderCountObj);
}
}
}
data.setOrderCount(orderCount);
// 从批量查询结果中获取及时单数量
Object timelyCountObj = timelyCountMap.get(attr);
int timelyCount = 0;
if (timelyCountObj != null) {
if (timelyCountObj instanceof Number) {
timelyCount = ((Number) timelyCountObj).intValue();
} else {
try {
timelyCount = Integer.parseInt(timelyCountObj.toString());
} catch (NumberFormatException e) {
log.warn("无法解析及时单数量: {}", timelyCountObj);
}
}
}
data.setTimelyOrderCount(timelyCount);
// 从批量查询结果中获取非及时单数量
Object nonTimelyCountObj = nonTimelyCountMap.get(attr);
int nonTimelyCount = 0;
if (nonTimelyCountObj != null) {
if (nonTimelyCountObj instanceof Number) {
nonTimelyCount = ((Number) nonTimelyCountObj).intValue();
} else {
try {
nonTimelyCount = Integer.parseInt(nonTimelyCountObj.toString());
} catch (NumberFormatException e) {
log.warn("无法解析非及时单数量: {}", nonTimelyCountObj);
}
}
}
data.setNonTimelyOrderCount(nonTimelyCount);
log.debug("重新计算成单数: groupName={}, orderCount={}, timelyCount={}, nonTimelyCount={}",
groupName, data.getOrderCount(), data.getTimelyOrderCount(), data.getNonTimelyOrderCount());
}
}
/**
* 重新计算比率指标
* 在SQL聚合数量后需要重新计算各种比率
*/
private void recalculateRates(CustomerStatisticsDataV2 data) {
// 转化率 = 成单数 / 进粉数
data.setConversionRate(calculateRate(data.getOrderCount(), data.getCustomerCount()));
// 及时单占比 = 及时单数 / 成单数
data.setTimelyRate(calculateRate(data.getTimelyOrderCount(), data.getOrderCount()));
// 非及时单占比 = 非及时单数 / 成单数
data.setNonTimelyRate(calculateRate(data.getNonTimelyOrderCount(), data.getOrderCount()));
// 客户属性占比
data.setParentRate(calculateRate(data.getParentCount(), data.getCustomerAttrCount()));
data.setStudentRate(calculateRate(data.getStudentCount(), data.getCustomerAttrCount()));
data.setTeacherRate(calculateRate(data.getTeacherCount(), data.getCustomerAttrCount()));
data.setUnknownRate(calculateRate(data.getUnknownAttrCount(), data.getCustomerAttrCount()));
// 意向度占比
data.setActiveQuoteRate(calculateRate(data.getActiveQuoteCount(), data.getIntentionCount()));
data.setPassiveQuoteRate(calculateRate(data.getPassiveQuoteCount(), data.getIntentionCount()));
data.setNoQuoteRate(calculateRate(data.getNoQuoteCount(), data.getIntentionCount()));
data.setDeletedQuoteRate(calculateRate(data.getDeletedQuoteCount(), data.getIntentionCount()));
// 年级占比
data.setPrimaryRate(calculateRate(data.getPrimaryCount(), data.getGradeCount()));
data.setMiddleRate(calculateRate(data.getMiddleCount(), data.getGradeCount()));
data.setHighRate(calculateRate(data.getHighCount(), data.getGradeCount()));
// 出单率
data.setParentOrderRate(calculateRate(data.getParentDailyOrderCount(), data.getParentDailyCount()));
data.setStudentOrderRate(calculateRate(data.getStudentDailyOrderCount(), data.getStudentDailyCount()));
data.setTeacherOrderRate(calculateRate(data.getTeacherDailyOrderCount(), data.getTeacherDailyCount()));
data.setUnknownOrderRate(calculateRate(data.getUnknownDailyOrderCount(), data.getUnknownDailyCount()));
// 成本指标基于聚合后的总成本重新计算
// 初始化成本为0
data.setSingleCost(BigDecimal.ZERO);
data.setOrderCost(BigDecimal.ZERO);
// 确保totalCost不为null
if (data.getTotalCost() == null) {
data.setTotalCost(BigDecimal.ZERO);
}
if (data.getTotalCost().compareTo(BigDecimal.ZERO) > 0) {
// 单条成本 = 总成本 / 进粉数
if (data.getCustomerCount() != null && data.getCustomerCount() > 0) {
BigDecimal singleCost = data.getTotalCost().divide(
new BigDecimal(data.getCustomerCount()), 2, RoundingMode.HALF_UP);
data.setSingleCost(singleCost);
}
// 成单成本 = 总成本 / 成单数
if (data.getOrderCount() != null && data.getOrderCount() > 0) {
BigDecimal orderCost = data.getTotalCost().divide(
new BigDecimal(data.getOrderCount()), 2, RoundingMode.HALF_UP);
data.setOrderCost(orderCost);
}
}
}
/**
* 计算比率返回百分比字符串
*/
private String calculateRate(Integer count, Integer total) {
if (total == null || total == 0 || count == null) {
return "0%";
}
BigDecimal rate = new BigDecimal(count)
.multiply(new BigDecimal(100))
.divide(new BigDecimal(total), 2, RoundingMode.HALF_UP);
return rate.toString() + "%";
}
/**
* 计算周范围修复跨年问题
* 规则
* 1. 周的第一天是周一不是周日
* 2. 第一周从1月1日开始到1月1日所在周的周日结束
* - 如果1月1日是周日第一周只有1天1月1日当天
* 3. 最后一周从最后一周的周一12月31日往前推开始到12月31日结束
* - 如果12月31日是周一最后一周只有1天12月31日当天
* 4. 其他周按正常的周一到周日计算
*/
private Date[] calculateWeekRangeByMySQL(int year, int week) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, Calendar.JANUARY);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// 获取1月1日是星期几1=周日2=周一...7=周六
int jan1DayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
// 转换为0=周日1=周一...6=周六
int jan1Weekday = (jan1DayOfWeek == Calendar.SUNDAY) ? 0 : (jan1DayOfWeek - 1);
// 计算12月31日
Calendar dec31Cal = Calendar.getInstance();
dec31Cal.set(Calendar.YEAR, year);
dec31Cal.set(Calendar.MONTH, Calendar.DECEMBER);
dec31Cal.set(Calendar.DAY_OF_MONTH, 31);
dec31Cal.set(Calendar.HOUR_OF_DAY, 0);
dec31Cal.set(Calendar.MINUTE, 0);
dec31Cal.set(Calendar.SECOND, 0);
dec31Cal.set(Calendar.MILLISECOND, 0);
// 第一周从1月1日开始
if (week == 1) {
Date startDate = calendar.getTime();
// 计算第一周的结束日期周日
// 如果1月1日是周日(0)则第一周只有1天当天
// 否则计算到本周日
int daysToSunday;
if (jan1Weekday == 0) {
daysToSunday = 0; // 1月1日是周日第一周只有1天
} else {
daysToSunday = 7 - jan1Weekday; // 到本周日
}
calendar.add(Calendar.DAY_OF_MONTH, daysToSunday);
Date endDate = calendar.getTime();
return new Date[]{startDate, endDate};
}
// 计算第一周结束日期1月1日所在周的周日
Calendar firstWeekEndCal = (Calendar) calendar.clone();
int firstWeekDays;
if (jan1Weekday == 0) {
firstWeekDays = 0; // 1月1日是周日第一周只有1天
} else {
firstWeekDays = 7 - jan1Weekday; // 到本周日
}
firstWeekEndCal.add(Calendar.DAY_OF_MONTH, firstWeekDays);
// 计算第二周开始日期第一周结束后的周一
Calendar secondWeekStartCal = (Calendar) firstWeekEndCal.clone();
secondWeekStartCal.add(Calendar.DAY_OF_MONTH, 1);
// 计算目标周的开始和结束
// 从第二周开始每周都是周一到周日
Calendar targetWeekStartCal = (Calendar) secondWeekStartCal.clone();
targetWeekStartCal.add(Calendar.WEEK_OF_YEAR, week - 2);
// 如果开始日期已经跨年了大于12月31日则返回null
if (targetWeekStartCal.after(dec31Cal)) {
return null;
}
Date startDate = targetWeekStartCal.getTime();
Calendar targetWeekEndCal = (Calendar) targetWeekStartCal.clone();
targetWeekEndCal.add(Calendar.DAY_OF_MONTH, 6);
Date endDate = targetWeekEndCal.getTime();
// 如果目标周的结束超过了12月31日则调整到12月31日
if (targetWeekEndCal.after(dec31Cal)) {
endDate = dec31Cal.getTime();
}
return new Date[]{startDate, endDate};
}
/**
* 计算月的开始和结束日期
*/
private Date[] calculateMonthRange(int year, int month) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Date startDate = calendar.getTime();
calendar.add(Calendar.MONTH, 1);
calendar.add(Calendar.DAY_OF_MONTH, -1);
Date endDate = calendar.getTime();
return new Date[]{startDate, endDate};
}
/**
* 格式化日期为 yyyy-MM-dd 字符串
*/
private String formatDate(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
return String.format("%d-%02d-%02d", year, month, day);
}
}

View File

@ -139,7 +139,10 @@
<select id="selectByFinishDate" resultType="java.lang.Long"> <select id="selectByFinishDate" resultType="java.lang.Long">
select count(1) from customer_export_data select count(1) from customer_export_data
WHERE WHERE
corp_id = #{corpId} and finish_date = #{date} corp_id = #{corpId} and finish_date = #{date} and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="attr != null and attr != '' and attr == 'tag_group1'"> <if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 is not null AND tag_group1 is not null
</if> </if>
@ -196,6 +199,225 @@
</if> </if>
</select> </select>
<!-- 查询及时单数量(根据成交日期和订单状态) -->
<select id="selectTimelyOrderCount" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId} and finish_date = #{date}
and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 is not null
</if>
</select>
<!-- 查询非及时单数量(根据成交日期和订单状态) -->
<select id="selectNonTimelyOrderCount" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId} and finish_date = #{date}
and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 is not null
</if>
</select>
<!-- 查询成单数(根据成交日期、组字段和标签值) -->
<select id="selectByFinishDateAndTag" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId} and finish_date = #{date} and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 = #{tagValue}
</if>
</select>
<!-- 查询及时单数量(根据成交日期、订单状态和标签值) -->
<select id="selectTimelyOrderCountByTag" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId} and finish_date = #{date}
and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 = #{tagValue}
</if>
</select>
<!-- 查询非及时单数量(根据成交日期、订单状态和标签值) -->
<select id="selectNonTimelyOrderCountByTag" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId} and finish_date = #{date}
and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 = #{tagValue}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 = #{tagValue}
</if>
</select>
<!-- 分页查询客户导出数据VO列表(用于异步导出) --> <!-- 分页查询客户导出数据VO列表(用于异步导出) -->
<select id="selectCustomerExportDataVOListByPage" resultType="com.ruoyi.excel.wecom.vo.CustomerExportDataVO"> <select id="selectCustomerExportDataVOListByPage" resultType="com.ruoyi.excel.wecom.vo.CustomerExportDataVO">
SELECT SELECT
@ -248,4 +470,236 @@
LIMIT #{limit} OFFSET #{offset} LIMIT #{limit} OFFSET #{offset}
</select> </select>
<!-- 按日期范围查询成单数根据finish_date -->
<select id="selectOrderCountByFinishDateRange" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId} and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="startDate != null">
AND finish_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND finish_date &lt;= #{endDate}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 is not null
</if>
</select>
<!-- 按日期范围查询及时单数量根据finish_date -->
<select id="selectTimelyOrderCountByDateRange" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId}
and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="startDate != null">
AND finish_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND finish_date &lt;= #{endDate}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 is not null
</if>
</select>
<!-- 按日期范围查询非及时单数量根据finish_date -->
<select id="selectNonTimelyOrderCountByDateRange" resultType="java.lang.Long">
select count(1) from customer_export_data
WHERE
corp_id = #{corpId}
and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="startDate != null">
AND finish_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND finish_date &lt;= #{endDate}
</if>
<if test="attr != null and attr != '' and attr == 'tag_group1'">
AND tag_group1 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group2'">
AND tag_group2 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group3'">
AND tag_group3 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group10'">
AND tag_group10 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group11'">
AND tag_group11 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group12'">
AND tag_group12 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group13'">
AND tag_group13 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group14'">
AND tag_group14 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group16'">
AND tag_group16 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group17'">
AND tag_group17 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group18'">
AND tag_group18 is not null
</if>
</select>
<!-- 批量查询所有组的成单数根据finish_date- 一次查询返回所有组的数据 -->
<select id="selectOrderCountBatchByFinishDateRange" resultType="java.util.Map">
SELECT
SUM(CASE WHEN tag_group1 IS NOT NULL THEN 1 ELSE 0 END) as tag_group1,
SUM(CASE WHEN tag_group2 IS NOT NULL THEN 1 ELSE 0 END) as tag_group2,
SUM(CASE WHEN tag_group3 IS NOT NULL THEN 1 ELSE 0 END) as tag_group3,
SUM(CASE WHEN tag_group10 IS NOT NULL THEN 1 ELSE 0 END) as tag_group10,
SUM(CASE WHEN tag_group11 IS NOT NULL THEN 1 ELSE 0 END) as tag_group11,
SUM(CASE WHEN tag_group12 IS NOT NULL THEN 1 ELSE 0 END) as tag_group12,
SUM(CASE WHEN tag_group13 IS NOT NULL THEN 1 ELSE 0 END) as tag_group13,
SUM(CASE WHEN tag_group14 IS NOT NULL THEN 1 ELSE 0 END) as tag_group14,
SUM(CASE WHEN tag_group16 IS NOT NULL THEN 1 ELSE 0 END) as tag_group16,
SUM(CASE WHEN tag_group17 IS NOT NULL THEN 1 ELSE 0 END) as tag_group17,
SUM(CASE WHEN tag_group18 IS NOT NULL THEN 1 ELSE 0 END) as tag_group18
FROM customer_export_data
WHERE corp_id = #{corpId} and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="startDate != null">
AND finish_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND finish_date &lt;= #{endDate}
</if>
</select>
<!-- 批量查询所有组的及时单数量根据finish_date -->
<select id="selectTimelyOrderCountBatchByDateRange" resultType="java.util.Map">
SELECT
SUM(CASE WHEN tag_group1 IS NOT NULL THEN 1 ELSE 0 END) as tag_group1,
SUM(CASE WHEN tag_group2 IS NOT NULL THEN 1 ELSE 0 END) as tag_group2,
SUM(CASE WHEN tag_group3 IS NOT NULL THEN 1 ELSE 0 END) as tag_group3,
SUM(CASE WHEN tag_group10 IS NOT NULL THEN 1 ELSE 0 END) as tag_group10,
SUM(CASE WHEN tag_group11 IS NOT NULL THEN 1 ELSE 0 END) as tag_group11,
SUM(CASE WHEN tag_group12 IS NOT NULL THEN 1 ELSE 0 END) as tag_group12,
SUM(CASE WHEN tag_group13 IS NOT NULL THEN 1 ELSE 0 END) as tag_group13,
SUM(CASE WHEN tag_group14 IS NOT NULL THEN 1 ELSE 0 END) as tag_group14,
SUM(CASE WHEN tag_group16 IS NOT NULL THEN 1 ELSE 0 END) as tag_group16,
SUM(CASE WHEN tag_group17 IS NOT NULL THEN 1 ELSE 0 END) as tag_group17,
SUM(CASE WHEN tag_group18 IS NOT NULL THEN 1 ELSE 0 END) as tag_group18
FROM customer_export_data
WHERE corp_id = #{corpId}
and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="startDate != null">
AND finish_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND finish_date &lt;= #{endDate}
</if>
</select>
<!-- 批量查询所有组的非及时单数量根据finish_date -->
<select id="selectNonTimelyOrderCountBatchByDateRange" resultType="java.util.Map">
SELECT
SUM(CASE WHEN tag_group1 IS NOT NULL THEN 1 ELSE 0 END) as tag_group1,
SUM(CASE WHEN tag_group2 IS NOT NULL THEN 1 ELSE 0 END) as tag_group2,
SUM(CASE WHEN tag_group3 IS NOT NULL THEN 1 ELSE 0 END) as tag_group3,
SUM(CASE WHEN tag_group10 IS NOT NULL THEN 1 ELSE 0 END) as tag_group10,
SUM(CASE WHEN tag_group11 IS NOT NULL THEN 1 ELSE 0 END) as tag_group11,
SUM(CASE WHEN tag_group12 IS NOT NULL THEN 1 ELSE 0 END) as tag_group12,
SUM(CASE WHEN tag_group13 IS NOT NULL THEN 1 ELSE 0 END) as tag_group13,
SUM(CASE WHEN tag_group14 IS NOT NULL THEN 1 ELSE 0 END) as tag_group14,
SUM(CASE WHEN tag_group16 IS NOT NULL THEN 1 ELSE 0 END) as tag_group16,
SUM(CASE WHEN tag_group17 IS NOT NULL THEN 1 ELSE 0 END) as tag_group17,
SUM(CASE WHEN tag_group18 IS NOT NULL THEN 1 ELSE 0 END) as tag_group18
FROM customer_export_data
WHERE corp_id = #{corpId}
and tag_group7 in
<foreach collection="successFlags" item="successFlag" open="(" separator="," close=")">
#{successFlag}
</foreach>
<if test="startDate != null">
AND finish_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND finish_date &lt;= #{endDate}
</if>
</select>
</mapper> </mapper>

View File

@ -61,6 +61,20 @@
ORDER BY cur_date, sort_no ORDER BY cur_date, sort_no
</select> </select>
<!-- 按日期范围查询周数据(修复跨年周问题) -->
<select id="selectDailyDataByWeekRange" resultType="com.ruoyi.excel.wecom.domain.CustomerStatisticsData">
SELECT * FROM customer_statistics_data
<where>
corp_id = #{corpId}
AND cur_date &gt;= #{startDate}
AND cur_date &lt;= #{endDate}
<if test="indicatorName != null and indicatorName != ''">
AND indicator_name LIKE CONCAT('%', #{indicatorName}, '%')
</if>
</where>
ORDER BY cur_date, sort_no
</select>
<select id="selectDailyDataByMonth" resultType="com.ruoyi.excel.wecom.domain.CustomerStatisticsData"> <select id="selectDailyDataByMonth" resultType="com.ruoyi.excel.wecom.domain.CustomerStatisticsData">
SELECT * FROM customer_statistics_data SELECT * FROM customer_statistics_data
<where> <where>

View File

@ -0,0 +1,416 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.excel.wecom.mapper.CustomerStatisticsDataV2Mapper">
<resultMap id="BaseResultMap" type="com.ruoyi.excel.wecom.domain.CustomerStatisticsDataV2">
<id column="id" property="id"/>
<result column="corp_id" property="corpId"/>
<result column="cur_date" property="curDate"/>
<result column="group_name" property="groupName"/>
<result column="tag_name" property="tagName"/>
<result column="tag_group_id" property="tagGroupId"/>
<result column="tag_id" property="tagId"/>
<result column="data_level" property="dataLevel"/>
<result column="parent_id" property="parentId"/>
<result column="total_cost" property="totalCost"/>
<result column="single_cost" property="singleCost"/>
<result column="order_cost" property="orderCost"/>
<result column="cost_input_type" property="costInputType"/>
<result column="order_count" property="orderCount"/>
<result column="customer_count" property="customerCount"/>
<result column="timely_order_count" property="timelyOrderCount"/>
<result column="non_timely_order_count" property="nonTimelyOrderCount"/>
<result column="conversion_rate" property="conversionRate"/>
<result column="timely_rate" property="timelyRate"/>
<result column="non_timely_rate" property="nonTimelyRate"/>
<result column="customer_attr_count" property="customerAttrCount"/>
<result column="parent_count" property="parentCount"/>
<result column="student_count" property="studentCount"/>
<result column="teacher_count" property="teacherCount"/>
<result column="unknown_attr_count" property="unknownAttrCount"/>
<result column="parent_rate" property="parentRate"/>
<result column="student_rate" property="studentRate"/>
<result column="teacher_rate" property="teacherRate"/>
<result column="unknown_rate" property="unknownRate"/>
<result column="parent_order_count" property="parentOrderCount"/>
<result column="student_order_count" property="studentOrderCount"/>
<result column="teacher_order_count" property="teacherOrderCount"/>
<result column="unknown_order_count" property="unknownOrderCount"/>
<result column="parent_daily_count" property="parentDailyCount"/>
<result column="student_daily_count" property="studentDailyCount"/>
<result column="teacher_daily_count" property="teacherDailyCount"/>
<result column="unknown_daily_count" property="unknownDailyCount"/>
<result column="parent_daily_order_count" property="parentDailyOrderCount"/>
<result column="student_daily_order_count" property="studentDailyOrderCount"/>
<result column="teacher_daily_order_count" property="teacherDailyOrderCount"/>
<result column="unknown_daily_order_count" property="unknownDailyOrderCount"/>
<result column="parent_order_rate" property="parentOrderRate"/>
<result column="student_order_rate" property="studentOrderRate"/>
<result column="teacher_order_rate" property="teacherOrderRate"/>
<result column="unknown_order_rate" property="unknownOrderRate"/>
<result column="intention_count" property="intentionCount"/>
<result column="active_quote_count" property="activeQuoteCount"/>
<result column="passive_quote_count" property="passiveQuoteCount"/>
<result column="no_quote_count" property="noQuoteCount"/>
<result column="deleted_quote_count" property="deletedQuoteCount"/>
<result column="active_quote_rate" property="activeQuoteRate"/>
<result column="passive_quote_rate" property="passiveQuoteRate"/>
<result column="no_quote_rate" property="noQuoteRate"/>
<result column="deleted_quote_rate" property="deletedQuoteRate"/>
<result column="grade_count" property="gradeCount"/>
<result column="primary_count" property="primaryCount"/>
<result column="middle_count" property="middleCount"/>
<result column="high_count" property="highCount"/>
<result column="primary_rate" property="primaryRate"/>
<result column="middle_rate" property="middleRate"/>
<result column="high_rate" property="highRate"/>
<result column="sort_no" property="sortNo"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
id, corp_id, cur_date, group_name, tag_name, tag_group_id, tag_id, data_level, parent_id,
total_cost, single_cost, order_cost, cost_input_type,
order_count, customer_count, timely_order_count, non_timely_order_count,
conversion_rate, timely_rate, non_timely_rate,
customer_attr_count, parent_count, student_count, teacher_count, unknown_attr_count,
parent_rate, student_rate, teacher_rate, unknown_rate,
parent_order_count, student_order_count, teacher_order_count, unknown_order_count,
parent_daily_count, student_daily_count, teacher_daily_count, unknown_daily_count,
parent_daily_order_count, student_daily_order_count, teacher_daily_order_count, unknown_daily_order_count,
parent_order_rate, student_order_rate, teacher_order_rate, unknown_order_rate,
intention_count, active_quote_count, passive_quote_count, no_quote_count, deleted_quote_count,
active_quote_rate, passive_quote_rate, no_quote_rate, deleted_quote_rate,
grade_count, primary_count, middle_count, high_count,
primary_rate, middle_rate, high_rate,
sort_no, create_time, update_time
</sql>
<!-- 根据企业ID、日期、组名、标签名查询数据 -->
<select id="selectByCorpDateGroupTag" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
AND cur_date = #{curDate}
AND group_name = #{groupName}
AND (
(tag_name = #{tagName} AND #{tagName} IS NOT NULL AND #{tagName} != '')
OR (tag_name IS NULL AND (#{tagName} IS NULL OR #{tagName} = ''))
)
</select>
<!-- 查询组级数据列表 -->
<select id="selectGroupLevelList" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
AND data_level = 1
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND cur_date &lt;= #{endDate}
</if>
ORDER BY cur_date DESC, sort_no, group_name
</select>
<!-- 查询标签级数据列表 -->
<select id="selectTagLevelList" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
AND data_level = 2
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND cur_date &lt;= #{endDate}
</if>
<if test="groupName != null and groupName != ''">
AND group_name = #{groupName}
</if>
ORDER BY cur_date DESC, group_name, tag_name
</select>
<!-- 根据企业ID、日期、组名查询组级数据 -->
<select id="selectGroupLevelByCorpDateGroup" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
AND cur_date = #{curDate}
AND group_name = #{groupName}
AND data_level = 1
</select>
<!-- 根据企业ID、日期、组名查询标签级数据列表 -->
<select id="selectTagLevelByCorpDateGroup" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
AND cur_date = #{curDate}
AND group_name = #{groupName}
AND data_level = 2
ORDER BY tag_name
</select>
<!-- 批量插入数据 -->
<insert id="batchInsert">
INSERT INTO customer_statistics_data_v2 (
corp_id, cur_date, group_name, tag_name, tag_group_id, tag_id, data_level, parent_id,
total_cost, single_cost, order_cost, cost_input_type,
order_count, customer_count, timely_order_count, non_timely_order_count,
conversion_rate, timely_rate, non_timely_rate,
customer_attr_count, parent_count, student_count, teacher_count, unknown_attr_count,
parent_rate, student_rate, teacher_rate, unknown_rate,
parent_order_count, student_order_count, teacher_order_count, unknown_order_count,
parent_daily_count, student_daily_count, teacher_daily_count, unknown_daily_count,
parent_daily_order_count, student_daily_order_count, teacher_daily_order_count, unknown_daily_order_count,
parent_order_rate, student_order_rate, teacher_order_rate, unknown_order_rate,
intention_count, active_quote_count, passive_quote_count, no_quote_count, deleted_quote_count,
active_quote_rate, passive_quote_rate, no_quote_rate, deleted_quote_rate,
grade_count, primary_count, middle_count, high_count,
primary_rate, middle_rate, high_rate,
sort_no
) VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.corpId}, #{item.curDate}, #{item.groupName}, #{item.tagName}, #{item.tagGroupId}, #{item.tagId},
#{item.dataLevel}, #{item.parentId},
#{item.totalCost}, #{item.singleCost}, #{item.orderCost}, #{item.costInputType},
#{item.orderCount}, #{item.customerCount}, #{item.timelyOrderCount}, #{item.nonTimelyOrderCount},
#{item.conversionRate}, #{item.timelyRate}, #{item.nonTimelyRate},
#{item.customerAttrCount}, #{item.parentCount}, #{item.studentCount}, #{item.teacherCount}, #{item.unknownAttrCount},
#{item.parentRate}, #{item.studentRate}, #{item.teacherRate}, #{item.unknownRate},
#{item.parentOrderCount}, #{item.studentOrderCount}, #{item.teacherOrderCount}, #{item.unknownOrderCount},
#{item.parentDailyCount}, #{item.studentDailyCount}, #{item.teacherDailyCount}, #{item.unknownDailyCount},
#{item.parentDailyOrderCount}, #{item.studentDailyOrderCount}, #{item.teacherDailyOrderCount}, #{item.unknownDailyOrderCount},
#{item.parentOrderRate}, #{item.studentOrderRate}, #{item.teacherOrderRate}, #{item.unknownOrderRate},
#{item.intentionCount}, #{item.activeQuoteCount}, #{item.passiveQuoteCount}, #{item.noQuoteCount}, #{item.deletedQuoteCount},
#{item.activeQuoteRate}, #{item.passiveQuoteRate}, #{item.noQuoteRate}, #{item.deletedQuoteRate},
#{item.gradeCount}, #{item.primaryCount}, #{item.middleCount}, #{item.highCount},
#{item.primaryRate}, #{item.middleRate}, #{item.highRate},
#{item.sortNo}
)
</foreach>
</insert>
<!-- 删除指定日期范围的数据 -->
<delete id="deleteByDateRange">
DELETE FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND cur_date &lt;= #{endDate}
</if>
</delete>
<!-- 查询树状结构数据(组+标签) -->
<select id="selectTreeData" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND cur_date &lt;= #{endDate}
</if>
ORDER BY data_level, group_name, tag_name
</select>
<!-- 根据筛选条件查询数据列表(支持按组、标签筛选) -->
<select id="selectListByFilter" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND cur_date &lt;= #{endDate}
</if>
<if test="groupName != null and groupName != ''">
AND group_name = #{groupName}
</if>
<if test="tagName != null and tagName != ''">
AND tag_name = #{tagName}
</if>
ORDER BY cur_date DESC, data_level ASC, group_name ASC, sort_no ASC
</select>
<!-- 按日期范围聚合查询(支持按组、标签筛选) -->
<!-- 注意比率指标需要在Java层重新计算SQL只聚合数量指标 -->
<select id="selectAggregatedByDateRange" resultMap="BaseResultMap">
SELECT
NULL as id,
#{corpId} as corp_id,
MAX(cur_date) as cur_date,
group_name,
tag_name,
NULL as parent_id,
MIN(sort_no) as sort_no,
data_level,
SUM(order_count) as order_count,
SUM(customer_count) as customer_count,
SUM(customer_attr_count) as customer_attr_count,
SUM(parent_count) as parent_count,
SUM(student_count) as student_count,
SUM(teacher_count) as teacher_count,
SUM(unknown_attr_count) as unknown_attr_count,
SUM(timely_order_count) as timely_order_count,
SUM(non_timely_order_count) as non_timely_order_count,
NULL as conversion_rate,
NULL as timely_rate,
NULL as non_timely_rate,
NULL as parent_rate,
NULL as student_rate,
NULL as teacher_rate,
NULL as unknown_rate,
SUM(total_cost) as total_cost,
NULL as single_cost,
NULL as order_cost,
SUM(intention_count) as intention_count,
SUM(active_quote_count) as active_quote_count,
SUM(passive_quote_count) as passive_quote_count,
SUM(no_quote_count) as no_quote_count,
SUM(deleted_quote_count) as deleted_quote_count,
NULL as active_quote_rate,
NULL as passive_quote_rate,
NULL as no_quote_rate,
NULL as deleted_quote_rate,
SUM(grade_count) as grade_count,
SUM(primary_count) as primary_count,
SUM(middle_count) as middle_count,
SUM(high_count) as high_count,
NULL as primary_rate,
NULL as middle_rate,
NULL as high_rate,
SUM(parent_order_count) as parent_order_count,
SUM(student_order_count) as student_order_count,
SUM(teacher_order_count) as teacher_order_count,
SUM(unknown_order_count) as unknown_order_count,
SUM(parent_daily_count) as parent_daily_count,
SUM(student_daily_count) as student_daily_count,
SUM(teacher_daily_count) as teacher_daily_count,
SUM(unknown_daily_count) as unknown_daily_count,
SUM(parent_daily_order_count) as parent_daily_order_count,
SUM(student_daily_order_count) as student_daily_order_count,
SUM(teacher_daily_order_count) as teacher_daily_order_count,
SUM(unknown_daily_order_count) as unknown_daily_order_count,
NULL as parent_order_rate,
NULL as student_order_rate,
NULL as teacher_order_rate,
NULL as unknown_order_rate
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
<choose>
<when test="tagName != null and tagName != ''">
AND data_level = 2
</when>
<otherwise>
AND data_level = 1
</otherwise>
</choose>
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND cur_date &lt;= #{endDate}
</if>
<if test="groupName != null and groupName != ''">
AND group_name = #{groupName}
</if>
<if test="tagName != null and tagName != ''">
AND tag_name = #{tagName}
</if>
GROUP BY group_name, tag_name, data_level
ORDER BY group_name ASC, tag_name ASC, data_level ASC, MIN(sort_no) ASC
</select>
<!-- 查询所有数据并聚合(支持按组、标签筛选) -->
<!-- 注意比率指标需要在Java层重新计算SQL只聚合数量指标 -->
<select id="selectAllAggregated" resultMap="BaseResultMap">
SELECT
NULL as id,
#{corpId} as corp_id,
MAX(cur_date) as cur_date,
group_name,
tag_name,
NULL as parent_id,
MIN(sort_no) as sort_no,
data_level,
SUM(order_count) as order_count,
SUM(customer_count) as customer_count,
SUM(customer_attr_count) as customer_attr_count,
SUM(parent_count) as parent_count,
SUM(student_count) as student_count,
SUM(teacher_count) as teacher_count,
SUM(unknown_attr_count) as unknown_attr_count,
SUM(timely_order_count) as timely_order_count,
SUM(non_timely_order_count) as non_timely_order_count,
NULL as conversion_rate,
NULL as timely_rate,
NULL as non_timely_rate,
NULL as parent_rate,
NULL as student_rate,
NULL as teacher_rate,
NULL as unknown_rate,
SUM(total_cost) as total_cost,
NULL as single_cost,
NULL as order_cost,
SUM(intention_count) as intention_count,
SUM(active_quote_count) as active_quote_count,
SUM(passive_quote_count) as passive_quote_count,
SUM(no_quote_count) as no_quote_count,
SUM(deleted_quote_count) as deleted_quote_count,
NULL as active_quote_rate,
NULL as passive_quote_rate,
NULL as no_quote_rate,
NULL as deleted_quote_rate,
SUM(grade_count) as grade_count,
SUM(primary_count) as primary_count,
SUM(middle_count) as middle_count,
SUM(high_count) as high_count,
NULL as primary_rate,
NULL as middle_rate,
NULL as high_rate,
SUM(parent_order_count) as parent_order_count,
SUM(student_order_count) as student_order_count,
SUM(teacher_order_count) as teacher_order_count,
SUM(unknown_order_count) as unknown_order_count,
SUM(parent_daily_count) as parent_daily_count,
SUM(student_daily_count) as student_daily_count,
SUM(teacher_daily_count) as teacher_daily_count,
SUM(unknown_daily_count) as unknown_daily_count,
SUM(parent_daily_order_count) as parent_daily_order_count,
SUM(student_daily_order_count) as student_daily_order_count,
SUM(teacher_daily_order_count) as teacher_daily_order_count,
SUM(unknown_daily_order_count) as unknown_daily_order_count,
NULL as parent_order_rate,
NULL as student_order_rate,
NULL as teacher_order_rate,
NULL as unknown_order_rate
FROM customer_statistics_data_v2
WHERE corp_id = #{corpId}
<choose>
<when test="tagName != null and tagName != ''">
AND data_level = 2
</when>
<otherwise>
AND data_level = 1
</otherwise>
</choose>
<if test="groupName != null and groupName != ''">
AND group_name = #{groupName}
</if>
<if test="tagName != null and tagName != ''">
AND tag_name = #{tagName}
</if>
GROUP BY group_name, tag_name, data_level
ORDER BY group_name ASC, tag_name ASC, data_level ASC, MIN(sort_no) ASC
</select>
</mapper>

View File

@ -0,0 +1,114 @@
-- 客户统计数据表V2支持标签级成本行列转换存储
DROP TABLE IF EXISTS `customer_statistics_data_v2`;
CREATE TABLE `customer_statistics_data_v2` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`corp_id` VARCHAR(100) NOT NULL COMMENT '企业ID',
`cur_date` DATE NOT NULL COMMENT '统计日期',
-- 维度信息
`group_name` VARCHAR(50) NOT NULL COMMENT '组名N组、O组等',
`tag_name` VARCHAR(100) DEFAULT NULL COMMENT '标签名NULL表示组级汇总',
`tag_group_id` VARCHAR(100) DEFAULT NULL COMMENT '标签组ID关联wecom_tag_group',
`tag_id` VARCHAR(100) DEFAULT NULL COMMENT '标签ID关联wecom_tag',
-- 层级关系
`data_level` TINYINT DEFAULT 1 COMMENT '数据级别1-组级汇总2-标签级明细',
`parent_id` BIGINT(20) DEFAULT NULL COMMENT '父记录ID标签级数据对应组级记录的ID',
-- 成本数据(支持标签级成本)
`total_cost` DECIMAL(12,2) DEFAULT NULL COMMENT '总成本(手工录入)',
`single_cost` DECIMAL(12,2) DEFAULT NULL COMMENT '单条成本(计算得出)',
`order_cost` DECIMAL(12,2) DEFAULT NULL COMMENT '成单成本(计算得出)',
`cost_input_type` VARCHAR(20) DEFAULT NULL COMMENT '成本录入类型total-总成本single-单条成本',
-- 数量指标
`order_count` INT DEFAULT 0 COMMENT '成单数',
`customer_count` INT DEFAULT 0 COMMENT '进粉数',
`timely_order_count` INT DEFAULT 0 COMMENT '及时单数',
`non_timely_order_count` INT DEFAULT 0 COMMENT '非及时单数',
-- 比率指标
`conversion_rate` VARCHAR(10) DEFAULT '0%' COMMENT '转化率',
`timely_rate` VARCHAR(10) DEFAULT '0%' COMMENT '及时单占比',
`non_timely_rate` VARCHAR(10) DEFAULT '0%' COMMENT '非及时单占比',
-- 客户属性指标
`customer_attr_count` INT DEFAULT 0 COMMENT '客户属性数量',
`parent_count` INT DEFAULT 0 COMMENT '家长数量',
`student_count` INT DEFAULT 0 COMMENT '学生数量',
`teacher_count` INT DEFAULT 0 COMMENT '老师数量',
`unknown_attr_count` INT DEFAULT 0 COMMENT '未知属性数量',
`parent_rate` VARCHAR(10) DEFAULT '0%' COMMENT '家长占比',
`student_rate` VARCHAR(10) DEFAULT '0%' COMMENT '学生占比',
`teacher_rate` VARCHAR(10) DEFAULT '0%' COMMENT '老师占比',
`unknown_rate` VARCHAR(10) DEFAULT '0%' COMMENT '未知占比',
-- 出单率指标
`parent_order_count` INT DEFAULT 0 COMMENT '家长出单数量',
`student_order_count` INT DEFAULT 0 COMMENT '学生出单数量',
`teacher_order_count` INT DEFAULT 0 COMMENT '老师出单数量',
`unknown_order_count` INT DEFAULT 0 COMMENT '未知出单数量',
`parent_daily_count` INT DEFAULT 0 COMMENT '家长当日数量',
`student_daily_count` INT DEFAULT 0 COMMENT '学生当日数量',
`teacher_daily_count` INT DEFAULT 0 COMMENT '老师当日数量',
`unknown_daily_count` INT DEFAULT 0 COMMENT '未知当日数量',
`parent_daily_order_count` INT DEFAULT 0 COMMENT '家长当日出单数量',
`student_daily_order_count` INT DEFAULT 0 COMMENT '学生当日出单数量',
`teacher_daily_order_count` INT DEFAULT 0 COMMENT '老师当日出单数量',
`unknown_daily_order_count` INT DEFAULT 0 COMMENT '未知当日出单数量',
`parent_order_rate` VARCHAR(10) DEFAULT '0%' COMMENT '家长出单率',
`student_order_rate` VARCHAR(10) DEFAULT '0%' COMMENT '学生出单率',
`teacher_order_rate` VARCHAR(10) DEFAULT '0%' COMMENT '老师出单率',
`unknown_order_rate` VARCHAR(10) DEFAULT '0%' COMMENT '未知出单率',
-- 意向度指标
`intention_count` INT DEFAULT 0 COMMENT '意向度数量',
`active_quote_count` INT DEFAULT 0 COMMENT '主动报价数量',
`passive_quote_count` INT DEFAULT 0 COMMENT '被动报价数量',
`no_quote_count` INT DEFAULT 0 COMMENT '未开口报价数量',
`deleted_quote_count` INT DEFAULT 0 COMMENT '已删除报价数量',
`active_quote_rate` VARCHAR(10) DEFAULT '0%' COMMENT '主动报价占比',
`passive_quote_rate` VARCHAR(10) DEFAULT '0%' COMMENT '被动报价占比',
`no_quote_rate` VARCHAR(10) DEFAULT '0%' COMMENT '未开口报价占比',
`deleted_quote_rate` VARCHAR(10) DEFAULT '0%' COMMENT '已删除报价占比',
-- 年级指标
`grade_count` INT DEFAULT 0 COMMENT '年级数量',
`primary_count` INT DEFAULT 0 COMMENT '小学数量',
`middle_count` INT DEFAULT 0 COMMENT '初中数量',
`high_count` INT DEFAULT 0 COMMENT '高中数量',
`primary_rate` VARCHAR(10) DEFAULT '0%' COMMENT '小学占比',
`middle_rate` VARCHAR(10) DEFAULT '0%' COMMENT '初中占比',
`high_rate` VARCHAR(10) DEFAULT '0%' COMMENT '高中占比',
`sort_no` INT DEFAULT 0 COMMENT '排序号',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_corp_date_group_tag` (`corp_id`, `cur_date`, `group_name`, `tag_name`),
INDEX `idx_corp_date` (`corp_id`, `cur_date`),
INDEX `idx_group_name` (`group_name`),
INDEX `idx_data_level` (`data_level`),
INDEX `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户统计数据表V2支持标签级成本行列转换';
-- 成本录入记录表(用于追溯)
DROP TABLE IF EXISTS `cost_input_record_v2`;
CREATE TABLE `cost_input_record_v2` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`corp_id` VARCHAR(100) NOT NULL COMMENT '企业ID',
`cur_date` DATE NOT NULL COMMENT '统计日期',
`group_name` VARCHAR(50) NOT NULL COMMENT '组名',
`tag_name` VARCHAR(100) DEFAULT NULL COMMENT '标签名NULL表示组级',
`cost_type` VARCHAR(20) NOT NULL COMMENT 'total-总成本single-单条成本',
`input_value` DECIMAL(12,2) NOT NULL COMMENT '录入值',
`actual_total_cost` DECIMAL(12,2) NOT NULL COMMENT '实际总成本',
`input_by` VARCHAR(50) DEFAULT NULL COMMENT '录入人',
`input_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
INDEX `idx_corp_date_group` (`corp_id`, `cur_date`, `group_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成本录入记录表V2';

View File

@ -0,0 +1,195 @@
package com.ruoyi.web.controller.wocom;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.CorpContextHolder;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsDataV2;
import com.ruoyi.excel.wecom.domain.dto.TagTreeDTO;
import com.ruoyi.excel.wecom.service.ICustomerStatisticsDataV2Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 客户统计数据V2 Controller
* 支持标签级成本行列转换存储
*/
@RestController
@RequestMapping("/wecom/customerStatisticsV2")
public class CustomerStatisticsDataV2Controller extends BaseController {
private static final Logger log = LoggerFactory.getLogger(CustomerStatisticsDataV2Controller.class);
@Autowired
private ICustomerStatisticsDataV2Service customerStatisticsDataV2Service;
/**
* 查询客户统计数据V2列表支持按组标签筛选支持天//月维度
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:list')")
@GetMapping("/list")
public TableDataInfo list(
@RequestParam(value = "dataType", required = false) String dataType,
@RequestParam(value = "year", required = false) Integer year,
@RequestParam(value = "week", required = false) Integer week,
@RequestParam(value = "yearMonth", required = false) String yearMonth,
@RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(value = "groupName", required = false) String groupName,
@RequestParam(value = "tagName", required = false) String tagName) {
String corpId = CorpContextHolder.getCurrentCorpId();
log.info("list接口被调用: dataType={}, year={}, week={}, yearMonth={}, groupName={}, tagName={}",
dataType, year, week, yearMonth, groupName, tagName);
List<CustomerStatisticsDataV2> list;
if ("week".equals(dataType)) {
list = customerStatisticsDataV2Service.selectByWeekAggregation(corpId, year, week, groupName, tagName);
log.info("周聚合查询结果: {}条记录", list.size());
return getDataTable(list);
} else if ("month".equals(dataType)) {
list = customerStatisticsDataV2Service.selectByMonthAggregation(corpId, yearMonth, groupName, tagName);
log.info("月聚合查询结果: {}条记录", list.size());
return getDataTable(list);
} else if ("all".equals(dataType)) {
list = customerStatisticsDataV2Service.selectAllAggregation(corpId, groupName, tagName);
log.info("全部数据查询结果: {}条记录", list.size());
return getDataTable(list);
} else {
startPage();
list = customerStatisticsDataV2Service
.selectCustomerStatisticsDataV2List(corpId, startDate, endDate, groupName, tagName);
return getDataTable(list);
}
}
/**
* 查询标签树只返回组-标签结构不返回统计数据
* 用于前端左侧树状筛选面板
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:tree')")
@GetMapping("/tree")
public AjaxResult tree(
@RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
String corpId = CorpContextHolder.getCurrentCorpId();
List<TagTreeDTO> treeData = customerStatisticsDataV2Service
.selectTagTree(corpId, startDate, endDate);
return success(treeData);
}
/**
* 导出客户统计数据V2列表
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:export')")
@Log(title = "客户统计数据V2", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response,
@RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
String corpId = CorpContextHolder.getCurrentCorpId();
List<CustomerStatisticsDataV2> dataList = customerStatisticsDataV2Service
.selectCustomerStatisticsDataV2List(corpId, startDate, endDate);
ExcelUtil<CustomerStatisticsDataV2> util = new ExcelUtil<>(CustomerStatisticsDataV2.class);
util.exportExcel(response, dataList, "流量看板数据V2");
}
/**
* 获取客户统计数据V2详细信息
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return success(customerStatisticsDataV2Service.selectCustomerStatisticsDataV2ById(id));
}
/**
* 新增客户统计数据V2
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:add')")
@Log(title = "客户统计数据V2", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody CustomerStatisticsDataV2 data) {
return toAjax(customerStatisticsDataV2Service.insertCustomerStatisticsDataV2(data));
}
/**
* 修改客户统计数据V2
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:edit')")
@Log(title = "客户统计数据V2", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody CustomerStatisticsDataV2 data) {
return toAjax(customerStatisticsDataV2Service.updateCustomerStatisticsDataV2(data));
}
/**
* 删除客户统计数据V2
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:remove')")
@Log(title = "客户统计数据V2", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(customerStatisticsDataV2Service.deleteCustomerStatisticsDataV2ByIds(ids));
}
/**
* 录入成本支持组级和标签级
* @param date 日期
* @param groupName 组名
* @param tagName 标签名为空表示组级
* @param costValue 成本值
* @param inputType 录入类型total-总成本single-单条成本
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:cost')")
@Log(title = "客户统计数据V2-成本录入", businessType = BusinessType.UPDATE)
@PostMapping("/cost")
public AjaxResult inputCost(
@RequestParam(value = "date") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date,
@RequestParam(value = "groupName") String groupName,
@RequestParam(value = "tagName", required = false) String tagName,
@RequestParam(value = "costValue") BigDecimal costValue,
@RequestParam(value = "inputType") String inputType) {
String corpId = CorpContextHolder.getCurrentCorpId();
return toAjax(customerStatisticsDataV2Service.inputCost(
corpId, date, groupName, tagName, costValue, inputType));
}
/**
* 重新计算指定日期的统计数据
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:recalculate')")
@Log(title = "客户统计数据V2-重新计算", businessType = BusinessType.UPDATE)
@PostMapping("/recalculate")
public AjaxResult recalculate(
@RequestParam(value = "date") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {
String corpId = CorpContextHolder.getCurrentCorpId();
return toAjax(customerStatisticsDataV2Service.recalculateStatistics(corpId, date));
}
/**
* 重新计算指定日期范围的统计数据
*/
@PreAuthorize("@ss.hasPermi('wecom:customerStatisticsV2:recalculate')")
@Log(title = "客户统计数据V2-重新计算", businessType = BusinessType.UPDATE)
@PostMapping("/recalculateRange")
public AjaxResult recalculateRange(
@RequestParam(value = "startDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(value = "endDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
String corpId = CorpContextHolder.getCurrentCorpId();
return toAjax(customerStatisticsDataV2Service.recalculateStatisticsRange(
corpId, startDate, endDate));
}
}

View File

@ -3,6 +3,7 @@ package com.ruoyi.quartz.task;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.ruoyi.excel.wecom.domain.CorpInfo; import com.ruoyi.excel.wecom.domain.CorpInfo;
import com.ruoyi.excel.wecom.helper.HandleAllData; import com.ruoyi.excel.wecom.helper.HandleAllData;
import com.ruoyi.excel.wecom.helper.HandleAllDataV2;
import com.ruoyi.excel.wecom.mapper.CorpInfoMapper; import com.ruoyi.excel.wecom.mapper.CorpInfoMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -18,6 +19,8 @@ public class WeComTask {
@Autowired @Autowired
private HandleAllData handleAllData; private HandleAllData handleAllData;
@Autowired @Autowired
private HandleAllDataV2 handleAllDataV2;
@Autowired
private CorpInfoMapper corpInfoMapper; private CorpInfoMapper corpInfoMapper;
public void initData() throws IOException { public void initData() throws IOException {
System.out.println("初始化项目数据 包括 部门 人员"); System.out.println("初始化项目数据 包括 部门 人员");
@ -39,6 +42,11 @@ public class WeComTask {
handleAllData.createAllDepartmentReportData(); handleAllData.createAllDepartmentReportData();
} }
public void createAllDepartmentReportDataV2() throws IOException {
System.out.println("计算所有流量看板数据V2");
handleAllDataV2.createAllReportDataV2();
}
public void createCurDateCustomerReport() throws IOException { public void createCurDateCustomerReport() throws IOException {
Date from = Date.from(LocalDate.now().atStartOfDay() Date from = Date.from(LocalDate.now().atStartOfDay()
.atZone(ZoneId.systemDefault()).toInstant()); .atZone(ZoneId.systemDefault()).toInstant());
@ -59,4 +67,15 @@ public class WeComTask {
.atZone(ZoneId.systemDefault()).toInstant())); .atZone(ZoneId.systemDefault()).toInstant()));
}); });
} }
public void createCurDateCustomerReportV2() throws IOException {
System.out.println("计算所有流量看板数据V2");
Date from = Date.from(LocalDate.now().atStartOfDay()
.atZone(ZoneId.systemDefault()).toInstant());
System.out.println("计算" + JSON.toJSONString(from) + "流量看板数据");
List<CorpInfo> corpInfos = corpInfoMapper.selectCorpInfoList(new CorpInfo());
corpInfos.forEach(item->{
handleAllDataV2.createReportDataV2(item.getCorpId(), from);
});
}
} }

BIN
ruoyi-ui/dist.zip Normal file

Binary file not shown.

View File

@ -0,0 +1,93 @@
import request from '@/utils/request'
// 查询流量看板V2数据列表组级
export function listCustomerStatisticsV2(query) {
return request({
url: '/wecom/customerStatisticsV2/list',
method: 'get',
params: query
})
}
// 查询流量看板V2树状数据组+标签)
export function treeCustomerStatisticsV2(query) {
return request({
url: '/wecom/customerStatisticsV2/tree',
method: 'get',
params: query
})
}
// 查询流量看板V2数据详细
export function getCustomerStatisticsV2(id) {
return request({
url: '/wecom/customerStatisticsV2/' + id,
method: 'get'
})
}
// 新增流量看板V2数据
export function addCustomerStatisticsV2(data) {
return request({
url: '/wecom/customerStatisticsV2',
method: 'post',
data: data
})
}
// 修改流量看板V2数据
export function updateCustomerStatisticsV2(data) {
return request({
url: '/wecom/customerStatisticsV2',
method: 'put',
data: data
})
}
// 删除流量看板V2数据
export function delCustomerStatisticsV2(ids) {
return request({
url: '/wecom/customerStatisticsV2/' + ids,
method: 'delete'
})
}
// 导出流量看板V2数据
export function exportCustomerStatisticsV2(query) {
return request({
url: '/wecom/customerStatisticsV2/export',
method: 'post',
params: query,
responseType: 'blob'
})
}
// 成本录入(支持组级和标签级)
export function inputCost(data) {
return request({
url: '/wecom/customerStatisticsV2/cost',
method: 'post',
params: data
})
}
// 重新计算指定日期的统计数据
export function recalculateStatistics(date) {
return request({
url: '/wecom/customerStatisticsV2/recalculate',
method: 'post',
params: { date: date }
})
}
// 重新计算指定日期范围的统计数据
export function recalculateStatisticsRange(startDate, endDate) {
return request({
url: '/wecom/customerStatisticsV2/recalculateRange',
method: 'post',
params: {
startDate: startDate,
endDate: endDate
}
})
}

View File

@ -310,14 +310,85 @@ export default {
if (this.queryParams.year && this.queryParams.week) { if (this.queryParams.year && this.queryParams.week) {
const year = parseInt(this.queryParams.year) const year = parseInt(this.queryParams.year)
const week = this.queryParams.week const week = this.queryParams.week
const startDate = this.getWeekStartDate(year, week) const weekRange = this.calculateWeekRangeFixed(year, week)
const endDate = new Date(startDate) this.weekDateRange = weekRange.startDate + ' 至 ' + weekRange.endDate
endDate.setDate(endDate.getDate() + 6)
this.weekDateRange = this.formatDate(startDate) + ' 至 ' + this.formatDate(endDate)
} else { } else {
this.weekDateRange = '' this.weekDateRange = ''
} }
}, },
//
//
// 1.
// 2. 1111
// - 11111
// 3. 12311231
// - 123111231
// 4.
calculateWeekRangeFixed(year, week) {
const jan1 = new Date(year, 0, 1)
const jan1Weekday = jan1.getDay() // 0=1=...6=
const dec31 = new Date(year, 11, 31)
const dec31Weekday = dec31.getDay() // 0=1=...6=
// 11
if (week === 1) {
const startDate = this.formatDate(jan1)
//
// 11(0)1
//
let daysToSunday
if (jan1Weekday === 0) {
// 111
daysToSunday = 0
} else {
// 11
daysToSunday = 7 - jan1Weekday
}
const firstWeekEnd = new Date(jan1)
firstWeekEnd.setDate(jan1.getDate() + daysToSunday)
const endDate = this.formatDate(firstWeekEnd)
return { startDate, endDate }
}
//
let firstWeekDays
if (jan1Weekday === 0) {
firstWeekDays = 0 // 111
} else {
firstWeekDays = 7 - jan1Weekday //
}
const firstWeekEnd = new Date(jan1)
firstWeekEnd.setDate(jan1.getDate() + firstWeekDays)
//
const secondWeekStart = new Date(firstWeekEnd)
secondWeekStart.setDate(firstWeekEnd.getDate() + 1)
//
//
const targetWeekStart = new Date(secondWeekStart)
targetWeekStart.setDate(secondWeekStart.getDate() + (week - 2) * 7)
const targetWeekEnd = new Date(targetWeekStart)
targetWeekEnd.setDate(targetWeekStart.getDate() + 6)
// 12311231
let endDate = this.formatDate(targetWeekEnd)
if (targetWeekEnd > dec31) {
endDate = this.formatDate(dec31)
}
// 1231
if (targetWeekStart > dec31) {
return { startDate: '', endDate: '' }
}
return {
startDate: this.formatDate(targetWeekStart),
endDate: endDate
}
},
// -
getWeekStartDate(year, week) { getWeekStartDate(year, week) {
const date = new Date(year, 0, 1) const date = new Date(year, 0, 1)
const dayOfWeek = date.getDay() const dayOfWeek = date.getDay()
@ -371,12 +442,75 @@ export default {
}) })
}, },
handleDataTypeChange() { handleDataTypeChange() {
const dataType = this.queryParams.dataType
if (dataType === 'day' || !dataType) {
//
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
this.queryParams.startDate = `${year}-${month}-${day}`
this.queryParams.endDate = `${year}-${month}-${day}`
//
this.queryParams.year = undefined this.queryParams.year = undefined
this.queryParams.week = undefined this.queryParams.week = undefined
this.queryParams.yearMonth = undefined this.queryParams.yearMonth = undefined
this.weekDateRange = ''
} else if (dataType === 'week') {
//
const now = new Date()
// 使 el-date-picker value-format
this.queryParams.year = String(now.getFullYear())
//
const jan1 = new Date(now.getFullYear(), 0, 1)
const jan1Weekday = jan1.getDay() // 0=1=...6=
//
let firstWeekDays
if (jan1Weekday === 0) {
firstWeekDays = 0 // 111
} else {
firstWeekDays = 7 - jan1Weekday //
}
const firstWeekEnd = new Date(jan1)
firstWeekEnd.setDate(jan1.getDate() + firstWeekDays)
//
const secondWeekStart = new Date(firstWeekEnd)
secondWeekStart.setDate(firstWeekEnd.getDate() + 1)
//
const diffTime = now - secondWeekStart
const diffWeeks = Math.floor(diffTime / (7 * 24 * 60 * 60 * 1000))
this.queryParams.week = diffWeeks + 2 // 2
this.updateWeekDateRange()
//
this.queryParams.startDate = undefined this.queryParams.startDate = undefined
this.queryParams.endDate = undefined this.queryParams.endDate = undefined
this.queryParams.yearMonth = undefined
} else if (dataType === 'month') {
//
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
this.queryParams.yearMonth = `${year}-${month}`
//
this.queryParams.startDate = undefined
this.queryParams.endDate = undefined
this.queryParams.year = undefined
this.queryParams.week = undefined
this.weekDateRange = '' this.weekDateRange = ''
} else if (dataType === 'all') {
//
this.queryParams.startDate = undefined
this.queryParams.endDate = undefined
this.queryParams.year = undefined
this.queryParams.week = undefined
this.queryParams.yearMonth = undefined
this.weekDateRange = ''
}
}, },
handleQuery() { handleQuery() {
this.queryParams.pageNum = 1 this.queryParams.pageNum = 1

View File

@ -0,0 +1,907 @@
<template>
<div class="app-container">
<el-container>
<el-aside width="250px" class="tree-aside">
<div class="tree-header">
<span>/标签筛选</span>
<el-button type="text" size="mini" @click="resetTreeFilter">重置</el-button>
</div>
<el-tree
ref="filterTree"
:data="treeFilterData"
:props="treeProps"
node-key="id"
:default-expand-all="true"
:highlight-current="true"
@node-click="handleTreeNodeClick"
class="filter-tree"
>
<template slot-scope="{ node, data }">
<span class="tree-node">
<span :class="{'group-node': data.type === 'group', 'tag-node': data.type === 'tag'}">
{{ node.label }}
</span>
<span class="node-count" v-if="data.count !== undefined">({{ data.count }})</span>
</span>
</template>
</el-tree>
</el-aside>
<el-main>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="数据类型" prop="dataType">
<el-select
v-model="queryParams.dataType"
placeholder="请选择数据类型"
clearable
@change="handleDataTypeChange"
>
<el-option label="按天查询" value="day" />
<el-option label="自然周叠加数据" value="week" />
<el-option label="自然月叠加数据" value="month" />
<el-option label="所有数据" value="all" />
</el-select>
</el-form-item>
<el-form-item v-if="queryParams.dataType === 'week'" label="年份" prop="year">
<el-date-picker
v-model="queryParams.year"
type="year"
placeholder="选择年份"
value-format="yyyy"
style="width: 120px"
/>
</el-form-item>
<el-form-item v-if="queryParams.dataType === 'week'" label="周数" prop="week">
<el-select
v-model="queryParams.week"
placeholder="选择周数"
clearable
style="width: 120px"
>
<el-option v-for="i in 53" :key="i" :label="'第' + i + '周'" :value="i" />
</el-select>
<span v-if="weekDateRange" style="margin-left: 10px; color: #909399; font-size: 12px;">{{ weekDateRange }}</span>
</el-form-item>
<el-form-item v-if="queryParams.dataType === 'month'" label="年月" prop="yearMonth">
<el-date-picker
v-model="queryParams.yearMonth"
type="month"
placeholder="选择年月"
value-format="yyyy-MM"
/>
</el-form-item>
<el-form-item v-if="queryParams.dataType === 'day' || !queryParams.dataType" label="开始日期" prop="startDate">
<el-date-picker
v-model="queryParams.startDate"
type="date"
placeholder="选择开始日期"
value-format="yyyy-MM-dd"
clearable
/>
</el-form-item>
<el-form-item v-if="queryParams.dataType === 'day' || !queryParams.dataType" label="结束日期" prop="endDate">
<el-date-picker
v-model="queryParams.endDate"
type="date"
placeholder="选择结束日期"
value-format="yyyy-MM-dd"
clearable
/>
</el-form-item>
<el-form-item label="组" prop="groupName">
<el-input
v-model="queryParams.groupName"
placeholder="请输入组名"
clearable
size="small"
style="width: 120px"
/>
</el-form-item>
<el-form-item label="标签" prop="tagName">
<el-input
v-model="queryParams.tagName"
placeholder="请输入标签名"
clearable
size="small"
style="width: 120px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['wecom:customerStatisticsV2:export']"
>导出</el-button>
</el-col>
<!-- 重新计算按钮已隐藏
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-refresh"
size="mini"
@click="handleRecalculate"
v-hasPermi="['wecom:customerStatisticsV2:recalculate']"
>重新计算</el-button>
</el-col>
-->
<el-col :span="1.5">
<el-popover
placement="bottom"
width="400"
trigger="click"
>
<div class="column-config">
<div class="config-header">
<span>选择要显示的列</span>
<el-button type="text" size="mini" @click="selectAllColumns">全选</el-button>
<el-button type="text" size="mini" @click="unselectAllColumns">全不选</el-button>
<el-button type="text" size="mini" @click="resetColumns">重置</el-button>
</div>
<el-divider></el-divider>
<div class="config-body">
<div v-for="group in columnGroups" :key="group.label" class="config-group">
<div class="group-title">{{ group.label }}</div>
<el-checkbox-group v-model="selectedColumns" class="checkbox-group">
<el-checkbox
v-for="col in group.columns"
:key="col.prop"
:label="col.prop"
class="checkbox-item"
>{{ col.label }}</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
<el-button
slot="reference"
type="info"
plain
icon="el-icon-setting"
size="mini"
>列配置</el-button>
</el-popover>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
v-loading="loading"
:data="dataList"
row-key="id"
border
style="width: 100%"
>
<el-table-column label="组" align="left" prop="groupName" width="120" fixed>
<template slot-scope="scope">
<span :style="{fontWeight: scope.row.dataLevel === 1 ? 'bold' : 'normal', color: '#409EFF'}">
{{ scope.row.groupName }}
</span>
</template>
</el-table-column>
<el-table-column label="标签" align="left" prop="tagName" width="180" fixed>
<template slot-scope="scope">
<span v-if="scope.row.tagName" style="color: #606266; padding-left: 10px;">
{{ scope.row.tagName }}
</span>
<span v-else style="color: #909399; font-style: italic;">-</span>
</template>
</el-table-column>
<el-table-column label="日期" align="center" width="140" fixed>
<template slot-scope="scope">
<div v-if="queryParams.dataType === 'week'">
<div>{{ scope.row.yearWeek }}</div>
<div style="color: #909399; font-size: 12px;">{{ scope.row.dateRange }}</div>
</div>
<div v-else-if="queryParams.dataType === 'month'">
<div>{{ scope.row.yearMonth }}</div>
<div style="color: #909399; font-size: 12px;">{{ scope.row.dateRange }}</div>
</div>
<div v-else-if="queryParams.dataType === 'all'">
<span>全部数据</span>
</div>
<span v-else>{{ parseTime(scope.row.curDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<template v-for="col in allColumns">
<el-table-column
v-if="selectedColumns.includes(col.prop)"
:key="col.prop"
:label="col.label"
:align="col.align || 'center'"
:prop="col.prop"
:width="col.width"
>
<template slot-scope="scope">
<template v-if="col.editable && canEditCost(scope.row)">
<el-input-number
v-model="scope.row[col.prop]"
size="mini"
:controls="false"
:precision="col.precision || 2"
style="width: 100%"
@keyup.enter.native="handleInputCost(scope.row, scope.row[col.prop], col.costType || 'total')"
/>
</template>
<span v-else>{{ scope.row[col.prop] }}</span>
</template>
</el-table-column>
</template>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-main>
</el-container>
</div>
</template>
<script>
import { listCustomerStatisticsV2, treeCustomerStatisticsV2, exportCustomerStatisticsV2, inputCost, recalculateStatisticsRange } from "@/api/wecom/customerStatisticsV2"
const STORAGE_KEY = 'customerStatisticsV2_columns'
const DEFAULT_COLUMNS = [
'totalCost', 'singleCost', 'orderCost', 'orderCount', 'customerCount', 'conversionRate',
'timelyRate', 'parentRate', 'studentRate', 'teacherRate'
]
export default {
name: "CustomerStatisticsV2",
data() {
return {
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
dataList: [],
treeData: [], // /tree
weekDateRange: '', //
queryParams: {
pageNum: 1,
pageSize: 20,
dataType: 'day', //
startDate: undefined,
endDate: undefined,
year: new Date().getFullYear(),
week: undefined,
yearMonth: undefined,
groupName: undefined,
tagName: undefined
},
selectedColumns: [],
treeProps: {
children: 'children',
label: 'label'
},
columnGroups: [
{
label: '成本指标',
columns: [
{ prop: 'totalCost', label: '总成本', width: 120, editable: true, costType: 'total' },
{ prop: 'singleCost', label: '单条成本', width: 100, editable: true, costType: 'single' },
{ prop: 'orderCost', label: '成单成本', width: 100 }
]
},
{
label: '核心指标',
columns: [
{ prop: 'orderCount', label: '成单数', width: 80 },
{ prop: 'customerCount', label: '进粉数', width: 80 },
{ prop: 'conversionRate', label: '转化率', width: 80 }
]
},
{
label: '及时单指标',
columns: [
{ prop: 'timelyRate', label: '及时单占比', width: 100 },
{ prop: 'nonTimelyRate', label: '非及时单占比', width: 110 }
]
},
{
label: '客户属性',
columns: [
{ prop: 'customerAttrCount', label: '客户属性数量', width: 110 },
{ prop: 'parentRate', label: '家长占比', width: 80 },
{ prop: 'studentRate', label: '学生占比', width: 80 },
{ prop: 'teacherRate', label: '老师占比', width: 80 },
{ prop: 'unknownRate', label: '未知占比', width: 80 }
]
},
{
label: '意向度',
columns: [
{ prop: 'intentionCount', label: '意向度数量', width: 100 },
{ prop: 'activeQuoteRate', label: '主动报价占比', width: 110 },
{ prop: 'passiveQuoteRate', label: '被动报价占比', width: 110 },
{ prop: 'noQuoteRate', label: '未开口报价占比', width: 120 },
{ prop: 'deletedQuoteRate', label: '已删除报价占比', width: 120 }
]
},
{
label: '年级统计',
columns: [
{ prop: 'gradeCount', label: '年级数量', width: 80 },
{ prop: 'primaryRate', label: '小学占比', width: 80 },
{ prop: 'middleRate', label: '初中占比', width: 80 },
{ prop: 'highRate', label: '高中占比', width: 80 }
]
},
{
label: '出单率',
columns: [
{ prop: 'parentOrderRate', label: '家长出单率', width: 100 },
{ prop: 'studentOrderRate', label: '学生出单率', width: 100 },
{ prop: 'teacherOrderRate', label: '老师出单率', width: 100 },
{ prop: 'unknownOrderRate', label: '未知出单率', width: 100 }
]
}
]
}
},
computed: {
allColumns() {
const cols = []
this.columnGroups.forEach(group => {
group.columns.forEach(col => {
cols.push(col)
})
})
return cols
},
treeFilterData() {
// 使/tree
return this.treeData
}
},
created() {
this.loadColumnConfig()
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
this.queryParams.startDate = `${year}-${month}-${day}`
this.queryParams.endDate = `${year}-${month}-${day}`
//
this.$nextTick(() => {
this.getTreeData() //
this.getList() //
})
},
watch: {
//
'queryParams.startDate': function(newVal, oldVal) {
if (newVal !== oldVal && (this.queryParams.dataType === 'day' || !this.queryParams.dataType)) {
this.getTreeData()
}
},
'queryParams.endDate': function(newVal, oldVal) {
if (newVal !== oldVal && (this.queryParams.dataType === 'day' || !this.queryParams.dataType)) {
this.getTreeData()
}
},
//
'queryParams.year': function(newVal, oldVal) {
if (newVal !== oldVal && this.queryParams.dataType === 'week') {
this.updateWeekDateRange()
this.getTreeData()
}
},
'queryParams.week': function(newVal, oldVal) {
if (newVal !== oldVal && this.queryParams.dataType === 'week') {
this.updateWeekDateRange()
this.getTreeData()
}
},
//
'queryParams.yearMonth': function(newVal, oldVal) {
if (newVal !== oldVal && this.queryParams.dataType === 'month') {
this.getTreeData()
}
},
//
selectedColumns: {
handler() {
this.saveColumnConfig()
},
deep: true
}
},
methods: {
//
updateWeekDateRange() {
if (this.queryParams.year && this.queryParams.week) {
const year = parseInt(this.queryParams.year)
const week = this.queryParams.week
const weekRange = this.calculateWeekRangeFixed(year, week)
this.weekDateRange = weekRange.startDate + ' 至 ' + weekRange.endDate
} else {
this.weekDateRange = ''
}
},
//
//
// 1.
// 2. 1111
// - 11111
// 3. 12311231
// - 123111231
// 4.
calculateWeekRangeFixed(year, week) {
const jan1 = new Date(year, 0, 1)
const jan1Weekday = jan1.getDay() // 0=1=...6=
const dec31 = new Date(year, 11, 31)
const dec31Weekday = dec31.getDay() // 0=1=...6=
// 11
if (week === 1) {
const startDate = this.formatDate(jan1)
//
// 11(0)1
//
let daysToSunday
if (jan1Weekday === 0) {
// 111
daysToSunday = 0
} else {
// 11
daysToSunday = 7 - jan1Weekday
}
const firstWeekEnd = new Date(jan1)
firstWeekEnd.setDate(jan1.getDate() + daysToSunday)
const endDate = this.formatDate(firstWeekEnd)
return { startDate, endDate }
}
//
let firstWeekDays
if (jan1Weekday === 0) {
firstWeekDays = 0 // 111
} else {
firstWeekDays = 7 - jan1Weekday //
}
const firstWeekEnd = new Date(jan1)
firstWeekEnd.setDate(jan1.getDate() + firstWeekDays)
//
const secondWeekStart = new Date(firstWeekEnd)
secondWeekStart.setDate(firstWeekEnd.getDate() + 1)
//
//
const targetWeekStart = new Date(secondWeekStart)
targetWeekStart.setDate(secondWeekStart.getDate() + (week - 2) * 7)
const targetWeekEnd = new Date(targetWeekStart)
targetWeekEnd.setDate(targetWeekStart.getDate() + 6)
// 12311231
let endDate = this.formatDate(targetWeekEnd)
if (targetWeekEnd > dec31) {
endDate = this.formatDate(dec31)
}
// 1231
if (targetWeekStart > dec31) {
return { startDate: '', endDate: '' }
}
return {
startDate: this.formatDate(targetWeekStart),
endDate: endDate
}
},
// -
getWeekStartDate(year, week) {
const date = new Date(year, 0, 1)
const dayOfWeek = date.getDay()
const daysToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek
date.setDate(date.getDate() + daysToMonday)
date.setDate(date.getDate() + (week - 1) * 7)
return date
},
//
formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return year + '-' + month + '-' + day
},
loadColumnConfig() {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
try {
this.selectedColumns = JSON.parse(saved)
} catch (e) {
this.selectedColumns = [...DEFAULT_COLUMNS]
}
} else {
this.selectedColumns = [...DEFAULT_COLUMNS]
}
},
saveColumnConfig() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.selectedColumns))
},
selectAllColumns() {
this.selectedColumns = this.allColumns.map(col => col.prop)
this.saveColumnConfig()
},
unselectAllColumns() {
this.selectedColumns = []
this.saveColumnConfig()
},
resetColumns() {
this.selectedColumns = [...DEFAULT_COLUMNS]
this.saveColumnConfig()
},
handleTreeNodeClick(data) {
//
if (data.type === 'group') {
this.queryParams.groupName = data.groupName
this.queryParams.tagName = undefined
} else if (data.type === 'tag') {
this.queryParams.groupName = data.groupName
this.queryParams.tagName = data.tagName
}
//
this.handleQuery()
},
resetTreeFilter() {
this.queryParams.groupName = undefined
this.queryParams.tagName = undefined
this.$refs.filterTree.setCurrentKey(null)
this.handleQuery()
},
canEditCost(row) {
//
if (this.queryParams.dataType !== 'day') {
return false
}
return row.dataLevel === 1 || row.dataLevel === 2
},
// /tree
getTreeData() {
let params = {}
if (this.queryParams.dataType === 'day' || !this.queryParams.dataType) {
// 使
params = {
startDate: this.queryParams.startDate,
endDate: this.queryParams.endDate
}
} else if (this.queryParams.dataType === 'week') {
//
const weekRange = this.calculateWeekRange(this.queryParams.year, this.queryParams.week)
params = {
startDate: weekRange.startDate,
endDate: weekRange.endDate
}
} else if (this.queryParams.dataType === 'month') {
//
const monthRange = this.calculateMonthRange(this.queryParams.yearMonth)
params = {
startDate: monthRange.startDate,
endDate: monthRange.endDate
}
} else if (this.queryParams.dataType === 'all') {
//
params = {}
}
console.log('getTreeData called with params:', params, 'dataType:', this.queryParams.dataType)
treeCustomerStatisticsV2(params).then(response => {
console.log('getTreeData response:', response)
this.treeData = response.data || []
}).catch(error => {
console.error('getTreeData error:', error)
})
},
//
calculateWeekRange(year, week) {
const start = new Date(year, 0, 1)
const diff = (week - 1) * 7 * 24 * 60 * 60 * 1000
const startDate = new Date(start.getTime() + diff - ((start.getDay() + 6) % 7) * 24 * 60 * 60 * 1000)
const endDate = new Date(startDate.getTime() + 6 * 24 * 60 * 60 * 1000)
return {
startDate: this.parseTime(startDate, '{y}-{m}-{d}'),
endDate: this.parseTime(endDate, '{y}-{m}-{d}')
}
},
//
calculateMonthRange(yearMonth) {
if (!yearMonth) {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
yearMonth = `${year}-${month}`
}
const [year, month] = yearMonth.split('-')
const startDate = new Date(year, month - 1, 1)
const endDate = new Date(year, month, 0)
return {
startDate: this.parseTime(startDate, '{y}-{m}-{d}'),
endDate: this.parseTime(endDate, '{y}-{m}-{d}')
}
},
// /list
getList() {
this.loading = true
listCustomerStatisticsV2(this.queryParams).then(response => {
this.dataList = response.rows || []
this.total = response.total || 0
this.loading = false
}).catch(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
handleDataTypeChange() {
//
if (this.queryParams.dataType === 'day') {
//
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
this.queryParams.startDate = `${year}-${month}-${day}`
this.queryParams.endDate = `${year}-${month}-${day}`
//
this.queryParams.year = undefined
this.queryParams.week = undefined
this.queryParams.yearMonth = undefined
this.weekDateRange = ''
} else if (this.queryParams.dataType === 'week') {
//
const now = new Date()
// 使 el-date-picker value-format
this.queryParams.year = String(now.getFullYear())
const start = new Date(now.getFullYear(), 0, 1)
const diff = now - start + ((start.getDay() + 6) % 7) * 86400000
this.queryParams.week = Math.floor(diff / 604800000) + 1
this.updateWeekDateRange()
//
this.queryParams.startDate = undefined
this.queryParams.endDate = undefined
this.queryParams.yearMonth = undefined
} else if (this.queryParams.dataType === 'month') {
//
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
this.queryParams.yearMonth = `${year}-${month}`
//
this.queryParams.startDate = undefined
this.queryParams.endDate = undefined
this.queryParams.year = undefined
this.queryParams.week = undefined
this.weekDateRange = ''
} else if (this.queryParams.dataType === 'all') {
//
this.queryParams.startDate = undefined
this.queryParams.endDate = undefined
this.queryParams.year = undefined
this.queryParams.week = undefined
this.queryParams.yearMonth = undefined
this.weekDateRange = ''
}
//
this.getTreeData()
this.handleQuery()
},
resetQuery() {
this.resetForm("queryForm")
this.queryParams.dataType = 'day'
this.queryParams.groupName = undefined
this.queryParams.tagName = undefined
// 使 el-date-picker value-format
this.queryParams.year = String(new Date().getFullYear())
this.queryParams.week = undefined
this.queryParams.yearMonth = undefined
this.weekDateRange = ''
//
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
this.queryParams.startDate = `${year}-${month}-${day}`
this.queryParams.endDate = `${year}-${month}-${day}`
this.$refs.filterTree.setCurrentKey(null)
this.handleQuery()
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
handleExport() {
const formattedDate = this.parseTime(new Date(), '{y}-{m}-{d}')
this.download('/wecom/customerStatisticsV2/export', {
...this.queryParams
}, `流量看板V2数据_${formattedDate}.xlsx`)
},
handleInputCost(row, cost, inputType) {
if (cost === '' || cost === null || cost === undefined) {
this.$modal.msgWarning("请输入成本金额")
return
}
const numCost = Number(cost)
if (isNaN(numCost)) {
this.$modal.msgWarning("成本金额必须为数字")
return
}
if (numCost < 0) {
this.$modal.msgWarning("成本金额不能为负数")
return
}
let formattedDate = row.curDate
if (row.curDate instanceof Date) {
formattedDate = this.parseTime(row.curDate, '{y}-{m}-{d}')
} else if (typeof row.curDate === 'string' && row.curDate.length > 10) {
formattedDate = row.curDate.substring(0, 10)
}
const costTypeName = inputType === 'single' ? '单条成本' : '总成本'
const displayName = row.tagName || row.groupName
this.$modal.confirm(`确认修改 ${formattedDate} ${displayName}${costTypeName}${numCost}`).then(() => {
inputCost({
date: formattedDate,
groupName: row.groupName,
tagName: row.tagName || null,
costValue: numCost,
inputType: inputType
}).then(() => {
this.$modal.msgSuccess("修改成功")
this.getList()
}).catch(() => {
this.getList()
})
}).catch(() => {
this.getList()
})
},
handleRecalculate() {
if (!this.queryParams.startDate || !this.queryParams.endDate) {
this.$modal.msgWarning("请选择日期范围")
return
}
this.$modal.confirm(`确认重新计算 ${this.queryParams.startDate}${this.queryParams.endDate} 的统计数据?`).then(() => {
recalculateStatisticsRange(this.queryParams.startDate, this.queryParams.endDate).then(() => {
this.$modal.msgSuccess("重新计算成功")
this.getList()
})
})
}
}
}
</script>
<style scoped>
.el-container {
min-height: calc(100vh - 180px);
}
.tree-aside {
background: #fff;
border-right: 1px solid #e6e6e6;
padding: 10px;
overflow-y: auto;
}
.tree-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 5px;
border-bottom: 1px solid #e6e6e6;
margin-bottom: 10px;
}
.tree-header span {
font-weight: bold;
font-size: 14px;
color: #333;
}
.filter-tree {
background: transparent;
}
.tree-node {
display: flex;
align-items: center;
font-size: 13px;
}
.group-node {
font-weight: bold;
color: #409EFF;
}
.tag-node {
color: #606266;
padding-left: 5px;
}
.node-count {
margin-left: 5px;
font-size: 12px;
color: #909399;
}
.el-table .el-input-number {
width: 100%;
}
.column-config {
max-height: 500px;
overflow-y: auto;
}
.config-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 10px;
}
.config-header span {
font-weight: bold;
font-size: 14px;
}
.config-body {
padding: 10px 0;
}
.config-group {
margin-bottom: 15px;
}
.group-title {
font-weight: bold;
color: #409EFF;
margin-bottom: 8px;
padding-left: 5px;
border-left: 3px solid #409EFF;
}
.checkbox-group {
display: flex;
flex-wrap: wrap;
}
.checkbox-item {
margin-right: 15px;
margin-bottom: 8px;
width: 120px;
}
</style>