Compare commits

..

29 Commits

Author SHA1 Message Date
MerCry 1451698d66 修改计算逻辑 2026-03-16 16:33:07 +08:00
MerCry 7e9422281b 增加合计显示和百分比显示 2026-03-15 12:57:01 +08:00
MerCry 56cc49958a 修改同名时 使用最新的用户信息 2026-03-14 00:21:12 +08:00
MerCry ba3f96b2d3 修改成单数据和进粉数据不正确的问题 2026-03-13 22:35:14 +08:00
MerCry f7ecda456e 修改成单数据不正确的问题 2026-03-13 02:47:24 +08:00
MerCry 130089bf61 修改成单数据不正确的问题 2026-03-13 00:31:45 +08:00
MerCry 43ec404567 增加2个定时任务 用于处理在2点锁定数据 2026-03-10 01:20:58 +08:00
MerCry 32fd5e8afc 不需要提交这个文件 2026-03-08 15:56:31 +08:00
MerCry 6c16ea1661 增加新界面用于数据展示 2026-03-08 15:52:17 +08:00
MerCry 143a9e90be 修改统计数据显示问题 2026-03-07 18:49:12 +08:00
MerCry 1bf27164d3 修改统计数据显示问题 2026-03-06 17:11:45 +08:00
MerCry 52e4d883f9 修改统计数据显示问题 2026-03-04 23:33:48 +08:00
MerCry 911f8cb583 修改统计数据显示问题 2026-03-04 17:21:33 +08:00
MerCry 14425ebf4a 修改统计数据显示问题 2026-03-04 14:17:58 +08:00
MerCry 38e61a934d 修改统计数据显示问题 2026-03-04 13:41:28 +08:00
MerCry a5264f2ab8 修改统计数据显示问题 2026-03-04 13:20:26 +08:00
MerCry 61d640da2b 修改调用任务时报错的问题 2026-02-27 19:09:08 +08:00
MerCry e9d0b0921c 修改定时任务启动时报错的问题 2026-02-24 21:36:36 +08:00
MerCry daa0172240 改成支持 如果是一个企业的话 默认登录后选择这一个企业 2026-02-10 22:18:25 +08:00
MerCry 1220e38d38 恢复修改 2026-02-10 03:26:46 +08:00
MerCry 85efb6f0e3 修改为hash模式 2026-02-10 02:33:25 +08:00
MerCry 5f66bf5627 修改配置文件 2026-02-10 02:20:09 +08:00
MerCry f22c0d1b2e 增加菜单 初始化数据 删除多余文件 2026-02-09 14:51:39 +08:00
MerCry f577cbcbe5 增加许可证认证 2026-02-09 14:48:02 +08:00
MerCry 023ea78d18 增加配置文件修改 2026-02-09 11:27:33 +08:00
MerCry 8831100cc4 增加重复插入机制 允许 多次重复执行 不会出现数据重复 2026-02-09 10:39:52 +08:00
MerCry 8464884288 增加企业信息 提交企业信息修改 增加多公司支持 运行无错初版 2026-02-08 23:40:44 +08:00
MerCry c619c28895 增加企业信息 提交企业信息修改 增加多公司支持 运行无错初版 2026-02-08 23:38:56 +08:00
MerCry a4a8b468cd 增加企业信息 提交企业信息修改 增加多公司支持 2026-02-08 20:32:01 +08:00
153 changed files with 12029 additions and 4156 deletions

View File

@ -1,353 +0,0 @@
# 若依项目 Docker 部署指南
## 概述
本文档说明如何使用 Docker 和 Docker Compose 部署若依项目,并通过子路径 `/ashai-wecom-test` 访问。
## 前提条件
- Docker 已安装(版本 20.10+
- Docker Compose 已安装(版本 1.29+
- 服务器 80 端口已被占用,需要使用其他端口(如 8081
## 项目结构
```
wecom-dashboards/
├── Dockerfile.backend # 后端 Dockerfile
├── docker-compose.yml # Docker Compose 编排文件
├── ruoyi-ui/
│ ├── Dockerfile # 前端 Dockerfile
│ └── nginx.conf # Nginx 配置文件
├── ruoyi-admin/
│ └── src/main/resources/
│ ├── application.yml # 后端主配置
│ └── application-prod.yml # 生产环境配置
└── sql/ # 数据库初始化脚本
```
## 配置说明
### 1. 前端配置
前端已配置为使用子路径 `/ashai-wecom-test`
- **vue.config.js**: `publicPath` 设置为 `/ashai-wecom-test`
- **.env.production**: `VUE_APP_BASE_API` 设置为 `/prod-api`
- **nginx.conf**: 配置了子路径访问和 API 代理
### 2. 后端配置
后端配置支持环境变量注入:
- **数据库连接**: 通过环境变量 `SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_USERNAME`、`SPRING_DATASOURCE_PASSWORD`
- **Redis 连接**: 通过环境变量 `SPRING_REDIS_HOST`、`SPRING_REDIS_PORT`
- **上传路径**: 使用 Docker 卷挂载 `/home/ruoyi/uploadPath`
### 3. Docker Compose 配置
包含以下服务:
- **mysql**: MySQL 5.7 数据库
- **redis**: Redis 6 缓存
- **backend**: Spring Boot 后端服务
- **frontend**: Nginx 前端服务
## 部署步骤
### 步骤 1: 准备数据库脚本
确保 `sql/` 目录下有数据库初始化脚本:
```bash
cd Ruoyi-Vue-2/wecom-dashboards
ls sql/
# 应该包含: ry_20250522.sql, quartz.sql, schema.sql 等
```
### 步骤 2: 修改配置(可选)
如果需要修改数据库密码或其他配置,编辑 `docker-compose.yml`
```yaml
services:
mysql:
environment:
MYSQL_ROOT_PASSWORD: your_password # 修改数据库密码
backend:
environment:
SPRING_DATASOURCE_PASSWORD: your_password # 同步修改
```
### 步骤 3: 构建和启动服务
```bash
# 进入项目目录
cd Ruoyi-Vue-2/wecom-dashboards
# 构建并启动所有服务
docker-compose up -d --build
# 查看服务状态
docker-compose ps
# 查看日志
docker-compose logs -f
```
### 步骤 4: 等待服务启动
首次启动需要等待:
1. MySQL 初始化数据库(约 1-2 分钟)
2. 后端服务启动(约 30 秒)
3. 前端服务启动(约 10 秒)
查看后端日志确认启动成功:
```bash
docker-compose logs -f backend
# 看到 "Started RuoYiApplication" 表示启动成功
```
### 步骤 5: 访问应用
前端服务运行在 8081 端口,通过以下 URL 访问:
```
http://your-server-ip:8081/ashai-wecom-test
```
默认登录账号:
- 用户名: `admin`
- 密码: `admin123`
## 集成到现有 Nginx
如果你的服务器 80 端口已经有 Nginx 在运行,可以通过反向代理将请求转发到容器:
### 方案 1: Nginx 反向代理(推荐)
在你现有的 Nginx 配置中添加:
```nginx
# /etc/nginx/conf.d/ruoyi.conf 或在主配置文件中添加
server {
listen 80;
server_name your-domain.com; # 替换为你的域名
# 其他现有配置...
# 若依项目代理
location /ashai-wecom-test {
proxy_pass http://localhost:8081/ashai-wecom-test;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
```
重启 Nginx
```bash
nginx -t # 测试配置
nginx -s reload # 重新加载配置
```
现在可以通过以下 URL 访问:
```
http://your-domain.com/ashai-wecom-test
```
### 方案 2: 修改 Docker Compose 端口映射
如果不想使用反向代理,可以修改 `docker-compose.yml` 中的端口映射:
```yaml
services:
frontend:
ports:
- "8081:80" # 改为其他未占用的端口
```
## 常用命令
```bash
# 启动服务
docker-compose up -d
# 停止服务
docker-compose down
# 重启服务
docker-compose restart
# 查看日志
docker-compose logs -f [service_name]
# 进入容器
docker-compose exec backend bash
docker-compose exec frontend sh
# 重新构建并启动
docker-compose up -d --build
# 清理所有数据(包括数据库)
docker-compose down -v
```
## 数据持久化
Docker Compose 配置了以下数据卷:
- `mysql-data`: MySQL 数据库文件
- `redis-data`: Redis 持久化数据
- `upload-data`: 文件上传目录
数据会持久化保存,即使容器重启也不会丢失。
## 备份和恢复
### 备份数据库
```bash
docker-compose exec mysql mysqldump -uroot -ppassword ruoyi > backup.sql
```
### 恢复数据库
```bash
docker-compose exec -T mysql mysql -uroot -ppassword ruoyi < backup.sql
```
### 备份上传文件
```bash
docker cp ruoyi-backend:/home/ruoyi/uploadPath ./uploadPath_backup
```
## 故障排查
### 1. 前端无法访问
检查前端容器日志:
```bash
docker-compose logs frontend
```
确认 Nginx 配置正确:
```bash
docker-compose exec frontend cat /etc/nginx/conf.d/default.conf
```
### 2. 后端无法连接数据库
检查后端日志:
```bash
docker-compose logs backend
```
确认 MySQL 已启动:
```bash
docker-compose ps mysql
```
进入 MySQL 容器检查:
```bash
docker-compose exec mysql mysql -uroot -ppassword -e "SHOW DATABASES;"
```
### 3. API 请求失败
检查 Nginx 代理配置:
```bash
docker-compose exec frontend cat /etc/nginx/conf.d/default.conf
```
确认后端服务可访问:
```bash
curl http://localhost:8080/
```
### 4. 前端静态资源 404
确认前端构建时 `publicPath` 配置正确:
```bash
# 检查 vue.config.js
cat ruoyi-ui/vue.config.js | grep publicPath
```
确认 Nginx 中的文件路径:
```bash
docker-compose exec frontend ls -la /usr/share/nginx/html/ashai-wecom-test
```
## 性能优化
### 1. 调整 JVM 参数
修改 `Dockerfile.backend`,在 `ENTRYPOINT` 中添加 JVM 参数:
```dockerfile
ENTRYPOINT ["java", "-Xms512m", "-Xmx1024m", "-jar", "app.jar"]
```
### 2. 启用 Nginx Gzip
前端 Dockerfile 已配置 gzip 压缩,确保 Nginx 配置中启用:
```nginx
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
```
### 3. 调整数据库连接池
修改 `application-prod.yml` 中的 Druid 配置。
## 安全建议
1. **修改默认密码**: 修改 MySQL root 密码和应用管理员密码
2. **配置 Redis 密码**: 在生产环境中为 Redis 设置密码
3. **使用 HTTPS**: 配置 SSL 证书,使用 HTTPS 访问
4. **限制端口访问**: 使用防火墙限制数据库和 Redis 端口的外部访问
5. **定期备份**: 设置定时任务定期备份数据库和文件
## 更新部署
当代码更新后,重新部署:
```bash
# 拉取最新代码
git pull
# 重新构建并启动
docker-compose up -d --build
# 查看日志确认启动成功
docker-compose logs -f
```
## 联系支持
如有问题,请查看:
- 若依官方文档: http://doc.ruoyi.vip
- Docker 官方文档: https://docs.docker.com
- 项目 README: ./README.md

View File

@ -1,35 +0,0 @@
# 后端 Dockerfile
FROM maven:3.8.5-openjdk-8 AS build
# 设置工作目录
WORKDIR /app
# 复制 pom.xml 和源代码
COPY pom.xml .
COPY ruoyi-admin ./ruoyi-admin
COPY ruoyi-common ./ruoyi-common
COPY ruoyi-framework ./ruoyi-framework
COPY ruoyi-generator ./ruoyi-generator
COPY ruoyi-quartz ./ruoyi-quartz
COPY ruoyi-system ./ruoyi-system
COPY excel-handle ./excel-handle
# 构建项目
RUN mvn clean package -DskipTests
# 运行阶段
FROM openjdk:8-jre-slim
WORKDIR /app
# 复制构建好的 jar 包
COPY --from=build /app/ruoyi-admin/target/*.jar app.jar
# 创建上传目录
RUN mkdir -p /home/ruoyi/uploadPath
# 暴露端口
EXPOSE 8080
# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@ -1,180 +0,0 @@
# 配置文件说明
## 外部配置文件方案
为了解决 Druid 配置无法通过环境变量覆盖的问题,我们使用外部配置文件方案。
## 文件结构
```
deploy/backend/
├── Dockerfile
├── entrypoint.sh
├── application-druid.yml # 外部配置文件
└── ruoyi-admin.jar # 你的 jar 包
```
## 配置文件说明
### application-druid.yml
这个文件会覆盖 jar 包内的 `application-druid.yml` 配置。
**重要配置项**
```yaml
spring:
datasource:
druid:
master:
url: jdbc:mysql://host.docker.internal:3316/ry-vue?...
username: root
password: jiong1114 # 修改为你的密码
redis:
host: host.docker.internal
port: 6379
password: # 如果有密码,填写这里
```
### 修改配置
如果需要修改数据库连接信息,编辑 [application-druid.yml](backend/application-druid.yml)
1. **数据库地址**: 修改 `master.url`
2. **数据库用户名**: 修改 `master.username`
3. **数据库密码**: 修改 `master.password`
4. **Redis 地址**: 修改 `redis.host`
5. **Redis 密码**: 修改 `redis.password`
## 工作原理
Spring Boot 会按以下顺序加载配置(后面的会覆盖前面的):
1. jar 包内的 `application.yml`
2. jar 包内的 `application-druid.yml`
3. jar 包外的 `application.yml`(如果存在)
4. jar 包外的 `application-druid.yml` ✅ **我们使用这个**
## 重新部署
修改配置后,需要重新构建镜像:
```bash
# 停止并删除旧容器
docker stop wecom-backend
docker rm wecom-backend
# 重新构建并启动
cd deploy
docker compose up -d --build backend
# 查看日志
docker compose logs -f backend
```
## 不需要重新构建的方案
如果你想修改配置而不重新构建镜像,可以使用卷挂载:
### 修改 docker-compose.yml
```yaml
services:
backend:
volumes:
- upload_data:/home/ruoyi/uploadPath
- ./backend/application-druid.yml:/app/application-druid.yml # 挂载配置文件
```
这样修改 `backend/application-druid.yml` 后,只需重启容器:
```bash
docker compose restart backend
```
## 验证配置
### 1. 检查配置文件是否被复制
```bash
docker exec -it wecom-backend cat /app/application-druid.yml
```
### 2. 查看启动日志
```bash
docker compose logs backend | grep -i "datasource\|redis"
```
### 3. 测试数据库连接
```bash
# 进入容器
docker exec -it wecom-backend sh
# 测试 MySQL 连接(需要安装 telnet 或 nc
nc -zv host.docker.internal 3316
# 测试 Redis 连接
nc -zv host.docker.internal 6379
```
## 常见问题
### 1. 配置文件没有生效
确认配置文件在正确的位置:
```bash
docker exec -it wecom-backend ls -la /app/
```
应该看到 `application-druid.yml` 文件。
### 2. 数据库名称不匹配
确保 MySQL 中存在对应的数据库:
```bash
docker exec -it mysql-jijin-test mysql -uroot -pjiong1114 -e "SHOW DATABASES;"
```
如果没有 `ry-vue` 数据库,创建它:
```bash
docker exec -it mysql-jijin-test mysql -uroot -pjiong1114 -e "CREATE DATABASE IF NOT EXISTS \`ry-vue\` DEFAULT CHARACTER SET utf8mb4;"
```
### 3. 仍然报配置错误
检查 jar 包的 Spring Boot 版本和配置加载方式。某些版本可能需要使用 `--spring.config.location` 参数:
修改 `entrypoint.sh`
```bash
#!/bin/sh
exec java -Dspring.profiles.active=prod \
--spring.config.location=classpath:/,file:/app/ \
-jar app.jar
```
## 完整的部署流程
```bash
# 1. 准备文件
deploy/backend/
├── ruoyi-admin.jar # 你的 jar 包
├── application-druid.yml # 已创建
├── entrypoint.sh # 已创建
└── Dockerfile # 已创建
# 2. 修改配置(如果需要)
vim deploy/backend/application-druid.yml
# 3. 构建并启动
cd deploy
docker compose up -d --build backend
# 4. 查看日志
docker compose logs -f backend
# 5. 验证启动成功
# 看到 "Started RuoYiApplication" 表示成功
```

View File

@ -1,266 +0,0 @@
# 使用现有 MySQL 和 Redis 容器的配置说明
## 📋 当前配置
你的现有容器:
- **MySQL**: `mysql-jijin-test` (端口 3316)
- **Redis**: `redis` (端口 6379)
## 🔧 配置步骤
### 1. 修改 docker-compose.yml
已经配置为使用 `host.docker.internal` 连接宿主机的容器。
关键配置:
```yaml
environment:
# MySQL 连接(注意端口是 3316
- SPRING_DATASOURCE_URL=jdbc:mysql://host.docker.internal:3316/ry-vue?...
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=password # 修改为你的密码
# Redis 连接
- SPRING_REDIS_HOST=host.docker.internal
- SPRING_REDIS_PORT=6379
- SPRING_REDIS_PASSWORD= # 如果有密码,填写这里
extra_hosts:
- "host.docker.internal:host-gateway" # 允许容器访问宿主机
```
### 2. 修改数据库配置
**重要**:请根据你的实际情况修改以下配置:
#### 数据库名称
默认使用 `ry-vue`,如果你的数据库名称不同,修改:
```yaml
SPRING_DATASOURCE_URL=jdbc:mysql://host.docker.internal:3316/你的数据库名?...
```
#### 数据库密码
修改为你的 MySQL root 密码:
```yaml
SPRING_DATASOURCE_PASSWORD=你的密码
```
#### Redis 密码
如果你的 Redis 设置了密码,修改:
```yaml
SPRING_REDIS_PASSWORD=你的redis密码
```
### 3. 准备数据库
在你的 MySQL 容器中创建数据库并导入数据:
```bash
# 方式 1: 进入 MySQL 容器
docker exec -it mysql-jijin-test mysql -uroot -p
# 创建数据库
CREATE DATABASE IF NOT EXISTS `ry-vue` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
# 退出
exit
# 方式 2: 导入 SQL 文件
docker exec -i mysql-jijin-test mysql -uroot -p你的密码 ry-vue < /path/to/your.sql
```
### 4. 启动服务
```bash
cd deploy
docker-compose up -d
```
## 🔍 验证连接
### 检查后端日志
```bash
docker-compose logs -f backend
```
如果看到类似以下内容,说明连接成功:
```
Started RuoYiApplication in X seconds
```
### 测试数据库连接
```bash
# 进入后端容器
docker exec -it wecom-backend sh
# 测试 MySQL 连接
wget -O- http://host.docker.internal:3316 2>&1 | grep -i mysql
# 测试 Redis 连接
ping host.docker.internal
```
## 🐛 常见问题
### 1. 无法连接到 host.docker.internal
**Windows/Mac**: `host.docker.internal` 自动可用
**Linux**: 需要手动添加,已在 docker-compose.yml 中配置:
```yaml
extra_hosts:
- "host.docker.internal:host-gateway"
```
### 2. 数据库连接被拒绝
检查 MySQL 容器是否允许远程连接:
```bash
# 进入 MySQL 容器
docker exec -it mysql-jijin-test mysql -uroot -p
# 检查用户权限
SELECT host, user FROM mysql.user WHERE user='root';
# 如果 host 只有 localhost需要授权
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '你的密码';
FLUSH PRIVILEGES;
```
### 3. 端口连接错误
确认你的 MySQL 端口是 3316
```bash
docker ps | grep mysql
```
如果端口不同,修改 docker-compose.yml 中的端口号。
### 4. Redis 连接失败
检查 Redis 是否允许远程连接:
```bash
# 查看 Redis 配置
docker exec -it redis redis-cli CONFIG GET bind
# 如果绑定了 127.0.0.1,需要修改为 0.0.0.0
docker exec -it redis redis-cli CONFIG SET bind "0.0.0.0"
```
## 🔄 替代方案:使用 Docker 网络
如果 `host.docker.internal` 不工作,可以让新容器加入现有容器的网络:
### 1. 查看现有容器的网络
```bash
docker inspect mysql-jijin-test | grep NetworkMode
docker inspect redis | grep NetworkMode
```
### 2. 修改 docker-compose.yml
假设现有容器在 `bridge` 网络:
```yaml
services:
backend:
# ... 其他配置
environment:
# 直接使用容器名连接
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql-jijin-test:3306/ry-vue?...
- SPRING_REDIS_HOST=redis
networks:
- default
networks:
default:
external: true
name: bridge # 或者你的网络名称
```
### 3. 或者创建自定义网络
```bash
# 创建网络
docker network create my-network
# 将现有容器连接到网络
docker network connect my-network mysql-jijin-test
docker network connect my-network redis
# 修改 docker-compose.yml
networks:
wecom-network:
external: true
name: my-network
```
## 📝 完整配置示例
### docker-compose.yml使用 host.docker.internal
```yaml
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: wecom-backend
restart: always
ports:
- "8080:8080"
volumes:
- upload_data:/home/ruoyi/uploadPath
environment:
- SPRING_PROFILES_ACTIVE=prod
- TZ=Asia/Shanghai
- SPRING_DATASOURCE_URL=jdbc:mysql://host.docker.internal:3316/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=your_password
- SPRING_REDIS_HOST=host.docker.internal
- SPRING_REDIS_PORT=6379
- SPRING_REDIS_PASSWORD=
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- wecom-network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: wecom-frontend
restart: always
ports:
- "8081:80"
depends_on:
- backend
networks:
- wecom-network
volumes:
upload_data:
driver: local
networks:
wecom-network:
driver: bridge
```
## 🎯 快速检查清单
- [ ] 修改了 MySQL 密码配置
- [ ] 修改了数据库名称(如果不是 ry-vue
- [ ] 修改了 Redis 密码(如果有)
- [ ] 确认 MySQL 端口是 3316
- [ ] 确认 Redis 端口是 6379
- [ ] 在 MySQL 中创建了数据库
- [ ] 导入了数据库初始化脚本
- [ ] MySQL 允许远程连接
- [ ] Redis 允许远程连接
完成以上检查后,运行 `docker-compose up -d` 即可启动服务。

View File

@ -1,85 +0,0 @@
# Docker 镜像问题说明
## 问题原因
`openjdk:8-jre-alpine` 镜像已被弃用,无法从 Docker Hub 拉取。
## 解决方案
已将 Dockerfile 中的基础镜像更改为 `eclipse-temurin:8-jre-alpine`
### Eclipse Temurin 是什么?
- Eclipse Temurin 是 OpenJDK 的官方继任者
- 由 Eclipse Adoptium 项目维护
- 完全兼容 OpenJDK
- 持续更新和维护
## 其他可用的 Java 8 镜像
如果 `eclipse-temurin:8-jre-alpine` 也无法拉取,可以尝试以下替代方案:
### 1. Amazon Corretto推荐
```dockerfile
FROM amazoncorretto:8-alpine-jre
```
### 2. Eclipse Temurin非 Alpine
```dockerfile
FROM eclipse-temurin:8-jre
```
注意:体积较大(约 200MB vs 85MB
### 3. Azul Zulu
```dockerfile
FROM azul/zulu-openjdk-alpine:8-jre
```
### 4. 使用国内镜像加速
如果网络问题导致拉取失败,可以配置 Docker 镜像加速器:
#### 阿里云镜像加速
```json
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://registry.docker-cn.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
```
配置位置:
- **Windows**: Docker Desktop -> Settings -> Docker Engine
- **Linux**: `/etc/docker/daemon.json`
配置后重启 Docker
```bash
# Linux
sudo systemctl restart docker
# Windows
重启 Docker Desktop
```
## 当前配置
已修改 [backend/Dockerfile](backend/Dockerfile) 使用:
```dockerfile
FROM eclipse-temurin:8-jre-alpine
```
现在可以重新尝试构建:
```bash
docker compose up -d
```
## 如果仍然失败
1. 检查网络连接
2. 配置镜像加速器
3. 或者使用非 Alpine 版本(体积更大但更稳定):
```dockerfile
FROM eclipse-temurin:8-jre
```

View File

@ -1,204 +0,0 @@
# 不使用 docker-compose 的部署方案
如果你没有安装 docker-compose可以使用以下两种方式
## 方式 1: 使用 Docker Compose V2推荐
Docker Desktop 和新版 Docker 已经内置了 Compose V2命令是 `docker compose`(注意是空格,不是连字符)。
### 测试是否可用
```bash
docker compose version
```
如果可用,只需将所有 `docker-compose` 命令改为 `docker compose`
```bash
# 启动服务
docker compose up -d
# 查看日志
docker compose logs -f
# 停止服务
docker compose down
```
## 方式 2: 使用纯 Docker 命令
如果 `docker compose` 也不可用,可以使用纯 Docker 命令手动启动容器。
### 1. 创建网络
```bash
docker network create wecom-network
```
### 2. 构建镜像
#### 构建后端镜像
```bash
cd deploy/backend
docker build -t wecom-backend:latest .
cd ../..
```
#### 构建前端镜像
```bash
cd deploy/frontend
docker build -t wecom-frontend:latest .
cd ../..
```
### 3. 启动容器
#### 启动后端容器
```bash
docker run -d \
--name wecom-backend \
--restart always \
--network wecom-network \
-p 8080:8080 \
-v wecom-upload-data:/home/ruoyi/uploadPath \
-e SPRING_PROFILES_ACTIVE=prod \
-e TZ=Asia/Shanghai \
-e SPRING_DATASOURCE_URL="jdbc:mysql://host.docker.internal:3316/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8" \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=你的MySQL密码 \
-e SPRING_REDIS_HOST=host.docker.internal \
-e SPRING_REDIS_PORT=6379 \
-e SPRING_REDIS_PASSWORD= \
--add-host host.docker.internal:host-gateway \
wecom-backend:latest
```
#### 启动前端容器
```bash
docker run -d \
--name wecom-frontend \
--restart always \
--network wecom-network \
-p 8081:80 \
wecom-frontend:latest
```
### 4. 常用管理命令
```bash
# 查看运行中的容器
docker ps
# 查看所有容器(包括停止的)
docker ps -a
# 查看后端日志
docker logs -f wecom-backend
# 查看前端日志
docker logs -f wecom-frontend
# 停止容器
docker stop wecom-backend wecom-frontend
# 启动容器
docker start wecom-backend wecom-frontend
# 重启容器
docker restart wecom-backend wecom-frontend
# 删除容器
docker rm -f wecom-backend wecom-frontend
# 删除网络
docker network rm wecom-network
# 删除数据卷
docker volume rm wecom-upload-data
```
### 5. 更新部署
#### 更新后端
```bash
# 停止并删除旧容器
docker stop wecom-backend
docker rm wecom-backend
# 重新构建镜像
cd deploy/backend
docker build -t wecom-backend:latest .
cd ../..
# 启动新容器(使用上面的 docker run 命令)
docker run -d \
--name wecom-backend \
--restart always \
--network wecom-network \
-p 8080:8080 \
-v wecom-upload-data:/home/ruoyi/uploadPath \
-e SPRING_PROFILES_ACTIVE=prod \
-e TZ=Asia/Shanghai \
-e SPRING_DATASOURCE_URL="jdbc:mysql://host.docker.internal:3316/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8" \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=你的MySQL密码 \
-e SPRING_REDIS_HOST=host.docker.internal \
-e SPRING_REDIS_PORT=6379 \
-e SPRING_REDIS_PASSWORD= \
--add-host host.docker.internal:host-gateway \
wecom-backend:latest
```
#### 更新前端
```bash
# 停止并删除旧容器
docker stop wecom-frontend
docker rm wecom-frontend
# 重新构建镜像
cd deploy/frontend
docker build -t wecom-frontend:latest .
cd ../..
# 启动新容器
docker run -d \
--name wecom-frontend \
--restart always \
--network wecom-network \
-p 8081:80 \
wecom-frontend:latest
```
## 方式 3: 使用部署脚本(最简单)
我可以为你创建一个 Shell 脚本,自动执行所有 Docker 命令。
### Windows (PowerShell)
创建 `deploy.ps1` 脚本
### Linux/Mac (Bash)
创建 `deploy.sh` 脚本
需要我创建这些脚本吗?
## 🔍 检查 Docker Compose 可用性
```bash
# 检查 docker-compose旧版
docker-compose --version
# 检查 docker compose新版
docker compose version
# 检查 Docker 版本
docker --version
```
## 📝 推荐方案
1. **最推荐**: 使用 `docker compose`Docker Compose V2
2. **次推荐**: 使用部署脚本(我可以帮你创建)
3. **备选**: 手动执行 docker 命令
你想使用哪种方式?我可以帮你:
- 测试 `docker compose` 是否可用
- 创建自动化部署脚本
- 提供完整的手动命令清单

View File

@ -1,181 +0,0 @@
# 端口配置说明
## 当前端口配置
### 后端服务
- **容器内端口**: 8888
- **宿主机端口**: 8888
- **访问地址**: `http://your-server-ip:8888`
### 前端服务
- **容器内端口**: 80
- **宿主机端口**: 8889
- **访问地址**: `http://your-server-ip:8889/ashai-wecom-test`
## 配置文件位置
### 1. 后端端口配置
**文件**: [backend/application-druid.yml](backend/application-druid.yml)
```yaml
server:
port: 8888
```
### 2. Docker 端口映射
**文件**: [docker-compose.yml](docker-compose.yml)
```yaml
services:
backend:
ports:
- "8888:8888" # 宿主机:容器
frontend:
ports:
- "8889:80" # 宿主机:容器
```
### 3. Dockerfile 暴露端口
**文件**: [backend/Dockerfile](backend/Dockerfile)
```dockerfile
EXPOSE 8888
```
## 修改端口
如果需要修改端口,需要同时修改以上三个地方:
### 示例:改为 9000 端口
1. **修改 application-druid.yml**:
```yaml
server:
port: 9000
```
2. **修改 docker-compose.yml**:
```yaml
ports:
- "9000:9000"
```
3. **修改 Dockerfile**:
```dockerfile
EXPOSE 9000
```
4. **修改 healthcheck**:
```yaml
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9000/"]
```
## 重新部署
修改端口后需要重新构建:
```bash
# 停止并删除旧容器
docker stop wecom-backend wecom-frontend
docker rm wecom-backend wecom-frontend
# 重新构建并启动
cd deploy
docker compose up -d --build
# 查看日志
docker compose logs -f
```
## 验证端口
### 检查容器端口
```bash
docker ps
```
应该看到类似:
```
CONTAINER ID IMAGE PORTS
xxx deploy-backend 0.0.0.0:8888->8888/tcp
xxx deploy-frontend 0.0.0.0:8889->80/tcp
```
### 测试后端访问
```bash
curl http://localhost:8888
```
### 测试前端访问
```bash
curl http://localhost:8889/ashai-wecom-test
```
## 常见问题
### 1. 端口被占用
如果启动时报错端口被占用:
```bash
# 查看端口占用
netstat -tulpn | grep 8888
# 或使用 lsofMac/Linux
lsof -i :8888
# Windows
netstat -ano | findstr 8888
```
解决方案:
- 停止占用端口的程序
- 或修改为其他未占用的端口
### 2. 容器内外端口不一致
如果宿主机 8080 被占用,可以只修改宿主机端口:
```yaml
ports:
- "9000:8888" # 宿主机 9000 映射到容器 8888
```
这样:
- 容器内应用仍运行在 8888
- 外部通过 9000 访问
### 3. 前端无法连接后端
确保前端 Nginx 配置中的后端地址正确:
**文件**: [frontend/nginx.conf](frontend/nginx.conf)
```nginx
location /ashai-wecom-test/prod-api/ {
proxy_pass http://backend:8888/; # 使用容器名和容器内端口
...
}
```
注意:
- 容器间通信使用**容器名**和**容器内端口**
- 不是宿主机端口
## 完整的访问流程
```
用户浏览器
http://server-ip:8889/ashai-wecom-test
宿主机 8889 端口
前端容器 80 端口 (Nginx)
API 请求: /ashai-wecom-test/prod-api/*
Nginx 代理到: http://backend:8888/
后端容器 8888 端口 (Spring Boot)
返回数据
```

View File

@ -1,293 +0,0 @@
# 简化版 Docker 部署指南
这是一个简化的 Docker 部署方案,直接使用已经打包好的 jar 包和 dist 文件,无需在容器内重新构建。
## 📁 目录结构
```
deploy/
├── docker-compose.yml # Docker Compose 配置文件
├── backend/ # 后端部署目录
│ ├── Dockerfile # 后端 Dockerfile
│ └── ruoyi-admin.jar # 【需要放置】后端 jar 包
├── frontend/ # 前端部署目录
│ ├── Dockerfile # 前端 Dockerfile
│ ├── nginx.conf # Nginx 配置文件
│ └── dist/ # 【需要放置】前端打包文件
└── sql/ # 【可选】数据库初始化脚本
└── ry_20xx.sql
```
## 🚀 快速部署步骤
### 1. 准备文件
将以下文件上传到服务器的 `deploy` 目录:
```bash
# 1. 将后端 jar 包放到 backend 目录
deploy/backend/ruoyi-admin.jar
# 2. 将前端 dist 文件夹放到 frontend 目录
deploy/frontend/dist/
# 3. (可选)将数据库初始化脚本放到 sql 目录
deploy/sql/ry_20xx.sql
```
### 2. 修改配置
编辑 `docker-compose.yml`,根据需要修改以下配置:
```yaml
# MySQL 配置
environment:
MYSQL_ROOT_PASSWORD: password # 修改为你的密码
MYSQL_DATABASE: ry-vue # 数据库名称
# 端口配置(如果端口冲突,可以修改)
ports:
- "3306:3306" # MySQL
- "6379:6379" # Redis
- "8080:8080" # 后端
- "8081:80" # 前端
```
### 3. 启动服务
```bash
# 进入 deploy 目录
cd deploy
# 启动所有服务
docker-compose up -d
# 查看服务状态
docker-compose ps
# 查看日志
docker-compose logs -f
```
### 4. 访问应用
- **前端地址**: `http://your-server-ip:8081/ashai-wecom-test`
- **后端 API**: `http://your-server-ip:8080`
- **默认账号**: admin / admin123
## 📝 常用命令
```bash
# 启动所有服务
docker-compose up -d
# 停止所有服务
docker-compose down
# 重启某个服务
docker-compose restart backend
docker-compose restart frontend
# 查看服务日志
docker-compose logs -f backend
docker-compose logs -f frontend
# 查看服务状态
docker-compose ps
# 进入容器
docker exec -it wecom-backend sh
docker exec -it wecom-frontend sh
# 重新构建并启动
docker-compose up -d --build
# 停止并删除所有容器、网络、数据卷
docker-compose down -v
```
## 🔄 更新部署
### 更新后端
```bash
# 1. 停止后端服务
docker-compose stop backend
# 2. 替换 jar 包
cp new-ruoyi-admin.jar backend/ruoyi-admin.jar
# 3. 重新构建并启动
docker-compose up -d --build backend
```
### 更新前端
```bash
# 1. 停止前端服务
docker-compose stop frontend
# 2. 替换 dist 文件
rm -rf frontend/dist
cp -r new-dist frontend/dist
# 3. 重新构建并启动
docker-compose up -d --build frontend
```
## 🔧 配置说明
### 后端配置
后端使用 `--spring.profiles.active=prod` 启动,确保你的 jar 包中包含正确的生产环境配置:
- 数据库连接:`jdbc:mysql://mysql:3306/ry-vue`
- Redis 连接:`redis://redis:6379`
如果需要修改配置,可以通过环境变量或重新打包 jar。
### 前端配置
前端通过 Nginx 提供服务,配置文件在 [frontend/nginx.conf](frontend/nginx.conf)
- 访问路径:`/ashai-wecom-test`
- API 代理:`/ashai-wecom-test/prod-api/` → `http://backend:8080/`
### 数据库初始化
首次启动时,如果 `sql` 目录中有 `.sql` 文件MySQL 会自动执行这些脚本。
如果需要手动导入:
```bash
# 复制 SQL 文件到容器
docker cp ry_20xx.sql wecom-mysql:/tmp/
# 进入 MySQL 容器
docker exec -it wecom-mysql bash
# 导入数据库
mysql -uroot -ppassword ry-vue < /tmp/ry_20xx.sql
```
## 🌐 集成到现有 Nginx80 端口)
如果服务器 80 端口已有 Nginx可以添加反向代理配置
```nginx
# 在你的 Nginx 配置中添加
location /ashai-wecom-test {
proxy_pass http://localhost:8081/ashai-wecom-test;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
然后重启 Nginx
```bash
nginx -t
nginx -s reload
```
## 🐛 故障排查
### 1. 后端无法连接数据库
```bash
# 检查 MySQL 是否启动
docker-compose ps mysql
# 查看 MySQL 日志
docker-compose logs mysql
# 检查网络连接
docker exec -it wecom-backend ping mysql
```
### 2. 前端无法访问后端 API
```bash
# 检查后端是否启动
docker-compose ps backend
# 查看后端日志
docker-compose logs backend
# 测试后端接口
curl http://localhost:8080/
```
### 3. 前端页面 404
```bash
# 检查 dist 文件是否正确放置
docker exec -it wecom-frontend ls -la /usr/share/nginx/html/ashai-wecom-test
# 查看 Nginx 日志
docker-compose logs frontend
# 检查 Nginx 配置
docker exec -it wecom-frontend cat /etc/nginx/conf.d/default.conf
```
### 4. 端口被占用
```bash
# 查看端口占用
netstat -tulpn | grep 8080
netstat -tulpn | grep 8081
# 修改 docker-compose.yml 中的端口映射
ports:
- "8082:8080" # 改为其他端口
```
### 5. 容器启动失败
```bash
# 查看详细日志
docker-compose logs -f
# 检查容器状态
docker-compose ps
# 重新构建
docker-compose down
docker-compose up -d --build
```
## 📊 数据持久化
所有数据都通过 Docker 卷持久化:
- `mysql_data`: MySQL 数据
- `redis_data`: Redis 数据
- `upload_data`: 文件上传目录
即使删除容器,数据也不会丢失。如需完全清理:
```bash
docker-compose down -v
```
## 🔒 安全建议
1. **修改默认密码**:修改 MySQL root 密码和应用管理员密码
2. **限制端口访问**:使用防火墙限制数据库端口的外部访问
3. **使用 HTTPS**:在生产环境配置 SSL 证书
4. **定期备份**:定期备份数据库和上传文件
## 📈 性能优化
1. **调整 JVM 参数**:在 [backend/Dockerfile](backend/Dockerfile) 中添加 JVM 参数
2. **配置 Nginx 缓存**:已在 [frontend/nginx.conf](frontend/nginx.conf) 中配置
3. **数据库优化**:根据实际情况调整 MySQL 配置
## 💡 提示
- 首次启动可能需要等待 1-2 分钟,等待数据库初始化
- 确保服务器有足够的内存(建议至少 2GB
- 生产环境建议使用外部数据库,而不是容器内的数据库

View File

@ -1,184 +0,0 @@
# 后端启动配置问题修复
## 问题描述
后端启动时报错:
```
Could not resolve placeholder 'spring.datasource.druid.initialSize'
```
## 原因分析
若依项目使用了 Druid 数据源,配置在 `application-druid.yml` 中。环境变量无法直接覆盖 Druid 的嵌套配置属性。
## 解决方案
### 1. 创建启动脚本
已创建 [entrypoint.sh](backend/entrypoint.sh),使用 Java 系统属性覆盖配置:
```bash
#!/bin/sh
exec java \
-Dspring.profiles.active=prod \
-Dspring.datasource.druid.master.url="${SPRING_DATASOURCE_URL}" \
-Dspring.datasource.druid.master.username="${SPRING_DATASOURCE_USERNAME}" \
-Dspring.datasource.druid.master.password="${SPRING_DATASOURCE_PASSWORD}" \
-Dspring.redis.host="${SPRING_REDIS_HOST}" \
-Dspring.redis.port="${SPRING_REDIS_PORT}" \
-jar app.jar
```
### 2. 修改 Dockerfile
已修改 [backend/Dockerfile](backend/Dockerfile),使用启动脚本:
```dockerfile
# 复制启动脚本
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 启动应用
ENTRYPOINT ["/entrypoint.sh"]
```
## 重新部署
### 1. 停止并删除旧容器
```bash
docker stop wecom-backend
docker rm wecom-backend
```
### 2. 重新构建并启动
```bash
cd deploy
docker compose up -d --build backend
```
### 3. 查看日志
```bash
docker compose logs -f backend
```
## 验证启动成功
查看日志中是否有以下内容:
```
Started RuoYiApplication in X seconds
```
## 其他可能的问题
### 1. 数据库连接失败
检查 MySQL 容器是否允许远程连接:
```bash
docker exec -it mysql-jijin-test mysql -uroot -p
# 授权远程访问
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'jiong1114';
FLUSH PRIVILEGES;
```
### 2. 数据库不存在
创建数据库:
```bash
docker exec -it mysql-jijin-test mysql -uroot -pjiong1114
CREATE DATABASE IF NOT EXISTS `ry-vue` DEFAULT CHARACTER SET utf8mb4;
```
### 3. Redis 连接失败
检查 Redis 是否允许远程连接:
```bash
docker exec -it redis redis-cli CONFIG GET bind
# 如果绑定了 127.0.0.1,需要修改
docker exec -it redis redis-cli CONFIG SET bind "0.0.0.0"
```
### 4. 端口配置不匹配
注意 docker-compose.yml 中配置的是 8888 端口:
```yaml
ports:
- "8888:8888" # 宿主机:容器
```
但 Dockerfile 暴露的是 8080 端口。需要修改其中一个保持一致。
#### 方案 A: 修改 docker-compose.yml推荐
```yaml
ports:
- "8888:8080" # 宿主机 8888 映射到容器 8080
```
#### 方案 B: 修改应用端口
在 docker-compose.yml 中添加环境变量:
```yaml
environment:
- SERVER_PORT=8888
```
## 完整的 docker-compose.yml 配置
```yaml
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: wecom-backend
restart: always
ports:
- "8888:8080" # 宿主机 8888 映射到容器 8080
volumes:
- upload_data:/home/ruoyi/uploadPath
environment:
- SPRING_PROFILES_ACTIVE=prod
- TZ=Asia/Shanghai
- SPRING_DATASOURCE_URL=jdbc:mysql://host.docker.internal:3316/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=jiong1114
- SPRING_REDIS_HOST=host.docker.internal
- SPRING_REDIS_PORT=6379
- SPRING_REDIS_PASSWORD=
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- wecom-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/"]
interval: 30s
timeout: 10s
retries: 3
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: wecom-frontend
restart: always
ports:
- "8889:80"
depends_on:
- backend
networks:
- wecom-network
volumes:
upload_data:
driver: local
networks:
wecom-network:
driver: bridge
```
注意 healthcheck 中使用的是容器内部端口 8080。

View File

@ -1,58 +0,0 @@
version: '3.8'
services:
# 后端服务
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: wecom-backend
restart: always
ports:
- "8888:8888"
volumes:
- upload_data:/home/ruoyi/uploadPath
environment:
- SPRING_PROFILES_ACTIVE=prod
- TZ=Asia/Shanghai
# 数据库连接配置(连接到宿主机的 MySQL 容器)
- SPRING_DATASOURCE_URL=jdbc:mysql://host.docker.internal:3316/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=jiong1114
# Redis 连接配置(连接到宿主机的 Redis 容器)
- SPRING_REDIS_HOST=host.docker.internal
- SPRING_REDIS_PORT=6379
- SPRING_REDIS_PASSWORD=
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- wecom-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8888/"]
interval: 30s
timeout: 10s
retries: 3
# 前端服务
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: wecom-frontend
restart: always
ports:
- "8889:80"
depends_on:
- backend
networks:
- wecom-network
# 数据卷
volumes:
upload_data:
driver: local
# 网络
networks:
wecom-network:
driver: bridge

View File

@ -3,7 +3,7 @@ FROM nginx:alpine
# 复制 dist 文件到 Nginx 的子路径目录
# 使用时将 dist 目录放在与 Dockerfile 同级目录
COPY dist /usr/share/nginx/html/ashai-wecom-test
COPY dist /usr/share/nginx/html
# 复制 Nginx 配置文件
COPY nginx.conf /etc/nginx/conf.d/default.conf

View File

@ -28,9 +28,9 @@ server {
}
# 前端静态文件路径(带项目路径前缀)
location /ashai-wecom-test {
alias /usr/share/nginx/html/ashai-wecom-test;
try_files $uri $uri/ /ashai-wecom-test/index.html;
location {
alias /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
index index.html;
# 静态资源缓存配置
@ -41,7 +41,7 @@ server {
}
# API 代理到后端(带项目路径前缀)
location /ashai-wecom-test/prod-api/ {
location /prod-api/ {
proxy_pass http://backend:8888/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -59,12 +59,12 @@ server {
# 根路径重定向到项目路径
location = / {
return 301 /ashai-wecom-test/;
return 301 /;
}
# 其他所有路径都尝试从项目目录加载(支持 Vue Router History 模式)
location / {
root /usr/share/nginx/html/ashai-wecom-test;
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
index index.html;

View File

@ -3,77 +3,107 @@ version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:5.7
container_name: ruoyi-mysql
image: mysql:8.0
container_name: wecom-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: ruoyi
MYSQL_ROOT_PASSWORD: ash@szmp
MYSQL_DATABASE: ash-vue
MYSQL_USER: ash
MYSQL_PASSWORD: ash@szmp
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./sql:/docker-entrypoint-initdb.d
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
- mysql_data:/var/lib/mysql
- mysql_conf:/etc/mysql/conf.d
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
networks:
- ruoyi-network
restart: unless-stopped
- wecom-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-pash@szmp"]
interval: 10s
timeout: 5s
retries: 5
# Redis 缓存
redis:
image: redis:6-alpine
container_name: ruoyi-redis
image: redis:7-alpine
container_name: wecom-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis-data:/data
- redis_data:/data
command: redis-server --appendonly yes --requirepass ash@szmp
networks:
- ruoyi-network
restart: unless-stopped
- wecom-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# 后端服务
backend:
build:
context: .
dockerfile: Dockerfile.backend
container_name: ruoyi-backend
context: ./backend
dockerfile: Dockerfile
container_name: wecom-backend
restart: always
ports:
- "8888:8888"
volumes:
- upload_data:/home/ash/uploadPath
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ruoyi?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: password
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ash-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
SPRING_DATASOURCE_USERNAME: ash
SPRING_DATASOURCE_PASSWORD: ash@szmp
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
SERVER_SERVLET_CONTEXT_PATH: /
ports:
- "8080:8080"
volumes:
- upload-data:/home/ruoyi/uploadPath
SPRING_REDIS_PASSWORD: ash@szmp
depends_on:
- mysql
- redis
mysql:
condition: service_healthy
redis:
condition: service_healthy
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- ruoyi-network
restart: unless-stopped
- wecom-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8888/"]
interval: 30s
timeout: 10s
retries: 3
# 前端 Nginx
# 前端服务
frontend:
build:
context: ./ruoyi-ui
context: ./frontend
dockerfile: Dockerfile
container_name: ruoyi-frontend
container_name: wecom-frontend
restart: always
ports:
- "8081:80"
- "8889:80"
depends_on:
- backend
backend:
condition: service_healthy
networks:
- ruoyi-network
restart: unless-stopped
networks:
ruoyi-network:
driver: bridge
- wecom-network
# 数据卷
volumes:
mysql-data:
redis-data:
upload-data:
mysql_data:
driver: local
mysql_conf:
driver: local
redis_data:
driver: local
upload_data:
driver: local
# 网络
networks:
wecom-network:
driver: bridge

View File

@ -1,316 +0,0 @@
# 客户数据变更追踪系统使用说明
## 一、系统概述
客户数据变更追踪系统是一个用于记录和追踪客户数据变更历史的功能模块。该系统能够:
1. **自动记录数据变更**:每次客户数据发生变化时,自动保存完整的数据快照
2. **版本管理**:为每个客户的数据维护版本号,支持查看历史版本
3. **字段级变更追踪**:详细记录每个字段的变更内容(旧值→新值)
4. **数据指纹识别**通过MD5哈希快速判断数据是否真正发生变化
## 二、系统架构
### 2.1 数据库表结构
系统包含两个核心数据表:
#### 1. customer_export_data_history客户数据历史记录表
- 保存客户数据的每个版本快照
- 包含完整的客户信息字段
- 记录变更类型INSERT/UPDATE/DELETE和变更时间
- 使用数据指纹MD5快速判断数据是否变更
#### 2. customer_data_change_log客户数据变更日志表
- 记录每个字段的具体变更内容
- 包含字段名称、字段标签、旧值、新值
- 关联历史记录表,支持按版本查询变更详情
### 2.2 核心类说明
#### 实体类
- `CustomerExportDataHistory`:历史记录实体类
- `CustomerDataChangeLog`:变更日志实体类
#### Mapper接口
- `CustomerExportDataHistoryMapper`:历史记录数据访问接口
- `CustomerDataChangeLogMapper`:变更日志数据访问接口
#### 服务类
- `CustomerDataTrackingService`:数据变更追踪核心服务
- `trackDataChange()`:追踪数据变更
- `compareAndLogChanges()`:比较数据并记录变更
- `calculateDataFingerprint()`:计算数据指纹
## 三、部署步骤
### 3.1 执行数据库脚本
执行SQL脚本创建数据表
```bash
# 脚本位置
src/main/resources/sql/customer_data_tracking.sql
```
该脚本会创建:
- `customer_export_data_history`
- `customer_data_change_log`
- 为 `customer_export_data` 表的 `customer_name` 字段添加索引
### 3.2 验证Mapper配置
确认以下Mapper XML文件已正确配置
1. `CustomerExportDataMapper.xml`
- 已添加 `selectByCustomerName` 方法
2. `CustomerExportDataHistoryMapper.xml`
- 包含版本查询、历史记录查询等方法
3. `CustomerDataChangeLogMapper.xml`
- 包含变更日志查询、批量插入等方法
### 3.3 配置Spring Bean
确保以下服务类已注册为Spring Bean已使用@Service注解
- `CustomerDataTrackingService`
## 四、使用方法
### 4.1 自动追踪(推荐)
系统已集成到 `CustomerExportService` 中,在以下场景会自动追踪数据变更:
#### 场景1导入Excel数据
```java
// 在 importExcelData() 方法中
// 系统会自动为每条新增或更新的数据创建历史记录
customerExportService.importExcelData(file);
```
**追踪逻辑**
- 新增数据:创建 INSERT 类型的历史记录(版本号=1
- 更新数据:比较新旧数据,如有变化则创建 UPDATE 类型的历史记录(版本号递增)
- 无变化:不创建历史记录(通过数据指纹判断)
### 4.2 手动追踪
如果需要在其他地方手动追踪数据变更:
```java
@Autowired
private CustomerDataTrackingService trackingService;
// 追踪数据变更
trackingService.trackDataChange(newData, "INSERT"); // 新增
trackingService.trackDataChange(newData, "UPDATE"); // 更新
trackingService.trackDataChange(oldData, "DELETE"); // 删除
```
### 4.3 查询历史记录
#### 查询客户的所有历史版本
```java
@Autowired
private CustomerExportDataHistoryMapper historyMapper;
// 查询某个客户的所有历史记录
List<CustomerExportDataHistory> historyList =
historyMapper.selectHistoryByCustomerId(customerId);
```
#### 查询特定版本的数据
```java
// 查询客户的第2个版本
CustomerExportDataHistory history =
historyMapper.selectByCustomerIdAndVersion(customerId, 2);
```
#### 查询最新版本号
```java
// 获取客户的最大版本号
Integer maxVersion =
historyMapper.selectMaxVersionByCustomerId(customerId);
```
### 4.4 查询变更日志
#### 查询客户的所有变更记录
```java
@Autowired
private CustomerDataChangeLogMapper changeLogMapper;
// 查询某个客户的所有变更日志
List<CustomerDataChangeLog> changeLogs =
changeLogMapper.selectChangeLogByCustomerId(customerId);
```
#### 查询特定版本的变更详情
```java
// 查询客户第2个版本的变更内容
List<CustomerDataChangeLog> changeLogs =
changeLogMapper.selectChangeLogByCustomerIdAndVersion(customerId, 2);
```
## 五、数据示例
### 5.1 历史记录示例
| history_id | customer_id | version | change_type | change_time | customer_name | mobile | ... |
|------------|-------------|---------|-------------|-------------|---------------|--------|-----|
| 1 | 100 | 1 | INSERT | 2024-01-01 10:00:00 | 张三 | 13800138000 | ... |
| 2 | 100 | 2 | UPDATE | 2024-01-02 14:30:00 | 张三 | 13900139000 | ... |
| 3 | 100 | 3 | UPDATE | 2024-01-03 09:15:00 | 张三 | 13900139000 | ... |
### 5.2 变更日志示例
| log_id | customer_id | version | field_name | field_label | old_value | new_value | change_time |
|--------|-------------|---------|------------|-------------|-----------|-----------|-------------|
| 1 | 100 | 2 | mobile | 手机号 | 13800138000 | 13900139000 | 2024-01-02 14:30:00 |
| 2 | 100 | 3 | company | 公司名称 | ABC公司 | XYZ公司 | 2024-01-03 09:15:00 |
| 3 | 100 | 3 | position | 职位 | 经理 | 总监 | 2024-01-03 09:15:00 |
## 六、常见查询SQL
### 6.1 查询某个客户的所有历史版本
```sql
SELECT *
FROM customer_export_data_history
WHERE customer_id = 100
ORDER BY version DESC;
```
### 6.2 查询某个客户的所有变更日志
```sql
SELECT *
FROM customer_data_change_log
WHERE customer_id = 100
ORDER BY change_time DESC;
```
### 6.3 查询某个版本的具体变更内容
```sql
SELECT *
FROM customer_data_change_log
WHERE customer_id = 100 AND version = 2;
```
### 6.4 查询最近的变更记录(所有客户)
```sql
SELECT *
FROM customer_data_change_log
ORDER BY change_time DESC
LIMIT 100;
```
### 6.5 统计每个客户的版本数量
```sql
SELECT customer_id, COUNT(*) as version_count
FROM customer_export_data_history
GROUP BY customer_id;
```
### 6.6 查询某个时间段内的所有变更
```sql
SELECT *
FROM customer_data_change_log
WHERE change_time BETWEEN '2024-01-01' AND '2024-01-31'
ORDER BY change_time DESC;
```
## 七、性能优化建议
### 7.1 索引优化
系统已为关键字段创建索引:
- `customer_export_data.customer_name`:用于快速查找客户
- `customer_export_data_history.customer_id`:用于查询历史记录
- `customer_export_data_history.version`:用于版本查询
- `customer_data_change_log.customer_id`:用于查询变更日志
### 7.2 数据指纹机制
- 使用MD5哈希值快速判断数据是否变更
- 避免为未变更的数据创建历史记录
- 减少数据库写入操作
### 7.3 批量操作
- 变更日志支持批量插入(`batchInsert`方法)
- 减少数据库交互次数
## 八、注意事项
### 8.1 数据一致性
- 历史记录和变更日志在同一个事务中创建
- 确保数据的一致性和完整性
### 8.2 存储空间
- 历史记录表会保存完整的数据快照
- 随着时间推移,数据量会持续增长
- 建议定期归档或清理过期数据
### 8.3 性能影响
- 每次数据变更都会触发历史记录创建
- 对于大批量导入,会增加一定的处理时间
- 通过数据指纹机制减少不必要的记录
### 8.4 字段映射
- 确保实体类字段名与数据库字段名一致
- 变更日志中的 `field_label` 使用中文标签,便于阅读
## 九、扩展功能建议
### 9.1 数据回滚
可以基于历史记录实现数据回滚功能:
```java
// 将数据回滚到指定版本
public void rollbackToVersion(Long customerId, Integer version) {
CustomerExportDataHistory history =
historyMapper.selectByCustomerIdAndVersion(customerId, version);
// 将历史数据复制到当前数据表
// ...
}
```
### 9.2 变更统计报表
可以基于变更日志生成统计报表:
- 每日变更数量统计
- 高频变更字段分析
- 用户操作行为分析
### 9.3 变更通知
可以在数据变更时发送通知:
- 邮件通知
- 系统消息通知
- 钉钉/企业微信通知
### 9.4 数据对比界面
可以开发前端界面展示:
- 版本对比视图
- 变更时间线
- 字段变更高亮显示
## 十、故障排查
### 10.1 历史记录未创建
检查项:
1. 数据库表是否正确创建
2. Mapper XML配置是否正确
3. 事务是否正常提交
4. 日志中是否有异常信息
### 10.2 变更日志为空
检查项:
1. 数据是否真正发生变化(通过数据指纹判断)
2. `compareAndLogChanges` 方法是否正常执行
3. 字段比较逻辑是否正确
### 10.3 性能问题
优化方案:
1. 检查索引是否生效
2. 考虑异步处理历史记录创建
3. 批量导入时使用批处理
4. 定期清理过期数据
## 十一、联系支持
如有问题或建议,请联系开发团队。

View File

@ -1,253 +0,0 @@
# 快速开始指南
## 📋 前置条件
1. Java 8 或更高版本
2. MySQL 5.7 或更高版本
3. Maven 3.x
4. 企业微信账号及相关权限
## 🚀 快速部署步骤
### 第一步:获取企业微信配置参数
#### 1.1 获取企业 ID (corp-id)
1. 登录企业微信管理后台https://work.weixin.qq.com/
2. 进入「我的企业」→「企业信息」
3. 复制「企业 ID」
#### 1.2 获取应用密钥 (app-secret)
1. 在企业微信管理后台,进入「应用管理」
2. 选择或创建一个自建应用
3. 在应用详情页面找到「Secret」
4. 点击「查看」并复制 Secret
**重要**:需要给应用授予以下权限:
- 企业微信文档 API 权限
- 文档读取权限
#### 1.3 获取文档 ID (doc-id)
1. 在企业微信中打开目标表格文档
2. 查看浏览器地址栏的 URL
3. URL 格式:`https://doc.weixin.qq.com/sheet/xxxxx`
4. `xxxxx` 部分就是 doc-id
#### 1.4 获取工作表 ID (sheet-id)
**方法一:通过浏览器开发者工具**
1. 打开企业微信文档
2. 按 F12 打开开发者工具
3. 切换到「Network」标签
4. 在文档中切换工作表
5. 查看网络请求,找到包含 `sheet_id` 的请求参数
**方法二:使用默认值**
- 如果是文档的第一个工作表,可以尝试使用 `sheet_id = "1"`
### 第二步:配置数据库
#### 2.1 创建数据库
```sql
CREATE DATABASE IF NOT EXISTS ruoyi DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
```
#### 2.2 执行表结构脚本
```bash
mysql -u root -p ruoyi < RuoYi-Vue/excel-handle/sql/schema.sql
```
或者在 MySQL 客户端中执行:
```sql
USE ruoyi;
SOURCE /path/to/RuoYi-Vue/excel-handle/sql/schema.sql;
```
### 第三步:配置应用参数
编辑配置文件 `RuoYi-Vue/excel-handle/src/main/resources/application.yml`
```yaml
# 企业微信配置
wecom:
# 企业 ID必填
corp-id: "ww1234567890abcdef" # 替换为你的企业 ID
# 应用密钥(必填)
app-secret: "your_secret_here" # 替换为你的应用 Secret
# 文档 ID必填
doc-id: "your_doc_id_here" # 替换为你的文档 ID
# 工作表 ID必填
sheet-id: "1" # 替换为你的工作表 ID
# 查询范围必填A1 表示法)
range: "A1:AE1000" # 根据实际表格列数和行数调整
# 抓取间隔(可选,默认 60 分钟)
fetch-interval: 60
# 数据库配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/ruoyi?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root # 替换为你的数据库用户名
password: password # 替换为你的数据库密码
```
### 第四步:确认表格格式
确保企业微信表格的列顺序与以下格式一致:
| 列号 | 字段名 | 说明 |
|------|--------|------|
| A | 客户名称 | 必填 |
| B | 描述 | 可选 |
| C | 添加人 | 可选 |
| D | 添加人账号 | 必填 |
| E | 添加人所属部门 | 可选 |
| F | 添加时间 | 格式yyyy-MM-dd |
| G | 来源 | 可选 |
| H | 手机 | 可选 |
| I | 企业 | 可选 |
| J | 邮箱 | 可选 |
| K | 地址 | 可选 |
| L | 职务 | 可选 |
| M | 电话 | 可选 |
| N-AE | 标签组1-18 | 可选 |
**注意**:第一行必须是表头,数据从第二行开始。
### 第五步:编译和运行
#### 5.1 编译项目
```bash
cd RuoYi-Vue
mvn clean package -DskipTests
```
#### 5.2 运行项目
```bash
cd ruoyi-admin/target
java -jar ruoyi-admin.jar
```
或者在 IDE 中直接运行 `RuoYiApplication.java`
### 第六步:验证功能
#### 6.1 查看日志
启动后查看日志,确认定时任务是否正常执行:
```
开始执行企业微信表格数据同步任务
获取企业微信access_token成功
获取企业微信表格数据成功,行数: 100
数据处理统计: 总计 99 条,新数据 99 条,当天已处理跳过 0 条,无效数据 0 条
企业微信表格数据同步任务执行完成
```
#### 6.2 手动触发同步
使用 API 工具(如 Postman或 curl 命令:
```bash
curl -X POST http://localhost:8080/wecom/table/sync
```
#### 6.3 查看服务状态
```bash
curl -X GET http://localhost:8080/wecom/table/status
```
#### 6.4 查询数据库
```sql
-- 查看客户数据
SELECT * FROM customer_data ORDER BY create_time DESC LIMIT 10;
-- 查看处理记录
SELECT * FROM processed_data_record ORDER BY create_time DESC LIMIT 10;
-- 查看统计数据
SELECT * FROM customer_tag_statistics ORDER BY stat_date DESC, tag_count DESC;
```
## 🔧 常见问题
### Q1: 获取 access_token 失败
**错误信息**: `获取企业微信access_token失败: 40013 - invalid corpid`
**解决方法**:
- 检查 `corp-id` 是否正确
- 检查 `app-secret` 是否正确
- 确认应用未被停用
### Q2: 获取表格数据失败
**错误信息**: `获取企业微信表格数据失败: 40003 - invalid openid`
**解决方法**:
- 检查 `doc-id` 是否正确
- 检查 `sheet-id` 是否正确
- 确认应用有文档访问权限
- 确认文档未被删除或移动
### Q3: 数据解析失败
**错误信息**: `转换表格数据失败`
**解决方法**:
- 检查表格列的顺序是否与代码映射一致
- 检查日期格式是否为 `yyyy-MM-dd`
- 检查表格是否有空行或格式异常
### Q4: 定时任务不执行
**解决方法**:
- 检查 Spring Boot 是否启用了定时任务:`@EnableScheduling`
- 查看日志是否有异常信息
- 检查定时任务配置是否正确
### Q5: 数据重复
**解决方法**:
- 检查去重逻辑是否正常工作
- 查看 `processed_data_record` 表是否有记录
- 检查数据的 `customer_name``add_person_account` 是否为空
## 📊 监控和维护
### 日志位置
- 应用日志:`logs/ruoyi-admin.log`
- 错误日志:`logs/ruoyi-admin-error.log`
### 定时任务时间
- 默认:每小时整点执行一次
- 修改:编辑 `WecomTableSyncTask.java` 中的 `@Scheduled` 注解
### 数据清理
系统会自动清理非当天的缓存数据,但数据库中的历史记录会保留。
如需清理历史数据:
```sql
-- 清理 30 天前的处理记录
DELETE FROM processed_data_record WHERE create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
-- 清理 90 天前的统计数据
DELETE FROM customer_tag_statistics WHERE create_time < DATE_SUB(NOW(), INTERVAL 90 DAY);
```
## 🎯 下一步
1. 根据业务需求调整表格字段映射
2. 自定义标签统计规则(实现 `TagValidator` 接口)
3. 添加数据导出功能
4. 配置告警通知(邮件、企业微信消息等)
5. 优化性能(批量处理、异步处理等)
## 📚 相关文档
- [完整文档](README.md)
- [企业微信 API 文档](https://developer.work.weixin.qq.com/document/)
- [RuoYi 框架文档](http://doc.ruoyi.vip)
## 💡 技术支持
如遇到问题,请:
1. 查看日志文件获取详细错误信息
2. 参考常见问题部分
3. 查阅企业微信 API 文档
4. 提交 Issue 或联系技术支持

View File

@ -59,6 +59,10 @@
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
</dependencies>

View File

@ -5,12 +5,26 @@
-- =============================================
-- 1. 企业组织架构相关表
-- =============================================
-- 企业部门表
DROP TABLE IF EXISTS `corp_info`;
CREATE TABLE `corp_info` (
`id` BIGINT(20) NOT NULL COMMENT '部门ID企业微信部门ID',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业微信公司id',
`secret` BIGINT(20) DEFAULT NULL COMMENT '企业微信秘钥',
`name` BIGINT(20) DEFAULT NULL COMMENT '企业名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业信息';
-- 企业部门表
DROP TABLE IF EXISTS `corp_department`;
CREATE TABLE `corp_department` (
`id` BIGINT(20) NOT NULL COMMENT '部门ID企业微信部门ID',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业id',
`parentid` BIGINT(20) DEFAULT NULL COMMENT '父部门ID',
`order_no` BIGINT(20) DEFAULT NULL COMMENT '部门排序',
`name` VARCHAR(200) NOT NULL COMMENT '部门名称',
@ -27,6 +41,8 @@ DROP TABLE IF EXISTS `corp_user`;
CREATE TABLE `corp_user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业id',
`userid` VARCHAR(100) NOT NULL COMMENT '员工ID企业微信userid',
`depart_id` BIGINT(20) DEFAULT NULL COMMENT '主部门ID',
`department_ids` VARCHAR(500) DEFAULT NULL COMMENT '所属部门ID列表JSON格式',
@ -54,8 +70,11 @@ CREATE TABLE `corp_user` (
DROP TABLE IF EXISTS `customer_export_data`;
CREATE TABLE `customer_export_data` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`customer_name` VARCHAR(100) DEFAULT NULL COMMENT '客户姓名',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业id',
`description` TEXT DEFAULT NULL COMMENT '描述/备注',
`gender` INT(11) DEFAULT NULL COMMENT '性别',
`add_user_name` VARCHAR(100) DEFAULT NULL COMMENT '添加人姓名',
@ -103,7 +122,10 @@ CREATE TABLE `customer_export_data` (
DROP TABLE IF EXISTS `customer_export_data_history`;
CREATE TABLE `customer_export_data_history` (
`history_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '历史记录主键ID',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业id',
`customer_id` BIGINT(20) NOT NULL COMMENT '关联的客户数据ID',
`version` INT(11) NOT NULL DEFAULT 1 COMMENT '数据版本号',
`data_fingerprint` VARCHAR(64) DEFAULT NULL COMMENT '数据指纹(MD5)',
@ -156,6 +178,8 @@ DROP TABLE IF EXISTS `customer_data_change_log`;
CREATE TABLE `customer_data_change_log` (
`log_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键ID',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业id',
`history_id` BIGINT(20) NOT NULL COMMENT '关联的历史记录ID',
`customer_id` BIGINT(20) NOT NULL COMMENT '关联的客户数据ID',
`field_name` VARCHAR(100) NOT NULL COMMENT '变更字段名称(英文)',
@ -182,6 +206,8 @@ DROP TABLE IF EXISTS `customer_contact_data`;
CREATE TABLE `customer_contact_data` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业id',
`stat_time` BIGINT(20) DEFAULT NULL COMMENT '统计时间戳(秒)',
`stat_date` DATE DEFAULT NULL COMMENT '统计日期',
`chat_cnt` INT(11) DEFAULT 0 COMMENT '聊天次数',
@ -207,6 +233,8 @@ DROP TABLE IF EXISTS `customer_statistics_data`;
CREATE TABLE `customer_statistics_data` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业id',
`cur_date` DATE DEFAULT NULL COMMENT '统计日期',
`indicator_name` VARCHAR(100) DEFAULT NULL COMMENT '重要指标名称',
`ntf_group` VARCHAR(50) DEFAULT NULL COMMENT 'N组(投放)',
@ -235,6 +263,8 @@ DROP TABLE IF EXISTS `department_statistics_data`;
CREATE TABLE `department_statistics_data` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`corp_id` VARCHAR(200) DEFAULT NULL COMMENT '企业id',
`stat_date` DATE NOT NULL COMMENT '统计日期',
`department_path` VARCHAR(500) NOT NULL COMMENT '部门路径(完整层级路径)',
`daily_total_accepted` INT(11) DEFAULT 0 COMMENT '当日总承接数',

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

@ -1,131 +0,0 @@
-- ========================================
-- 企业微信表格数据处理系统 - 数据库表结构
-- ========================================
-- 1. 客户数据表
DROP TABLE IF EXISTS `customer_data`;
CREATE TABLE `customer_data` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`customer_name` varchar(100) DEFAULT NULL COMMENT '客户名称',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`add_person` varchar(50) DEFAULT NULL COMMENT '添加人',
`add_person_account` varchar(50) DEFAULT NULL COMMENT '添加人账号',
`add_person_dept` varchar(100) DEFAULT NULL COMMENT '添加人所属部门',
`add_time` datetime DEFAULT NULL COMMENT '添加时间',
`add_date` date DEFAULT NULL COMMENT '添加日期',
`source` varchar(50) DEFAULT NULL COMMENT '来源',
`mobile` varchar(20) DEFAULT NULL COMMENT '手机',
`enterprise` varchar(100) DEFAULT NULL COMMENT '企业',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`address` varchar(200) DEFAULT NULL COMMENT '地址',
`position` varchar(50) DEFAULT NULL COMMENT '职务',
`phone` varchar(20) DEFAULT NULL COMMENT '电话',
`tag_group_1` varchar(50) DEFAULT NULL COMMENT '标签组1(投放)',
`tag_group_2` varchar(50) DEFAULT NULL COMMENT '标签组2(公司孵化)',
`tag_group_3` varchar(50) DEFAULT NULL COMMENT '标签组3(商务)',
`tag_group_4` varchar(50) DEFAULT NULL COMMENT '标签组4(成交日期)',
`tag_group_5` varchar(50) DEFAULT NULL COMMENT '标签组5(年级组)',
`tag_group_6` varchar(50) DEFAULT NULL COMMENT '标签组6(客户属性)',
`tag_group_7` varchar(50) DEFAULT NULL COMMENT '标签组7(成交)',
`tag_group_8` varchar(50) DEFAULT NULL COMMENT '标签组8(成交品牌)',
`tag_group_9` varchar(50) DEFAULT NULL COMMENT '标签组9(线索通标签)',
`tag_group_10` varchar(50) DEFAULT NULL COMMENT '标签组10(A1组)',
`tag_group_11` varchar(50) DEFAULT NULL COMMENT '标签组11(B1组)',
`tag_group_12` varchar(50) DEFAULT NULL COMMENT '标签组12(C1组)',
`tag_group_13` varchar(50) DEFAULT NULL COMMENT '标签组13(D1组)',
`tag_group_14` varchar(50) DEFAULT NULL COMMENT '标签组14(E1组)',
`tag_group_15` varchar(50) DEFAULT NULL COMMENT '标签组15(意向度)',
`tag_group_16` varchar(50) DEFAULT NULL COMMENT '标签组16(自然流)',
`tag_group_17` varchar(50) DEFAULT NULL COMMENT '标签组17(F1组)',
`tag_group_18` varchar(50) DEFAULT NULL COMMENT '标签组18(G1组)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_customer_name` (`customer_name`),
KEY `idx_add_person_account` (`add_person_account`),
KEY `idx_add_time` (`add_time`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户数据表';
-- 2. 已处理数据记录表(用于去重)
DROP TABLE IF EXISTS `processed_data_record`;
CREATE TABLE `processed_data_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`customer_name` varchar(100) NOT NULL COMMENT '客户名称',
`add_person_account` varchar(50) NOT NULL COMMENT '添加人账号',
`data_hash` varchar(64) NOT NULL COMMENT '数据哈希值(MD5)',
`processing_status` varchar(20) DEFAULT 'PROCESSED' COMMENT '处理状态',
`process_time` datetime DEFAULT NULL 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_customer_account_hash` (`customer_name`, `add_person_account`, `data_hash`),
KEY `idx_process_time` (`process_time`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='已处理数据记录表';
-- 3. 客户标签统计表
DROP TABLE IF EXISTS `customer_tag_statistics`;
CREATE TABLE `customer_tag_statistics` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`stat_date` varchar(10) NOT NULL COMMENT '统计日期(yyyy-MM-dd)',
`tag_name` varchar(100) NOT NULL COMMENT '标签名称',
`tag_count` int(11) 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_date_tag` (`stat_date`, `tag_name`),
KEY `idx_stat_date` (`stat_date`),
KEY `idx_tag_name` (`tag_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户标签统计表';
-- 4. 客户统计数据表(标签组维度)
DROP TABLE IF EXISTS `customer_statistics_data`;
CREATE TABLE `customer_statistics_data` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`cur_date` date NULL DEFAULT NULL COMMENT '统计日期',
`indicator_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '重要指标',
`ntf_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'N组',
`ofh_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'O组(公司孵化)',
`psw_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'P组(商务)',
`wa1_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'W组(A1组)',
`xb1_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'X组(B1组)',
`yc1_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'Y组(C1组)',
`zd1_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'Z组(D1组)',
`aa_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'AA组(E1组)',
`ac_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'AC组(自然流)',
`ad_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'AD组(F1组)',
`ae_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'AE组(G1组)',
`sort_no` int NULL DEFAULT NULL COMMENT '排序',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_date_indicator`(`cur_date` ASC, `indicator_name` ASC) USING BTREE,
INDEX `idx_cur_date`(`cur_date` ASC) USING BTREE,
INDEX `idx_indicator_name`(`indicator_name` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2301 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '客户统计数据表(标签组维度)' ROW_FORMAT = Dynamic;
-- 5. 部门统计数据表
DROP TABLE IF EXISTS `department_statistics_data`;
CREATE TABLE `department_statistics_data` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`stat_date` date NOT NULL COMMENT '统计日期',
`department_path` varchar(200) NOT NULL COMMENT '部门路径(如:苏州曼普/销售部/一组 或 苏州曼普/销售部/一组/盛宇婷)',
`indicator_name` varchar(100) NOT NULL COMMENT '指标名称',
`indicator_value` varchar(50) DEFAULT NULL 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_date_dept_indicator` (`stat_date`, `department_path`, `indicator_name`),
KEY `idx_stat_date` (`stat_date`),
KEY `idx_department_path` (`department_path`),
KEY `idx_indicator_name` (`indicator_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门统计数据表';
-- ========================================
-- 初始化数据(可选)
-- ========================================
-- 插入测试数据示例(可根据需要删除)
-- INSERT INTO `customer_data` (`customer_name`, `add_person_account`, `add_time`)
-- VALUES ('测试客户', 'test_account', NOW());

View File

@ -1,73 +1,94 @@
-- 菜单 SQL
-- 企业微信统计管理父菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('企业微信统计', 0, 5, 'wecom', NULL, 1, 0, 'M', '0', '0', '', 'chart', 'admin', sysdate(), '', NULL, '企业微信统计管理目录');
-- 获取刚插入的父菜单ID假设为2000实际使用时需要查询获取
SELECT @parent_id := menu_id FROM sys_menu WHERE menu_name = '企业微信统计' AND parent_id = 0;
-- 客户导出数据菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('客户列表数据', @parent_id, 4, 'customerExport', 'wecom/customerExport/index', 1, 0, 'C', '0', '0', 'wecom:customerExport:list', 'download', 'admin', sysdate(), '', NULL, '客户导出数据菜单');
-- 客户联系统计数据菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('客户联系统计', @parent_id, 2, 'customerContact', 'wecom/customerContact/index', 1, 0, 'C', '0', '0', 'wecom:customerContact:list', 'peoples', 'admin', sysdate(), '', NULL, '客户联系统计数据菜单');
-- 客户统计数据菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('流量看板数据', @parent_id, 1, 'customerStatistics', 'wecom/customerStatistics/index', 1, 0, 'C', '0', '0', 'wecom:customerStatistics:list', 'table', 'admin', sysdate(), '', NULL, '流量看板数据菜单');
-- 部门统计数据菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('销售看板数据', @parent_id, 3, 'departmentStatistics', 'wecom/departmentStatistics/index', 1, 0, 'C', '0', '0', 'wecom:departmentStatistics:list', 'tree-table', 'admin', sysdate(), '', NULL, '销售看板数据菜单');
-- 获取刚插入的父菜单ID假设为2000实际使用时需要查询获取
SELECT @parent_id := menu_id FROM sys_menu WHERE menu_name = '流量看板数据' AND parent_id = 0;
-- 客户统计数据查询按钮
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('流量看板数据查询', @parent_id, 1, '#', '', 1, 0, 'F', '0', '0', 'wecom:customerStatistics:query', '#', 'admin', sysdate(), '', NULL, '');
-- 客户统计数据导出按钮
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('流量看板数据导出', @parent_id, 2, '#', '', 1, 0, 'F', '0', '0', 'wecom:customerStatistics:export', '#', 'admin', sysdate(), '', NULL, '');
-- 获取刚插入的父菜单ID假设为2000实际使用时需要查询获取
SELECT @parent_id := menu_id FROM sys_menu WHERE menu_name = '客户联系统计' AND parent_id = 0;
-- 客户联系统计数据查询按钮
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('客户联系统计查询', @parent_id, 1, '#', '', 1, 0, 'F', '0', '0', 'wecom:customerContact:query', '#', 'admin', sysdate(), '', NULL, '');
-- 客户联系统计数据导出按钮
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('客户联系统计导出', @parent_id, 2, '#', '', 1, 0, 'F', '0', '0', 'wecom:customerContact:export', '#', 'admin', sysdate(), '', NULL, '');
-- 获取刚插入的父菜单ID假设为2000实际使用时需要查询获取
SELECT @parent_id := menu_id FROM sys_menu WHERE menu_name = '销售看板数据' AND parent_id = 0;
-- 部门统计数据查询按钮
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('销售看板数据查询', @parent_id, 1, '#', '', 1, 0, 'F', '0', '0', 'wecom:departmentStatistics:query', '#', 'admin', sysdate(), '', NULL, '');
-- 部门统计数据导出按钮
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('销售看板数据导出', @parent_id, 2, '#', '', 1, 0, 'F', '0', '0', 'wecom:departmentStatistics:export', '#', 'admin', sysdate(), '', NULL, '');
-- 获取刚插入的父菜单ID假设为2000实际使用时需要查询获取
SELECT @parent_id := menu_id FROM sys_menu WHERE menu_name = '客户列表数据' AND parent_id = 0;
-- 客户导出数据查询按钮
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('客户列表数据查询', @parent_id, 1, '#', '', 1, 0, 'F', '0', '0', 'wecom:customerExport:query', '#', 'admin', sysdate(), '', NULL, '');
-- 客户导出数据导出按钮
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('客户列表数据导出', @parent_id, 2, '#', '', 1, 0, 'F', '0', '0', 'wecom:customerExport:export', '#', 'admin', sysdate(), '', NULL, '');
INSERT INTO `excel-handle`.`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 (2000, '''', 0, 5, ''wecom'', NULL, NULL, '''', 1, 0, ''M'', ''0 '', ''0 '', '''', ''chart'', ''admin'',
''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2001, '''', 2000, 4, ''customerExport'', ''wecom/customerExport/index'', NULL, '''', 1, 0, ''C'', ''0 '',
''0 '', ''wecom:customerExport:list'', ''download'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL,
'''');
INSERT INTO `excel-handle`.`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 (2002, '''', 2000, 2, ''customerContact'', ''wecom/customerContact/index'', NULL, '''', 1, 0, ''C'',
''0 '', ''0 '', ''wecom:customerContact:list'', ''peoples'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL,
'''');
INSERT INTO `excel-handle`.`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 (2003, '''', 2000, 1, ''customerStatistics'', ''wecom/customerStatistics/index'', NULL, '''', 1, 0, ''C'',
''0 '', ''0 '', ''wecom:customerStatistics:list'', ''table'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''',
NULL, '''');
INSERT INTO `excel-handle`.`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 (2004, '''', 2000, 3, ''departmentStatistics'', ''wecom/departmentStatistics/index'', NULL, '''', 1, 0,
''C'', ''0 '', ''0 '', ''wecom:departmentStatistics:list'', ''tree-table'', ''admin'',
''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2005, '''', 2003, 1, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '',
''wecom:customerStatistics:query'', ''#'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2006, '''', 2003, 2, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '',
''wecom:customerStatistics:export'', ''#'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2007, '''', 2002, 1, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '',
''wecom:customerContact:query'', ''#'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2008, '''', 2002, 2, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '',
''wecom:customerContact:export'', ''#'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2009, '''', 2004, 1, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '',
''wecom:departmentStatistics:query'', ''#'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2010, '''', 2004, 2, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '',
''wecom:departmentStatistics:export'', ''#'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2011, '''', 2001, 1, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '',
''wecom:customerExport:query'', ''#'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (2012, '''', 2001, 2, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '',
''wecom:customerExport:export'', ''#'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (3000, '''', 2000, 4, ''corpInfo'', ''wecom/corpInfo/index'', NULL, '''', 1, 0, ''C'', ''0 '', ''0 '',
''wecom:corpInfo:list'', ''download'', ''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (3008, '''', 3000, 2, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '', ''wecom:corp:add'', ''#'',
''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (3009, '''', 3000, 2, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '', ''wecom:corp:query'', ''#'',
''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (3010, '''', 3000, 2, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '', ''wecom:corp:edit'', ''#'',
''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');
INSERT INTO `excel-handle`.`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 (3011, '''', 3000, 2, ''#'', '''', NULL, '''', 1, 0, ''F'', ''0 '', ''0 '', ''wecom:corp:remove'', ''#'',
''admin'', ''2026 - 02 - 07 15:39:03'', '''', NULL, '''');

View File

@ -6,15 +6,26 @@ import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
@Getter
@Setter
@Data
@TableName("corp_department")
public class CorpDepartment implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String corpId;
private Long parentid;
private Long orderNo;
private String name;
private Date createTime;
private Date updateTime;
}

View File

@ -0,0 +1,19 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
@Data
@TableName("corp_info")
public class CorpInfo implements Serializable {
private Long id;
private String corpId;
private String secret;
private String name;
}

View File

@ -6,6 +6,7 @@ import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
@Getter
@Setter
@ -14,6 +15,7 @@ import java.io.Serializable;
@TableName("corp_user")
public class CorpUser implements Serializable {
private Long id;
private String corpId;
private String userid;
private Long departId;
private String departmentIds;
@ -25,4 +27,8 @@ public class CorpUser implements Serializable {
private String alias;
private String openUserid;
private Date createTime;
private Date updateTime;
}

View File

@ -18,6 +18,7 @@ public class CustomerContactData implements Serializable {
/**
* 主键ID
*/
private String corpId;
private Long id;
/**

View File

@ -20,6 +20,8 @@ public class CustomerDataChangeLog implements Serializable {
/**
* 日志主键ID
*/
private String corpId;
@TableId(type = IdType.AUTO)
private Long logId;

View File

@ -22,6 +22,10 @@ public class CustomerExportData implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String corpId;
//客户唯一id
private String customerUserId;
/**
* 客户名称
*/
@ -62,6 +66,9 @@ public class CustomerExportData implements Serializable {
*/
private Date addDate;
//成交日期
private Date finishDate;
/**
* 来源
*/

View File

@ -23,6 +23,7 @@ public class CustomerExportDataHistory implements Serializable {
@TableId(type = IdType.AUTO)
private Long historyId;
private String corpId;
/**
* 关联的客户数据IDcustomer_export_data表的主键
*/

View File

@ -22,6 +22,8 @@ public class CustomerStatisticsData implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String corpId;
private Date curDate;
@ExcelProperty("重要指标")
private String indicatorName;
@ -61,4 +63,7 @@ public class CustomerStatisticsData implements Serializable {
private int sortNo;
//是否是隐藏数据
private Boolean hiddenFlag = false;
}

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

@ -24,6 +24,7 @@ public class DepartmentStatisticsData implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String corpId;
/**
* 统计日期
*/
@ -40,17 +41,53 @@ public class DepartmentStatisticsData implements Serializable {
//非及时单占比当日
private BigDecimal dailyNonTimelyOrderRatio;
/* //即时单
//即时单
private Integer dailyTimelyCount;
//非即时单
private Integer dailyNonTimelyCount;*/
private Integer dailyNonTimelyCount;
//管理员分配当日
private Integer managerAccepted;
/**
* 部门路径苏州曼普/销售部/一组 苏州曼普/销售部/一组/盛宇婷
*/
//新增家长出单率当日
private BigDecimal dailyParentOrderRate;
//新增学生出单率当日
private BigDecimal dailyStudentOrderRate;
//如果是四级的 默认是个人 前端显示时也是默认显示个人
private Boolean personFlag;
private String departmentPath;
//公司名称
private String corpName;
//部门名称
private String departmentName;
//组名称
private String groupName;
//个人名称
private String personName;
// ========== 新增家长/学生出单率统计当日 ==========
/**
* 家长出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int parentDailyOrderCount = 0;
/**
* 家长总数当日进粉- 用于出单率分母
*/
private int parentDailyCount = 0;
/**
* 学生出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int studentDailyOrderCount = 0;
/**
* 学生总数当日进粉- 用于出单率分母
*/
private int studentDailyCount = 0;
/**
* 创建时间

View File

@ -22,6 +22,8 @@ public class WecomTagDomain implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String corpId;
/**
* 标签ID
*/

View File

@ -22,6 +22,8 @@ public class WecomTagGroupDomain implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String corpId;
/**
* 标签组ID
*/

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

@ -0,0 +1,14 @@
package com.ruoyi.excel.wecom.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class CorpDepartmentDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String corpId;
private String name;
}

View File

@ -55,5 +55,26 @@ public class DepartmentStatisticsAccumulator {
* "由管理员XXX分配"
* */
private int managerAcceptCount = 0;
// ========== 新增家长/学生出单率统计当日 ==========
/**
* 家长出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int parentDailyOrderCount = 0;
/**
* 家长总数当日进粉- 用于出单率分母
*/
private int parentDailyCount = 0;
/**
* 学生出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int studentDailyOrderCount = 0;
/**
* 学生总数当日进粉- 用于出单率分母
*/
private int studentDailyCount = 0;
}
}

View File

@ -0,0 +1,814 @@
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 String UNGROUPED_NAME = "未分组";
/**
* 组名到字段名的映射使用括号内的名称
*/
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) {
boolean hasGroup = false;
// 遍历所有组
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;
}
hasGroup = true;
// 获取该组的统计器
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);
}
}
// 如果不属于任何组统计到"未分组"
if (!hasGroup) {
GroupStatistics ungroupedStats = accumulator.getGroupStats(UNGROUPED_NAME);
accumulateGroupStatistics(data, date, ungroupedStats);
}
}
/**
* 使用反射获取字段值带缓存
*/
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) {
if (orderStatus.equals("已成交及时单9元+")) {
stats.setTimelyOrderCount(stats.getTimelyOrderCount() + 1);
} else if (orderStatus.equals("已成交非及时单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;
// 1. 生成组级数据 - 遍历所有预定义的组确保有成单但无进粉的组也被统计
Map<String, Long> groupIdMap = new HashMap<>();
for (Map.Entry<String, String> groupEntry : GROUP_FIELD_MAP.entrySet()) {
String groupName = groupEntry.getKey();
String attr = GROUP_ATTR_MAP.get(groupName);
// 获取或创建该组的统计器
GroupStatistics stats = accumulator.getGroupStats(groupName);
// 使用SQL查询获取成单数与V1保持一致
Long finishCount = customerExportDataMapper.selectByFinishDate(
corpId, date, attr, finishFlag);
stats.setOrderCount(finishCount.intValue());
// 使用SQL查询获取及时单数量根据成交日期和订单状态
Long timelyCount = customerExportDataMapper.selectTimelyOrderCount(
corpId, date, attr, timelyFinishFlag);
stats.setTimelyOrderCount(timelyCount.intValue());
// 使用SQL查询获取非及时单数量根据成交日期和订单状态
Long nonTimelyCount = customerExportDataMapper.selectNonTimelyOrderCount(
corpId, date, attr, noTimelyfinishFlag);
stats.setNonTimelyOrderCount(nonTimelyCount.intValue());
// 只有当有成单数或进粉数时才生成记录
if (stats.getOrderCount() > 0 || stats.getCustomerCount() > 0) {
CustomerStatisticsDataV2 groupData = createStatisticsDataV2(
corpId, date, groupName, null, stats, sortNo++);
groupData.setDataLevel(1);
groupData.setId(tempId);
result.add(groupData);
groupIdMap.put(groupName, tempId);
tempId++;
}
}
// 2. 处理"未分组"数据
GroupStatistics ungroupedStats = accumulator.getGroupStats(UNGROUPED_NAME);
// 查询未分组记录的成单数
Long ungroupedFinishCount = customerExportDataMapper.selectUngroupedFinishCount(
corpId, date, finishFlag);
ungroupedStats.setOrderCount(ungroupedFinishCount.intValue());
// 查询未分组记录的及时单数量
Long ungroupedTimelyCount = customerExportDataMapper.selectUngroupedTimelyOrderCount(
corpId, date, timelyFinishFlag);
ungroupedStats.setTimelyOrderCount(ungroupedTimelyCount.intValue());
// 查询未分组记录的非及时单数量
Long ungroupedNonTimelyCount = customerExportDataMapper.selectUngroupedNonTimelyOrderCount(
corpId, date, noTimelyfinishFlag);
ungroupedStats.setNonTimelyOrderCount(ungroupedNonTimelyCount.intValue());
// 只有当有成单数或进粉数时才生成记录
if (ungroupedStats.getOrderCount() > 0 || ungroupedStats.getCustomerCount() > 0) {
CustomerStatisticsDataV2 ungroupedData = createStatisticsDataV2(
corpId, date, UNGROUPED_NAME, null, ungroupedStats, sortNo++);
ungroupedData.setDataLevel(1);
ungroupedData.setId(tempId);
result.add(ungroupedData);
tempId++;
}
// 3. 生成标签级数据
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);
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;
}
/**
* 检查数据来源是否匹配
* 排除"由管理员XXX分配"的记录
*/
private boolean matchesSource(CustomerExportData data) {
if(data == null || data.getSource() == null) {
return true;
}
if (data.getSource().contains("管理员") &&
data.getSource().contains("分配")) {
return false;
}
return true;
}
/**
* 检查成交日期是否匹配使用finishDate字段
*/
private boolean matchesQValue(CustomerExportData data, Date curDate) {
Date finishDate = data.getFinishDate();
if (finishDate == null) {
return false;
}
try {
Calendar orderCal = Calendar.getInstance();
orderCal.setTime(finishDate);
orderCal.set(Calendar.HOUR_OF_DAY, 0);
orderCal.set(Calendar.MINUTE, 0);
orderCal.set(Calendar.SECOND, 0);
orderCal.set(Calendar.MILLISECOND, 0);
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;
}
return orderStatus.equals("已成交及时单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

@ -78,6 +78,10 @@ public class StatisticsAccumulator {
* 学生数
*/
private int studentCount = 0;
/**
* 老师
*/
private int teacherCount = 0;
/**
* 未知空白
@ -141,6 +145,53 @@ public class StatisticsAccumulator {
* 学生出单数
*/
private int studentOrderCount = 0;
/**
* 老师出单数
*
*/
private int teacherOrderCount = 0;
private int unkownOrderCount = 0;
// ========== 新增家长/学生出单率统计当日 ==========
/**
* 家长出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int parentDailyOrderCount = 0;
/**
* 家长总数当日进粉- 用于出单率分母
*/
private int parentDailyCount = 0;
/**
* 学生出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int studentDailyOrderCount = 0;
/**
* 学生总数当日进粉- 用于出单率分母
*/
private int studentDailyCount = 0;
/**
* 老师总数当日进粉- 用于出单率分母
*/
private int teacherDailyCount = 0;
/**
* 未知总数当日进粉- 用于出单率分母
*/
private int unkownDailyCount = 0;
/**
* 学生出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int teacherDailyOrderCount = 0;
/**
* 学生出单数当日成交- 满足Q列=当日 AND T列=已成交及时单9元+
*/
private int unkownDailyOrderCount = 0;
// ========== 成本数据 ==========
/**

View File

@ -2,15 +2,14 @@ package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CorpDepartment;
import com.ruoyi.excel.wecom.domain.CorpUser;
import com.ruoyi.excel.wecom.dto.CorpDepartmentDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 部门mapper
*/
@Mapper
public interface CorpDepartmentMapper extends BaseMapper<CorpDepartment> {
List<CorpDepartment> selectCorpDepartmentList(@Param("dto") CorpDepartmentDTO dto);
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CorpInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 企业信息Mapper接口
*/
@Mapper
public interface CorpInfoMapper extends BaseMapper<CorpInfo> {
/**
* 查询企业信息列表
*
* @param corpInfo 企业信息
* @return 企业信息集合
*/
List<CorpInfo> selectCorpInfoList(CorpInfo corpInfo);
/**
* 根据企业ID查询企业信息
*
* @param corpId 企业ID
* @return 企业信息
*/
CorpInfo selectCorpInfoByCorpId(String corpId);
}

View File

@ -2,8 +2,8 @@ package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CorpUser;
import com.ruoyi.excel.wecom.domain.WecomTagDomain;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@ -13,6 +13,8 @@ import java.util.List;
@Mapper
public interface CorpUserMapper extends BaseMapper<CorpUser> {
List<String> selectAllUserIds();
List<String> selectAllUserIds(String corpId);
CorpUser selectByUserId(@Param("userid") String userid);
}

View File

@ -21,7 +21,7 @@ public interface CustomerContactDataMapper extends BaseMapper<CustomerContactDat
* @param statDate 统计日期
* @return 客户联系统计数据列表
*/
List<CustomerContactData> selectByStatDate(Date statDate);
List<CustomerContactData> selectByStatDate(@Param("corpId") String corpId,@Param("statDate")Date statDate);
/**
* 根据成员ID和日期查询客户联系统计数据
@ -30,7 +30,7 @@ public interface CustomerContactDataMapper extends BaseMapper<CustomerContactDat
* @param statDate 统计日期
* @return 客户联系统计数据列表
*/
List<CustomerContactData> selectByUseridAndStatDate(String userid, Date statDate);
List<CustomerContactData> selectByUseridAndStatDate(@Param("corpId")String corpId,@Param("userid")String userid, @Param("statDate")Date statDate);
/**
* 查询客户联系统计数据列表
@ -40,6 +40,7 @@ public interface CustomerContactDataMapper extends BaseMapper<CustomerContactDat
* @return 客户联系统计数据列表
*/
List<CustomerContactData> selectCustomerContactDataList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("userid") String userid
@ -53,6 +54,7 @@ public interface CustomerContactDataMapper extends BaseMapper<CustomerContactDat
* @return 客户联系统计数据VO列表
*/
List<CustomerContactDataVO> selectCustomerContactDataVOList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("userid") String userid

View File

@ -8,13 +8,14 @@ import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 客户导出数据Mapper
*/
@Mapper
public interface CustomerExportDataMapper extends BaseMapper<CustomerExportData> {
List<Date> getDistinctDate();
List<Date> getDistinctDate( @Param("corpId") String corpId);
/**
* 查询客户导出数据列表
@ -24,6 +25,7 @@ public interface CustomerExportDataMapper extends BaseMapper<CustomerExportData>
* @return 客户导出数据列表
*/
List<CustomerExportData> selectCustomerExportDataList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("customerName") String customerName
@ -37,6 +39,7 @@ public interface CustomerExportDataMapper extends BaseMapper<CustomerExportData>
* @return 客户导出数据VO列表
*/
List<CustomerExportDataVO> selectCustomerExportDataVOList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("customerName") String customerName
@ -50,8 +53,228 @@ public interface CustomerExportDataMapper extends BaseMapper<CustomerExportData>
* @return 客户导出数据
*/
CustomerExportData selectByUniqueKey(
@Param("customerName") String customerName,
@Param("corpId") String corpId,
@Param("customerUserId") String customerUserId,
@Param("addUserAccount") String addUserAccount,
@Param("addTime") Date addTime
);
Long selectByFinishDate(
@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数量(用于异步导出)
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称(可选)
* @return 数据总数
*/
int countCustomerExportDataVOList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("customerName") String customerName
);
/**
* 分页查询客户导出数据VO列表(用于异步导出)
* @param corpId 企业ID
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称(可选)
* @param offset 偏移量
* @param limit 每页数量
* @return 客户导出数据VO列表
*/
List<CustomerExportDataVO> selectCustomerExportDataVOListByPage(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("customerName") String customerName,
@Param("offset") int offset,
@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);
/**
* 查询未分组记录的成单数所有组字段都为空
* @param corpId 企业ID
* @param date 成交日期
* @param successFlags 成单状态标志
* @return 成单数
*/
Long selectUngroupedFinishCount(
@Param("corpId") String corpId,
@Param("date") Date date,
@Param("successFlags") List<String> successFlags);
/**
* 查询未分组记录的及时单数量
* @param corpId 企业ID
* @param date 成交日期
* @param successFlags 及时单状态标志
* @return 及时单数量
*/
Long selectUngroupedTimelyOrderCount(
@Param("corpId") String corpId,
@Param("date") Date date,
@Param("successFlags") List<String> successFlags);
/**
* 查询未分组记录的非及时单数量
* @param corpId 企业ID
* @param date 成交日期
* @param successFlags 非及时单状态标志
* @return 非及时单数量
*/
Long selectUngroupedNonTimelyOrderCount(
@Param("corpId") String corpId,
@Param("date") Date date,
@Param("successFlags") List<String> successFlags);
}

View File

@ -16,7 +16,7 @@ import java.util.List;
@Mapper
public interface CustomerStatisticsDataMapper extends BaseMapper<CustomerStatisticsData> {
List<CustomerStatisticsVO> selectByDate(Date date);
List<CustomerStatisticsVO> selectByDate(@Param("corpId") String corpId,@Param("date")Date date);
/**
* 查询客户统计数据列表
@ -26,6 +26,7 @@ public interface CustomerStatisticsDataMapper extends BaseMapper<CustomerStatist
* @return 客户统计数据列表
*/
List<CustomerStatisticsData> selectCustomerStatisticsDataList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("indicatorName") String indicatorName
@ -39,10 +40,44 @@ public interface CustomerStatisticsDataMapper extends BaseMapper<CustomerStatist
* @return 客户统计数据VO列表
*/
List<CustomerStatisticsDataVO> selectCustomerStatisticsDataVOList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("indicatorName") String indicatorName
);
List<CustomerStatisticsData> selectDailyDataByWeek(
@Param("corpId") String corpId,
@Param("year") Integer year,
@Param("week") Integer week,
@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(
@Param("corpId") String corpId,
@Param("yearMonth") String yearMonth,
@Param("indicatorName") String indicatorName
);
List<CustomerStatisticsData> selectAllDailyData(
@Param("corpId") String corpId,
@Param("indicatorName") String indicatorName
);
}

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

@ -22,7 +22,7 @@ public interface DepartmentStatisticsDataMapper extends BaseMapper<DepartmentSta
* @param date 统计日期
* @return 统计数据列表
*/
List<DepartmentStatisticsData> selectByDate(@Param("date") Date date);
List<DepartmentStatisticsData> selectByDate(@Param("corpId") String corpId,@Param("date") Date date);
/**
* 根据部门路径和日期范围查询统计数据
@ -32,6 +32,7 @@ public interface DepartmentStatisticsDataMapper extends BaseMapper<DepartmentSta
* @return 统计数据列表
*/
List<DepartmentStatisticsData> selectByDepartmentAndDateRange(
@Param("corpId") String corpId,
@Param("departmentPath") String departmentPath,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate
@ -42,14 +43,14 @@ public interface DepartmentStatisticsDataMapper extends BaseMapper<DepartmentSta
* @param departmentPath 部门路径
* @return 累计值
*/
Map<String, Object> selectHistoricalAcceptSum(@Param("departmentPath") String departmentPath);
Map<String, Object> selectHistoricalAcceptSum(@Param("corpId") String corpId,@Param("departmentPath") String departmentPath);
/**
* 查询指定部门的历史累计成单数
* @param departmentPath 部门路径
* @return 累计值
*/
Map<String, Object> selectHistoricalOrderSum(@Param("departmentPath") String departmentPath);
Map<String, Object> selectHistoricalOrderSum(@Param("corpId") String corpId,@Param("departmentPath") String departmentPath);
/**
* 查询部门统计数据列表
@ -59,6 +60,7 @@ public interface DepartmentStatisticsDataMapper extends BaseMapper<DepartmentSta
* @return 部门统计数据列表
*/
List<DepartmentStatisticsData> selectDepartmentStatisticsDataList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("departmentPath") String departmentPath
@ -72,12 +74,41 @@ public interface DepartmentStatisticsDataMapper extends BaseMapper<DepartmentSta
* @return 部门统计数据VO列表
*/
List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("departmentPath") String departmentPath
);
Map<String, BigDecimal> getSummary(@Param("startDate") Date startDate,
Map<String, BigDecimal> getSummary(@Param("corpId") String corpId,@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("departmentPath") String departmentPath);
/**
* 查询部门统计数据聚合列表(按部门路径聚合)
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径(可选)
* @return 部门统计数据聚合列表
*/
List<DepartmentStatisticsData> selectDepartmentStatisticsDataAggregatedList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("departmentPath") String departmentPath
);
/**
* 查询部门统计数据VO聚合列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径(可选)
* @return 部门统计数据VO聚合列表
*/
List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOAggregatedList(
@Param("corpId") String corpId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("departmentPath") String departmentPath
);
}

View File

@ -3,6 +3,7 @@ package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.WecomTagGroupDomain;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 企业微信标签组Mapper
@ -16,6 +17,6 @@ public interface WecomTagGroupMapper extends BaseMapper<WecomTagGroupDomain> {
* @param tagGroupId 标签组ID
* @return 标签组
*/
WecomTagGroupDomain selectByTagGroupId(String tagGroupId);
WecomTagGroupDomain selectByTagGroupId(@Param("corpId") String corpId,@Param("tagGroupId") String tagGroupId);
}

View File

@ -3,6 +3,7 @@ package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.WecomTagDomain;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 企业微信标签Mapper
@ -16,6 +17,6 @@ public interface WecomTagMapper extends BaseMapper<WecomTagDomain> {
* @param tagId 标签ID
* @return 标签
*/
WecomTagDomain selectByTagId(String tagId);
WecomTagDomain selectByTagId(@Param("corpId") String corpId, @Param("tagId")String tagId);
}

View File

@ -1,55 +0,0 @@
package com.ruoyi.excel.wecom.model;
import org.springframework.context.annotation.Configuration;
/**
* 企业微信配置类
*/
@Configuration
public class WecomConfig {
private String corpId = "ww4f2fd849224439be";
private String corpSecret = "gsRCPzJuKsmxQVQlOjZWgYVCQMvNvliuUSJSbK8AWzk";
private String accessToken;
private long accessTokenExpireTime;
public String getCorpId() {
return corpId;
}
public void setCorpId(String corpId) {
this.corpId = corpId;
}
public String getCorpSecret() {
return corpSecret;
}
public void setCorpSecret(String corpSecret) {
this.corpSecret = corpSecret;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public long getAccessTokenExpireTime() {
return accessTokenExpireTime;
}
public void setAccessTokenExpireTime(long accessTokenExpireTime) {
this.accessTokenExpireTime = accessTokenExpireTime;
}
/**
* 检查accessToken是否过期
*
* @return 是否过期
*/
public boolean isAccessTokenExpired() {
return System.currentTimeMillis() > accessTokenExpireTime;
}
}

View File

@ -83,6 +83,7 @@ public class CustomerDataChangeTrackingService {
try {
// 构建用于计算指纹的字符串排除id字段
StringBuilder sb = new StringBuilder();
sb.append(data.getCustomerUserId()).append("|");
sb.append(data.getCustomerName()).append("|");
sb.append(data.getDescription()).append("|");
sb.append(data.getGender()).append("|");
@ -115,6 +116,7 @@ public class CustomerDataChangeTrackingService {
sb.append(data.getTagGroup15()).append("|");
sb.append(data.getTagGroup16()).append("|");
sb.append(data.getTagGroup17()).append("|");
sb.append(data.getFinishDate()).append("|");
sb.append(data.getTagGroup18());
// 计算MD5
@ -177,7 +179,7 @@ public class CustomerDataChangeTrackingService {
* @return 历史记录ID
*/
@Transactional(rollbackFor = Exception.class)
public Long saveDataChange(Long customerId, CustomerExportData newData, String changeType) {
public Long saveDataChange(String cropId,Long customerId, CustomerExportData newData, String changeType) {
if (customerId == null || newData == null) {
return null;
}
@ -192,6 +194,7 @@ public class CustomerDataChangeTrackingService {
// 3. 创建历史记录
CustomerExportDataHistory history = new CustomerExportDataHistory();
BeanUtils.copyProperties(newData, history);
history.setCorpId(cropId);
history.setHistoryId(null); // 清除ID让数据库自动生成
history.setCustomerId(customerId);
history.setVersion(newVersion);
@ -208,7 +211,7 @@ public class CustomerDataChangeTrackingService {
CustomerExportDataHistory previousHistory = historyMapper.selectByCustomerIdAndVersion(customerId, newVersion - 1);
if (previousHistory != null) {
// 比较字段变更
List<CustomerDataChangeLog> changeLogs = compareAndGenerateChangeLogs(
List<CustomerDataChangeLog> changeLogs = compareAndGenerateChangeLogs(cropId,
history.getHistoryId(), customerId, newVersion, previousHistory, history);
// 批量保存变更日志
@ -231,7 +234,7 @@ public class CustomerDataChangeTrackingService {
* @param newData 新数据
* @return 变更日志列表
*/
private List<CustomerDataChangeLog> compareAndGenerateChangeLogs(
private List<CustomerDataChangeLog> compareAndGenerateChangeLogs(String corpId,
Long historyId, Long customerId, Integer version,
CustomerExportDataHistory oldData, CustomerExportDataHistory newData) {
@ -265,6 +268,7 @@ public class CustomerDataChangeTrackingService {
changeLog.setNewValue(newValue != null ? newValue.toString() : null);
changeLog.setChangeTime(changeTime);
changeLog.setVersion(version);
changeLog.setCorpId(corpId);
changeLogs.add(changeLog);
}

View File

@ -0,0 +1,250 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import com.ruoyi.system.service.ISysExportTaskService;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Date;
import java.util.List;
/**
* 客户数据异步导出服务
*/
@Service
public class CustomerExportAsyncService
{
private static final Logger log = LoggerFactory.getLogger(CustomerExportAsyncService.class);
private static final int BATCH_SIZE = 5000;
private static final int ROW_ACCESS_WINDOW = 1000;
@Autowired
private CustomerExportDataMapper customerExportDataMapper;
@Autowired
private ISysExportTaskService exportTaskService;
@Async("threadPoolTaskExecutor")
public void executeExport(Long taskId, String corpId, Date startDate, Date endDate, String customerName)
{
log.info("开始执行异步导出任务taskId: {}, corpId: {}", taskId, corpId);
String filePath = null;
String fileName = null;
File file = null;
SXSSFWorkbook workbook = null;
FileOutputStream fos = null;
try
{
int totalCount = customerExportDataMapper.countCustomerExportDataVOList(corpId, startDate, endDate, customerName);
log.info("导出任务 taskId: {}, 总数据量: {}", taskId, totalCount);
if (totalCount == 0)
{
exportTaskService.markSuccess(taskId, "", "无数据.xlsx", 0L);
return;
}
exportTaskService.updateProgress(taskId, 0, totalCount);
String dateStr = DateUtils.dateTimeNow("yyyyMMddHHmmss");
fileName = "客户统计数据_" + dateStr + ".xlsx";
String downloadPath = RuoYiConfig.getDownloadPath();
File dir = new File(downloadPath);
if (!dir.exists())
{
dir.mkdirs();
}
filePath = downloadPath + fileName;
file = new File(filePath);
workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW);
Sheet sheet = workbook.createSheet("客户统计数据");
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle dataStyle = createDataStyle(workbook);
String[] headers = {
"客户名称", "描述", "添加人", "添加人账号", "添加人所属部门", "添加时间", "来源",
"手机", "企业", "邮箱", "地址", "职务", "电话",
"标签组1(投放)", "标签组2(公司孵化)", "标签组3(商务)", "标签组4(成交日期)", "标签组5(年级组)",
"标签组6(客户属性)", "标签组7(成交)", "标签组8(成交品牌)", "标签组9(线索通标签)", "标签组10(A1组)",
"标签组11(B1组)", "标签组12(C1组)", "标签组13(D1组)", "标签组14(E1组)", "标签组15(意向度)",
"标签组16(自然流)", "标签组17(F1组)", "标签组18(G1组)"
};
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++)
{
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
sheet.setColumnWidth(i, 20 * 256);
}
int processedCount = 0;
int rowIndex = 1;
int offset = 0;
while (offset < totalCount)
{
List<CustomerExportDataVO> dataList = customerExportDataMapper.selectCustomerExportDataVOListByPage(
corpId, startDate, endDate, customerName, offset, BATCH_SIZE);
if (dataList == null || dataList.isEmpty())
{
break;
}
for (CustomerExportDataVO data : dataList)
{
Row row = sheet.createRow(rowIndex++);
fillRowData(row, data, dataStyle);
processedCount++;
}
offset += BATCH_SIZE;
exportTaskService.updateProgress(taskId, processedCount, totalCount);
}
fos = new FileOutputStream(file);
workbook.write(fos);
fos.flush();
long fileSize = file.length();
exportTaskService.markSuccess(taskId, filePath, fileName, fileSize);
log.info("导出任务完成taskId: {}, 文件: {}, 大小: {} bytes", taskId, fileName, fileSize);
}
catch (Exception e)
{
log.error("导出任务执行失败taskId: {}", taskId, e);
exportTaskService.markFailed(taskId, "导出失败:" + e.getMessage());
}
finally
{
try
{
if (fos != null)
{
fos.close();
}
if (workbook != null)
{
workbook.dispose();
workbook.close();
}
}
catch (Exception e)
{
log.error("关闭资源失败", e);
}
}
}
private CellStyle createHeaderStyle(Workbook workbook)
{
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style.setBorderRight(BorderStyle.THIN);
style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderLeft(BorderStyle.THIN);
style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderTop(BorderStyle.THIN);
style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderBottom(BorderStyle.THIN);
style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
Font font = workbook.createFont();
font.setBold(true);
font.setFontName("Arial");
font.setFontHeightInPoints((short) 10);
style.setFont(font);
return style;
}
private CellStyle createDataStyle(Workbook workbook)
{
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderRight(BorderStyle.THIN);
style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderLeft(BorderStyle.THIN);
style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderTop(BorderStyle.THIN);
style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderBottom(BorderStyle.THIN);
style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
Font font = workbook.createFont();
font.setFontName("Arial");
font.setFontHeightInPoints((short) 10);
style.setFont(font);
return style;
}
private void fillRowData(Row row, CustomerExportDataVO data, CellStyle style)
{
int col = 0;
createCell(row, col++, data.getCustomerName(), style);
createCell(row, col++, data.getDescription(), style);
createCell(row, col++, data.getAddUserName(), style);
createCell(row, col++, data.getAddUserAccount(), style);
createCell(row, col++, data.getAddUserDepartment(), style);
createCell(row, col++, formatDate(data.getAddTime()), style);
createCell(row, col++, data.getSource(), style);
createCell(row, col++, data.getMobile(), style);
createCell(row, col++, data.getCompany(), style);
createCell(row, col++, data.getEmail(), style);
createCell(row, col++, data.getAddress(), style);
createCell(row, col++, data.getPosition(), style);
createCell(row, col++, data.getPhone(), style);
createCell(row, col++, data.getTagGroup1(), style);
createCell(row, col++, data.getTagGroup2(), style);
createCell(row, col++, data.getTagGroup3(), style);
createCell(row, col++, data.getTagGroup4(), style);
createCell(row, col++, data.getTagGroup5(), style);
createCell(row, col++, data.getTagGroup6(), style);
createCell(row, col++, data.getTagGroup7(), style);
createCell(row, col++, data.getTagGroup8(), style);
createCell(row, col++, data.getTagGroup9(), style);
createCell(row, col++, data.getTagGroup10(), style);
createCell(row, col++, data.getTagGroup11(), style);
createCell(row, col++, data.getTagGroup12(), style);
createCell(row, col++, data.getTagGroup13(), style);
createCell(row, col++, data.getTagGroup14(), style);
createCell(row, col++, data.getTagGroup15(), style);
createCell(row, col++, data.getTagGroup16(), style);
createCell(row, col++, data.getTagGroup17(), style);
createCell(row, col++, data.getTagGroup18(), style);
}
private void createCell(Row row, int column, String value, CellStyle style)
{
Cell cell = row.createCell(column);
cell.setCellValue(value == null ? "" : value);
cell.setCellStyle(style);
}
private String formatDate(Date date)
{
if (date == null)
{
return "";
}
return DateUtils.parseDateToStr("yyyy-MM-dd", date);
}
}

View File

@ -4,6 +4,7 @@ import com.ruoyi.excel.wecom.domain.*;
import com.ruoyi.excel.wecom.enums.AddWayEnum;
import com.ruoyi.excel.wecom.mapper.*;
import com.ruoyi.excel.wecom.model.WecomCustomer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -15,6 +16,7 @@ import java.util.stream.Collectors;
* 客户导出服务类
*/
@Service
@Slf4j
public class CustomerExportService {
@Autowired
@ -37,7 +39,7 @@ public class CustomerExportService {
// 缓存映射
private Map<String, CorpUser> userIdToUserMap = new HashMap<>();
private Map<Long, String> departmentIdToNameMap = new HashMap<>();
private Map<Long, CorpDepartment> departmentIdToNameMap = new HashMap<>();
private Map<String, WecomTagDomain> tagIdToTagMap = new HashMap<>();
private Map<String, WecomTagGroupDomain> tagGroupIdToGroupMap = new HashMap<>();
private Map<String, List<WecomTagDomain>> tagGroupIdToTagsMap = new HashMap<>();
@ -48,31 +50,31 @@ public class CustomerExportService {
*/
@PostConstruct
public void initCache() {
System.out.println("开始初始化缓存数据...");
log.debug("开始初始化缓存数据...");
// 1. 加载用户数据
List<CorpUser> userList = corpUserMapper.selectList(null);
userIdToUserMap = userList.stream()
.collect(Collectors.toMap(CorpUser::getUserid, user -> user, (v1, v2) -> v1));
System.out.println("已加载 " + userIdToUserMap.size() + " 个用户");
.collect(Collectors.toMap(CorpUser::getUserid, user -> user, (v1, v2) -> v1.getUpdateTime().compareTo(v2.getUpdateTime()) >= 0 ? v1 : v2));
log.debug("已加载 " + userIdToUserMap.size() + " 个用户");
// 2. 加载部门数据
List<CorpDepartment> departmentList = corpDepartmentMapper.selectList(null);
departmentIdToNameMap = departmentList.stream()
.collect(Collectors.toMap(CorpDepartment::getId, CorpDepartment::getName, (v1, v2) -> v1));
System.out.println("已加载 " + departmentIdToNameMap.size() + " 个部门");
.collect(Collectors.toMap(CorpDepartment::getId, department->department, (v1, v2) -> v1.getUpdateTime().compareTo(v2.getUpdateTime()) >= 0 ? v1 : v2));
log.debug("已加载 " + departmentIdToNameMap.size() + " 个部门");
// 3. 加载标签数据
List<WecomTagDomain> tagList = wecomTagMapper.selectList(null);
tagIdToTagMap = tagList.stream()
.collect(Collectors.toMap(WecomTagDomain::getTagId, tag -> tag, (v1, v2) -> v1));
System.out.println("已加载 " + tagIdToTagMap.size() + " 个标签");
.collect(Collectors.toMap(WecomTagDomain::getTagId, tag -> tag, (v1, v2) -> v2));
log.debug("已加载 " + tagIdToTagMap.size() + " 个标签");
// 4. 加载标签组数据
List<WecomTagGroupDomain> tagGroupList = wecomTagGroupMapper.selectList(null);
tagGroupIdToGroupMap = tagGroupList.stream()
.collect(Collectors.toMap(WecomTagGroupDomain::getTagGroupId, group -> group, (v1, v2) -> v1));
System.out.println("已加载 " + tagGroupIdToGroupMap.size() + " 个标签组");
.collect(Collectors.toMap(WecomTagGroupDomain::getTagGroupId, group -> group, (v1, v2) -> v2));
log.debug("已加载 " + tagGroupIdToGroupMap.size() + " 个标签组");
// 5. 构建标签组到标签列表的映射
for (WecomTagDomain tag : tagList) {
@ -80,11 +82,11 @@ public class CustomerExportService {
tagGroupIdToTagsMap.computeIfAbsent(groupId, k -> new ArrayList<>()).add(tag);
}
System.out.println("缓存初始化完成!");
log.debug("缓存初始化完成!");
}
public List<Date> getAllDate() {
return customerExportDataMapper.getDistinctDate();
public List<Date> getAllDate(String corpId) {
return customerExportDataMapper.getDistinctDate(corpId);
}
/**
* 处理客户数据并保存到数据库
@ -96,9 +98,9 @@ public class CustomerExportService {
* @param customerList 客户列表
* @return 处理成功的数量
*/
public int handleData(List<WecomCustomer> customerList) {
public int handleData(String corpId,List<WecomCustomer> customerList) {
if (customerList == null || customerList.isEmpty()) {
System.out.println("客户列表为空,无需处理");
log.debug("客户列表为空,无需处理");
return 0;
}
@ -106,7 +108,7 @@ public class CustomerExportService {
int insertCount = 0;
int updateCount = 0;
int unchangedCount = 0;
System.out.println("开始处理 " + customerList.size() + " 个客户数据...");
log.info("开始处理 " + customerList.size() + " 个客户数据...");
for (WecomCustomer customer : customerList) {
try {
@ -114,21 +116,23 @@ public class CustomerExportService {
if (exportData != null) {
// 根据唯一标识客户名称+添加人账号+添加时间查询是否已存在
CustomerExportData existingData = customerExportDataMapper.selectByUniqueKey(
exportData.getCustomerName(),
corpId,
exportData.getCustomerUserId(),
exportData.getAddUserAccount(),
exportData.getAddTime()
);
if (existingData == null) {
exportData.setCorpId(corpId);
// 新增客户
customerExportDataMapper.insert(exportData);
// 保存新增记录到历史表
changeTrackingService.saveDataChange(exportData.getId(), exportData, "INSERT");
changeTrackingService.saveDataChange(corpId,exportData.getId(), exportData, "INSERT");
insertCount++;
successCount++;
System.out.println("新增客户: " + exportData.getCustomerName());
log.debug("新增客户: " + exportData.getCustomerName());
} else {
// 检查数据是否发生变更
boolean hasChanged = changeTrackingService.hasDataChanged(existingData.getId(), exportData);
@ -139,30 +143,30 @@ public class CustomerExportService {
customerExportDataMapper.updateById(exportData);
// 保存变更记录到历史表和变更日志表
changeTrackingService.saveDataChange(existingData.getId(), exportData, "UPDATE");
changeTrackingService.saveDataChange(corpId,existingData.getId(), exportData, "UPDATE");
updateCount++;
successCount++;
System.out.println("更新客户: " + exportData.getCustomerName() + " (数据已变更)");
log.debug("更新客户: " + exportData.getCustomerName() + " (数据已变更)");
} else {
// 数据未变更跳过更新
unchangedCount++;
System.out.println("跳过客户: " + exportData.getCustomerName() + " (数据未变更)");
log.debug("跳过客户: " + exportData.getCustomerName() + " (数据未变更)");
}
}
}
} catch (Exception e) {
System.err.println("处理客户数据失败: " + e.getMessage());
log.error("处理客户数据失败: " + e.getMessage());
e.printStackTrace();
}
}
System.out.println("数据处理完成!");
System.out.println("总计: " + customerList.size() + "");
System.out.println("新增: " + insertCount + "");
System.out.println("更新: " + updateCount + "");
System.out.println("未变更: " + unchangedCount + "");
System.out.println("成功: " + successCount + "");
log.info("数据处理完成!");
log.info("总计: " + customerList.size() + "");
log.info("新增: " + insertCount + "");
log.info("更新: " + updateCount + "");
log.info("未变更: " + unchangedCount + "");
log.info("成功: " + successCount + "");
return successCount;
}
@ -182,6 +186,7 @@ public class CustomerExportService {
//设置性别
if(externalContact != null) {
exportData.setGender(externalContact.getGender());
exportData.setCustomerUserId(externalContact.getExternalUserid());
}
// 获取跟进信息
@ -230,6 +235,79 @@ public class CustomerExportService {
return exportData;
}
private Date parseCompleteDate(String orderDate, Date addDate) {
if (orderDate == null || orderDate.trim().isEmpty()) {
return null;
}
if (addDate == null) {
log.warn("parseCompleteDate: addDate is null, orderDate: {}", orderDate);
return null;
}
try {
String[] dates = orderDate.trim().split(",");
String lastDateStr = dates[dates.length - 1].trim();
log.debug("parseCompleteDate: input orderDate: {}, lastDateStr: {}", orderDate, lastDateStr);
Calendar orderCal = Calendar.getInstance();
orderCal.setTime(addDate);
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;
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;
log.info("parseCompleteDate: matched full date pattern, input: {}, result: {}-{}-{}", lastDateStr, year, month, day);
}
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(addDate);
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(addDate)) {
orderCal.add(Calendar.YEAR, 1);
}
parsed = true;
log.info("parseCompleteDate: matched month-day pattern, input: {}, result: {}-{}-{}", lastDateStr, orderCal.get(Calendar.YEAR), month, day);
}
}
if (!parsed) {
log.warn("parseCompleteDate: no pattern matched, input: {}", lastDateStr);
return null;
}
return orderCal.getTime();
} catch (Exception e) {
log.error("parseCompleteDate error, orderDate: {}, addDate: {}", orderDate, addDate, e);
return null;
}
}
/**
* 处理客户标签按标签组分类填充到对应字段
*
@ -272,8 +350,7 @@ public class CustomerExportService {
* @param tagNames 标签名称逗号分隔
*/
private void fillTagGroupField(CustomerExportData exportData, String groupName, String tagNames) {
// 根据标签组名称映射到对应字段
// 这里的映射关系需要根据实际业务调整
if (groupName.contains("投放")) {
exportData.setTagGroup1(tagNames);
} else if (groupName.contains("公司孵化")) {
@ -282,12 +359,15 @@ public class CustomerExportService {
exportData.setTagGroup3(tagNames);
} else if (groupName.contains("成交日期")) {
exportData.setTagGroup4(tagNames);
Date finishDate = parseCompleteDate(tagNames, exportData.getAddTime());
exportData.setFinishDate(finishDate);
} else if (groupName.contains("年级组")) {
exportData.setTagGroup5(tagNames);
} else if (groupName.contains("客户属性")) {
exportData.setTagGroup6(tagNames);
} else if (groupName.contains("成交") && !groupName.contains("日期") && !groupName.contains("品牌")) {
exportData.setTagGroup7(tagNames);
} else if (groupName.contains("成交品牌")) {
exportData.setTagGroup8(tagNames);
} else if (groupName.contains("线索通")) {

View File

@ -0,0 +1,10 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.excel.wecom.vo.CorpDepartmentVO;
import java.util.List;
public interface ICorpDepartmentService {
List<CorpDepartmentVO> selectCorpDepartmentTree(String corpId);
}

View File

@ -0,0 +1,67 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.excel.wecom.domain.CorpInfo;
import java.util.List;
/**
* 企业信息Service接口
*/
public interface ICorpInfoService {
/**
* 查询企业信息列表
*
* @param corpInfo 企业信息
* @return 企业信息集合
*/
List<CorpInfo> selectCorpInfoList(CorpInfo corpInfo);
/**
* 根据企业ID查询企业信息
*
* @param corpId 企业ID
* @return 企业信息
*/
CorpInfo selectCorpInfoByCorpId(String corpId);
/**
* 根据ID查询企业信息
*
* @param id 主键ID
* @return 企业信息
*/
CorpInfo selectCorpInfoById(Long id);
/**
* 新增企业信息
*
* @param corpInfo 企业信息
* @return 结果
*/
int insertCorpInfo(CorpInfo corpInfo);
/**
* 修改企业信息
*
* @param corpInfo 企业信息
* @return 结果
*/
int updateCorpInfo(CorpInfo corpInfo);
/**
* 批量删除企业信息
*
* @param ids 需要删除的企业信息主键集合
* @return 结果
*/
int deleteCorpInfoByIds(Long[] ids);
/**
* 删除企业信息
*
* @param id 企业信息主键
* @return 结果
*/
int deleteCorpInfoById(Long id);
}

View File

@ -18,7 +18,7 @@ public interface ICustomerContactDataService {
* @param userid 成员ID
* @return 客户联系统计数据列表
*/
List<CustomerContactData> selectCustomerContactDataList(Date startDate, Date endDate, String userid);
List<CustomerContactData> selectCustomerContactDataList(String corpId,Date startDate, Date endDate, String userid);
/**
* 查询客户联系统计数据VO列表(用于导出)
@ -27,7 +27,7 @@ public interface ICustomerContactDataService {
* @param userid 成员ID
* @return 客户联系统计数据VO列表
*/
List<CustomerContactDataVO> selectCustomerContactDataVOList(Date startDate, Date endDate, String userid);
List<CustomerContactDataVO> selectCustomerContactDataVOList(String corpId,Date startDate, Date endDate, String userid);
/**
* 根据ID查询客户联系统计数据
@ -36,13 +36,6 @@ public interface ICustomerContactDataService {
*/
CustomerContactData selectCustomerContactDataById(Long id);
/**
* 新增客户联系统计数据
* @param customerContactData 客户联系统计数据
* @return 结果
*/
int insertCustomerContactData(CustomerContactData customerContactData);
/**
* 修改客户联系统计数据
* @param customerContactData 客户联系统计数据

View File

@ -18,7 +18,7 @@ public interface ICustomerExportDataService {
* @param customerName 客户名称
* @return 客户导出数据列表
*/
List<CustomerExportData> selectCustomerExportDataList(Date startDate, Date endDate, String customerName);
List<CustomerExportData> selectCustomerExportDataList(String corpId,Date startDate, Date endDate, String customerName);
/**
* 查询客户导出数据VO列表(用于导出)
@ -27,7 +27,7 @@ public interface ICustomerExportDataService {
* @param customerName 客户名称
* @return 客户导出数据VO列表
*/
List<CustomerExportDataVO> selectCustomerExportDataVOList(Date startDate, Date endDate, String customerName);
List<CustomerExportDataVO> selectCustomerExportDataVOList(String corpId,Date startDate, Date endDate, String customerName);
/**
* 根据ID查询客户导出数据

View File

@ -19,7 +19,7 @@ public interface ICustomerStatisticsDataService {
* @param indicatorName 指标名称
* @return 客户统计数据列表
*/
List<CustomerStatisticsData> selectCustomerStatisticsDataList(Date startDate, Date endDate, String indicatorName);
List<CustomerStatisticsData> selectCustomerStatisticsDataList(String corpId,Date startDate, Date endDate, String indicatorName);
/**
* 查询客户统计数据VO列表(用于导出)
@ -28,7 +28,7 @@ public interface ICustomerStatisticsDataService {
* @param indicatorName 指标名称
* @return 客户统计数据VO列表
*/
List<CustomerStatisticsDataVO> selectCustomerStatisticsDataVOList(Date startDate, Date endDate, String indicatorName);
List<CustomerStatisticsDataVO> selectCustomerStatisticsDataVOList(String corpId,Date startDate, Date endDate, String indicatorName);
/**
* 根据ID查询客户统计数据
@ -58,5 +58,11 @@ public interface ICustomerStatisticsDataService {
*/
int deleteCustomerStatisticsDataByIds(Long[] ids);
int updateCost(Date curDate, BigDecimal totalCost, String titleAttr);
int updateCost(String corpId,Date curDate, BigDecimal totalCost, String type,String titleAttr);
List<CustomerStatisticsDataVO> selectByWeekAggregation(String corpId, Integer year, Integer week, String indicatorName);
List<CustomerStatisticsDataVO> selectByMonthAggregation(String corpId, String yearMonth, String indicatorName);
List<CustomerStatisticsDataVO> selectAllAggregation(String corpId, String indicatorName);
}

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

@ -18,18 +18,20 @@ public interface IDepartmentStatisticsDataService {
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径
* @param dataType 数据类型
* @return 部门统计数据列表
*/
List<DepartmentStatisticsData> selectDepartmentStatisticsDataList(Date startDate, Date endDate, String departmentPath);
List<DepartmentStatisticsData> selectDepartmentStatisticsDataList(String corpId, Date startDate, Date endDate, String departmentPath, String dataType);
/**
* 查询部门统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径
* @param dataType 数据类型
* @return 部门统计数据VO列表
*/
List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOList(Date startDate, Date endDate, String departmentPath);
List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOList(String corpId, Date startDate, Date endDate, String departmentPath, String dataType);
/**
* 根据ID查询部门统计数据
@ -59,5 +61,5 @@ public interface IDepartmentStatisticsDataService {
*/
int deleteDepartmentStatisticsDataByIds(Long[] ids);
Map<String, BigDecimal> getSummary(Date startDate, Date endDate, String departmentPath);
Map<String, BigDecimal> getSummary(String corpId, Date startDate, Date endDate, String departmentPath, String dataType);
}

View File

@ -2,7 +2,9 @@ package com.ruoyi.excel.wecom.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.excel.wecom.model.WecomConfig;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.excel.wecom.domain.CorpInfo;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
@ -13,37 +15,51 @@ import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* 企业微信基础服务类
*/
public class WecomBaseService {
private WecomConfig wecomConfig;
public WecomBaseService(WecomConfig wecomConfig) {
this.wecomConfig = wecomConfig;
}
/**
* Redis key 前缀wecom_access_token:{corpId}
*/
private static final String ACCESS_TOKEN_KEY_PREFIX = "wecom_access_token:";
/**
* 获取accessToken
* 获取accessToken根据当前企业上下文动态获取
*
* @return accessToken
* @throws IOException IO异常
*/
public String getAccessToken() throws IOException {
if (!wecomConfig.isAccessTokenExpired()) {
return wecomConfig.getAccessToken();
public String getAccessToken(String corpId) throws IOException {
// 2. Redis 获取缓存的 access_token
RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
String cacheKey = ACCESS_TOKEN_KEY_PREFIX + corpId;
String cachedToken = redisCache.getCacheObject(cacheKey);
if (cachedToken != null && !cachedToken.trim().isEmpty()) {
return cachedToken;
}
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + wecomConfig.getCorpId() + "&corpsecret=" + wecomConfig.getCorpSecret();
// 3. 缓存不存在或已过期从数据库查询企业信息
ICorpInfoService corpInfoService = SpringUtils.getBean(ICorpInfoService.class);
CorpInfo corpInfo = corpInfoService.selectCorpInfoByCorpId(corpId);
if (corpInfo == null) {
throw new RuntimeException("企业信息不存在: " + corpId);
}
// 4. 调用企业微信API获取新的 access_token
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpInfo.getCorpId() + "&corpsecret=" + corpInfo.getSecret();
String response = doGet(url);
JSONObject jsonObject = JSON.parseObject(response);
if (jsonObject.getInteger("errcode") == 0) {
String accessToken = jsonObject.getString("access_token");
int expiresIn = jsonObject.getInteger("expires_in");
wecomConfig.setAccessToken(accessToken);
wecomConfig.setAccessTokenExpireTime(System.currentTimeMillis() + (expiresIn - 60) * 1000);
// 5. access_token 缓存到 Redis提前60秒过期
redisCache.setCacheObject(cacheKey, accessToken, expiresIn - 60, TimeUnit.SECONDS);
return accessToken;
} else {
throw new RuntimeException("获取accessToken失败: " + jsonObject.getString("errmsg"));
@ -93,11 +109,5 @@ public class WecomBaseService {
}
}
public WecomConfig getWecomConfig() {
return wecomConfig;
}
public void setWecomConfig(WecomConfig wecomConfig) {
this.wecomConfig = wecomConfig;
}
}

View File

@ -5,7 +5,6 @@ import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.excel.wecom.domain.CorpDepartment;
import com.ruoyi.excel.wecom.domain.CorpUser;
import com.ruoyi.excel.wecom.model.WecomConfig;
import com.ruoyi.excel.wecom.model.WecomCustomer;
import com.ruoyi.excel.wecom.model.WecomTagGroup;
import org.springframework.stereotype.Component;
@ -20,19 +19,14 @@ import java.util.List;
*/
@Component
public class WecomContactService extends WecomBaseService {
public WecomContactService(WecomConfig wecomConfig) {
super(wecomConfig);
}
/**
* 获取配置了客户联系功能的成员列表
*
* @return 成员ID列表
* @throws IOException IO异常
*/
public List<String> getFollowUserList() throws IOException {
String accessToken = getAccessToken();
public List<String> getFollowUserList(String corpId) throws IOException {
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_follow_user_list?access_token=" + accessToken;
String response = doGet(url);
JSONObject jsonObject = JSON.parseObject(response);
@ -51,8 +45,8 @@ public class WecomContactService extends WecomBaseService {
* @return 客户外部联系人ID列表
* @throws IOException IO异常
*/
public List<String> getExternalContactList(String userid) throws IOException {
String accessToken = getAccessToken();
public List<String> getExternalContactList(String corpId,String userid) throws IOException {
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=" + accessToken + "&userid=" + userid;
String response = doGet(url);
JSONObject jsonObject = JSON.parseObject(response);
@ -70,12 +64,12 @@ public class WecomContactService extends WecomBaseService {
* @return 成员客户映射关系
* @throws IOException IO异常
*/
public List<MemberCustomer> getAllMembersCustomers() throws IOException {
List<String> userList = getFollowUserList();
public List<MemberCustomer> getAllMembersCustomers(String corpId) throws IOException {
List<String> userList = getFollowUserList(corpId);
List<MemberCustomer> result = new ArrayList<>();
for (String userid : userList) {
List<String> customerList = getExternalContactList(userid);
List<String> customerList = getExternalContactList(corpId,userid);
MemberCustomer memberCustomer = new MemberCustomer();
memberCustomer.setUserid(userid);
memberCustomer.setExternalUserids(customerList);
@ -94,13 +88,13 @@ public class WecomContactService extends WecomBaseService {
* @return 客户详情列表
* @throws IOException IO异常
*/
public String batchGetCustomerDetails(List<String> useridList,List<WecomCustomer> finalResult, String cursor, Integer limit) throws IOException {
public String batchGetCustomerDetails(String corpId,List<String> useridList,List<WecomCustomer> finalResult, String cursor, Integer limit) throws IOException {
if (useridList.size() > 100) {
throw new IllegalArgumentException("成员ID列表不能超过100个");
}
System.out.println("获取用户数据****" + cursor);
String accessToken = getAccessToken();
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user?access_token=" + accessToken;
JSONObject requestBody = new JSONObject();
@ -133,23 +127,10 @@ public class WecomContactService extends WecomBaseService {
}
} else if (jsonObject.containsKey("next_cursor") && StringUtils.isNotEmpty(jsonObject.getString("next_cursor"))) {
nextCursor = jsonObject.getString("next_cursor");
return batchGetCustomerDetails(useridList, finalResult, nextCursor, limit);
return batchGetCustomerDetails(corpId,useridList, finalResult, nextCursor, limit);
} else {
return null;
}
/* // 处理分页
if (jsonObject.containsKey("next_cursor") && StringUtils.isNotEmpty(jsonObject.getString("next_cursor"))) {
String nextCursor = jsonObject.getString("next_cursor");
//如果发现数量已经存储了2000条则先退出 由外部存储
if (finalResult.size() >= 2000) {
return nextCursor;
} else {
return batchGetCustomerDetails(useridList, finalResult, nextCursor, limit);
}
} else {
return null;
}*/
} else {
throw new RuntimeException("批量获取客户详情失败: " + jsonObject.getString("errmsg"));
}
@ -162,14 +143,14 @@ public class WecomContactService extends WecomBaseService {
* @return 客户详情列表
* @throws IOException IO异常
*/
public List<WecomCustomer> batchGetAllCustomerDetails(List<String> useridList) throws IOException {
public List<WecomCustomer> batchGetAllCustomerDetails(String corpId,List<String> useridList) throws IOException {
List<WecomCustomer> allCustomers = new ArrayList<>();
// 分批处理每次最多100个用户
for (int i = 0; i < useridList.size(); i += 100) {
int endIndex = Math.min(i + 100, useridList.size());
List<String> batchUserids = useridList.subList(i, endIndex);
batchGetCustomerDetails(batchUserids, allCustomers,null, 100);
batchGetCustomerDetails(corpId,batchUserids, allCustomers,null, 100);
}
return allCustomers;
@ -182,8 +163,8 @@ public class WecomContactService extends WecomBaseService {
* @return 客户详情
* @throws IOException IO异常
*/
public WecomCustomer getCustomerDetail(String externalUserid) throws IOException {
String accessToken = getAccessToken();
public WecomCustomer getCustomerDetail(String corpId,String externalUserid) throws IOException {
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=" + accessToken + "&external_userid=" + externalUserid;
String response = doGet(url);
JSONObject jsonObject = JSON.parseObject(response);
@ -206,8 +187,8 @@ public class WecomContactService extends WecomBaseService {
* @return 标签库列表
* @throws IOException IO异常
*/
public List<WecomTagGroup> getCorpTagList() throws IOException {
String accessToken = getAccessToken();
public List<WecomTagGroup> getCorpTagList(String corpId) throws IOException {
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_corp_tag_list?access_token=" + accessToken;
String response = doGet(url);
JSONObject jsonObject = JSON.parseObject(response);
@ -255,8 +236,8 @@ public class WecomContactService extends WecomBaseService {
* @return 子部门ID列表
* @throws IOException IO异常
*/
public List<CorpDepartment> getDepartmentList(Long departmentId) throws IOException {
String accessToken = getAccessToken();
public List<CorpDepartment> getDepartmentList(String corpId,Long departmentId) throws IOException {
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=" + accessToken;
if (departmentId != null) {
url += "&id=" + departmentId;
@ -282,8 +263,8 @@ public class WecomContactService extends WecomBaseService {
* @return 部门详情
* @throws IOException IO异常
*/
public String getDepartmentDetail(Long departmentId) throws IOException {
String accessToken = getAccessToken();
public String getDepartmentDetail(String corpId,Long departmentId) throws IOException {
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/department/get?access_token=" + accessToken + "&id=" + departmentId;
String response = doGet(url);
JSONObject jsonObject = JSON.parseObject(response);
@ -310,8 +291,8 @@ public class WecomContactService extends WecomBaseService {
* @return 部门成员详情列表
* @throws IOException IO异常
*/
public List<CorpUser> getDepartmentMemberList(Long departmentId, Integer fetchChild, Integer status) throws IOException {
String accessToken = getAccessToken();
public List<CorpUser> getDepartmentMemberList(String corpId,Long departmentId, Integer fetchChild, Integer status) throws IOException {
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=" + accessToken + "&department_id=" + departmentId;
if (fetchChild != null) {
url += "&fetch_child=" + fetchChild;
@ -354,8 +335,8 @@ public class WecomContactService extends WecomBaseService {
* @return 统计数据
* @throws IOException IO异常
*/
public JSONObject getUserBehaviorData(Long startTime, Long endTime, List<String> useridList) throws IOException {
String accessToken = getAccessToken();
public JSONObject getUserBehaviorData(String corpId,Long startTime, Long endTime, List<String> useridList) throws IOException {
String accessToken = getAccessToken(corpId);
String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_user_behavior_data?access_token=" + accessToken;
JSONObject requestBody = new JSONObject();
@ -383,7 +364,7 @@ public class WecomContactService extends WecomBaseService {
* @return 统计数据
* @throws IOException IO异常
*/
public JSONObject getDayUserBehaviorData(String date, List<String> useridList) throws IOException {
public JSONObject getDayUserBehaviorData(String corpId,String date, List<String> useridList) throws IOException {
try {
// 解析日期字符串为Date对象
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
@ -404,7 +385,7 @@ public class WecomContactService extends WecomBaseService {
Long endTime = calendar.getTimeInMillis() / 1000;
// 调用获取统计数据的方法
return getUserBehaviorData(startTime, endTime, useridList);
return getUserBehaviorData(corpId,startTime, endTime, useridList);
} catch (java.text.ParseException e) {
throw new RuntimeException("日期格式错误请使用yyyy-MM-dd格式", e);
}

View File

@ -32,7 +32,7 @@ public class WecomStatisticsService {
* @param statisticsData 统计数据
* @return 处理成功的数量
*/
public int handleAndSaveCustomerStatistics(String date, JSONObject statisticsData) {
public int handleAndSaveCustomerStatistics(String corpId,String date, JSONObject statisticsData) {
if (statisticsData == null || !statisticsData.containsKey("behavior_data")) {
return 0;
}
@ -53,7 +53,7 @@ public class WecomStatisticsService {
JSONObject data = behaviorData.getJSONObject("data");
// 处理每个统计指标
successCount += handleStatisticsIndicators(curDate, userid, data);
successCount += handleStatisticsIndicators(corpId,curDate, userid, data);
}
} catch (Exception e) {
e.printStackTrace();
@ -71,49 +71,49 @@ public class WecomStatisticsService {
* @param data 统计数据
* @return 处理成功的数量
*/
private int handleStatisticsIndicators(Date curDate, String userid, JSONObject data) {
private int handleStatisticsIndicators(String corpId,Date curDate, String userid, JSONObject data) {
int successCount = 0;
// 处理各项统计指标
// 这里需要根据实际的指标名称进行映射
// 示例处理新增客户数
if (data.containsKey("new_contact_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "新增客户数", userid, data.getInteger("new_contact_cnt"));
CustomerStatisticsData statisticsData = createCustomerStatisticsData(corpId,curDate, "新增客户数", userid, data.getInteger("new_contact_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理聊天次数
if (data.containsKey("chat_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "聊天次数", userid, data.getInteger("chat_cnt"));
CustomerStatisticsData statisticsData = createCustomerStatisticsData(corpId,curDate, "聊天次数", userid, data.getInteger("chat_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理发送消息数
if (data.containsKey("msg_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "发送消息数", userid, data.getInteger("msg_cnt"));
CustomerStatisticsData statisticsData = createCustomerStatisticsData(corpId,curDate, "发送消息数", userid, data.getInteger("msg_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理语音通话时长
if (data.containsKey("voice_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "语音通话时长(秒)", userid, data.getInteger("voice_cnt"));
CustomerStatisticsData statisticsData = createCustomerStatisticsData(corpId,curDate, "语音通话时长(秒)", userid, data.getInteger("voice_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理视频通话时长
if (data.containsKey("video_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "视频通话时长(秒)", userid, data.getInteger("video_cnt"));
CustomerStatisticsData statisticsData = createCustomerStatisticsData(corpId,curDate, "视频通话时长(秒)", userid, data.getInteger("video_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理拜访次数
if (data.containsKey("visit_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "拜访次数", userid, data.getInteger("visit_cnt"));
CustomerStatisticsData statisticsData = createCustomerStatisticsData(corpId,curDate, "拜访次数", userid, data.getInteger("visit_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
@ -130,9 +130,10 @@ public class WecomStatisticsService {
* @param value 指标值
* @return 客户统计数据实体
*/
private CustomerStatisticsData createCustomerStatisticsData(Date curDate, String indicatorName, String userid, Integer value) {
private CustomerStatisticsData createCustomerStatisticsData(String corpId,Date curDate, String indicatorName, String userid, Integer value) {
CustomerStatisticsData statisticsData = new CustomerStatisticsData();
statisticsData.setCurDate(curDate);
statisticsData.setCorpId(corpId);
statisticsData.setIndicatorName(indicatorName);
// 这里需要根据userid映射到对应的部门组
@ -160,8 +161,8 @@ public class WecomStatisticsService {
* @param date 日期
* @return 客户统计数据列表
*/
public List<CustomerStatisticsVO> getCustomerStatisticsByDate(Date date) {
return customerStatisticsDataMapper.selectByDate(date);
public List<CustomerStatisticsVO> getCustomerStatisticsByDate(String corpId,Date date) {
return customerStatisticsDataMapper.selectByDate(corpId,date);
}
/**
@ -170,7 +171,7 @@ public class WecomStatisticsService {
* @param statisticsData 统计数据
* @return 处理成功的数量
*/
public int handleAndSaveCustomerContactData(JSONObject statisticsData) {
public int handleAndSaveCustomerContactData(String corpId,JSONObject statisticsData) {
if (statisticsData == null || !statisticsData.containsKey("behavior_data")) {
return 0;
}
@ -189,7 +190,7 @@ public class WecomStatisticsService {
JSONObject data = behaviorData.getJSONObject("data");
// 处理每个成员的详细统计数据
successCount += handleCustomerContactData(userid, data);
successCount += handleCustomerContactData(corpId,userid, data);
}
} catch (Exception e) {
e.printStackTrace();
@ -206,7 +207,7 @@ public class WecomStatisticsService {
* @param data 统计数据
* @return 处理成功的数量
*/
private int handleCustomerContactData(String userid, JSONObject data) {
private int handleCustomerContactData(String corpId,String userid, JSONObject data) {
int successCount = 0;
// 处理每个统计时间点的数据
@ -217,7 +218,7 @@ public class WecomStatisticsService {
for (int i = 0; i < statDataArray.size(); i++) {
JSONObject statData = statDataArray.getJSONObject(i);
CustomerContactData contactData = createCustomerContactData(userid, statData);
CustomerContactData contactData = createCustomerContactData(corpId,userid, statData);
if (contactData != null) {
customerContactDataMapper.insert(contactData);
successCount++;
@ -234,9 +235,9 @@ public class WecomStatisticsService {
* @param statData 统计数据
* @return 客户联系统计数据实体
*/
private CustomerContactData createCustomerContactData(String userid, JSONObject statData) {
private CustomerContactData createCustomerContactData(String corpId,String userid, JSONObject statData) {
CustomerContactData contactData = new CustomerContactData();
contactData.setCorpId(corpId);
// 设置统计时间戳
if (statData.containsKey("stat_time")) {
contactData.setStatTime(statData.getLong("stat_time"));
@ -292,8 +293,8 @@ public class WecomStatisticsService {
* @param statDate 统计日期
* @return 客户联系统计数据列表
*/
public List<CustomerContactData> getCustomerContactDataByDate(Date statDate) {
return customerContactDataMapper.selectByStatDate(statDate);
public List<CustomerContactData> getCustomerContactDataByDate(String corpId,Date statDate) {
return customerContactDataMapper.selectByStatDate(corpId,statDate);
}
/**
@ -303,7 +304,7 @@ public class WecomStatisticsService {
* @param statDate 统计日期
* @return 客户联系统计数据列表
*/
public List<CustomerContactData> getCustomerContactDataByUseridAndDate(String userid, Date statDate) {
return customerContactDataMapper.selectByUseridAndStatDate(userid, statDate);
public List<CustomerContactData> getCustomerContactDataByUseridAndDate(String corpId,String userid, Date statDate) {
return customerContactDataMapper.selectByUseridAndStatDate(corpId,userid, statDate);
}
}

View File

@ -1,18 +1,16 @@
package com.ruoyi.excel.wecom.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.excel.wecom.domain.WecomTagDomain;
import com.ruoyi.excel.wecom.domain.WecomTagGroupDomain;
import com.ruoyi.excel.wecom.mapper.WecomTagMapper;
import com.ruoyi.excel.wecom.mapper.WecomTagGroupMapper;
import com.ruoyi.excel.wecom.mapper.WecomTagMapper;
import com.ruoyi.excel.wecom.model.WecomTag;
import com.ruoyi.excel.wecom.model.WecomTagGroup;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -26,6 +24,8 @@ public class WecomTagService {
private WecomTagGroupMapper wecomTagGroupMapper;
@Autowired
private WecomTagMapper wecomTagMapper;
@Autowired
private RedisCache redisCache;
/**
@ -34,7 +34,7 @@ public class WecomTagService {
* @param tagGroupList 标签库列表
* @return 同步结果
*/
public boolean syncCorpTagList(List<WecomTagGroup> tagGroupList) {
public boolean syncCorpTagList(List<WecomTagGroup> tagGroupList,String corpId) {
try {
// 遍历标签组列表
for (WecomTagGroup tagGroup : tagGroupList) {
@ -44,7 +44,14 @@ public class WecomTagService {
groupDomain.setName(tagGroup.getGroupName());
groupDomain.setCreateTime(new Date(tagGroup.getCreateTime() * 1000));
groupDomain.setOrderNo(tagGroup.getOrder());
wecomTagGroupMapper.insert(groupDomain);
groupDomain.setCorpId(corpId); // 设置企业ID
//保存标签组支持重复调用存在则更新不存在则插入
WecomTagGroupDomain existingGroup = wecomTagGroupMapper.selectByTagGroupId(corpId,groupDomain.getTagGroupId());
if (existingGroup != null) {
wecomTagGroupMapper.updateById(groupDomain);
} else {
wecomTagGroupMapper.insert(groupDomain);
}
// 保存标签
List<WecomTag> tagList = tagGroup.getTag();
if (tagList != null && !tagList.isEmpty()) {
@ -56,9 +63,15 @@ public class WecomTagService {
tagDomain.setOrderNo(tag.getOrder());
tagDomain.setCreateTime(new Date(tag.getCreateTime() * 1000));
tagDomain.setSyncTime(new Date());
tagDomain.setCorpId(corpId); // 设置企业ID
// 插入标签
wecomTagMapper.insert(tagDomain);
// 插入标签支持重复调用存在则更新不存在则插入
WecomTagDomain existingTag = wecomTagMapper.selectByTagId(corpId,tagDomain.getTagId());
if (existingTag != null) {
wecomTagMapper.updateById(tagDomain);
} else {
wecomTagMapper.insert(tagDomain);
}
}
}
}

View File

@ -0,0 +1,125 @@
package com.ruoyi.excel.wecom.service.impl;
import com.ruoyi.excel.wecom.domain.CorpDepartment;
import com.ruoyi.excel.wecom.dto.CorpDepartmentDTO;
import com.ruoyi.excel.wecom.mapper.CorpDepartmentMapper;
import com.ruoyi.excel.wecom.service.ICorpDepartmentService;
import com.ruoyi.excel.wecom.vo.CorpDepartmentVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class CorpDepartmentServiceImpl implements ICorpDepartmentService {
@Autowired
private CorpDepartmentMapper corpDepartmentMapper;
@Override
public List<CorpDepartmentVO> selectCorpDepartmentTree(String corpId) {
CorpDepartmentDTO query = new CorpDepartmentDTO();
query.setCorpId(corpId);
List<CorpDepartment> allDepartments = corpDepartmentMapper.selectCorpDepartmentList(query);
Map<Long, CorpDepartment> deptMap = new HashMap<>();
Map<Long, Long> parentMap = new HashMap<>();
for (CorpDepartment dept : allDepartments) {
deptMap.put(dept.getId(), dept);
parentMap.put(dept.getId(), dept.getParentid());
}
Map<Long, String> ancestorsMap = new HashMap<>();
for (CorpDepartment dept : allDepartments) {
String fullPath = buildDepartmentPath(dept.getId(), deptMap, parentMap);
ancestorsMap.put(dept.getId(), fullPath);
}
return buildTree(allDepartments, ancestorsMap);
}
private String buildDepartmentPath(Long deptId, Map<Long, CorpDepartment> deptMap, Map<Long, Long> parentMap) {
if (deptId == null || !deptMap.containsKey(deptId)) {
return "";
}
CorpDepartment dept = deptMap.get(deptId);
String currentName = dept.getName();
Long parentId = parentMap.get(deptId);
if (parentId == null || parentId == 0L || !parentMap.containsKey(parentId)) {
return currentName;
}
String parentPath = buildDepartmentPath(parentId, deptMap, parentMap);
if (parentPath.isEmpty()) {
return currentName;
} else {
return parentPath + "/" + currentName;
}
}
private List<CorpDepartmentVO> buildTree(List<CorpDepartment> departments, Map<Long, String> ancestorsMap) {
List<CorpDepartmentVO> result = new ArrayList<>();
List<Long> ids = departments.stream().map(CorpDepartment::getId).collect(Collectors.toList());
for (CorpDepartment dept : departments) {
Long parentId = dept.getParentid();
if (parentId == null || parentId == 0L || !ids.contains(parentId)) {
CorpDepartmentVO vo = convertToVO(dept, ancestorsMap.get(dept.getId()));
recursionFn(departments, vo, ancestorsMap);
result.add(vo);
}
}
if (result.isEmpty()) {
for (CorpDepartment dept : departments) {
result.add(convertToVO(dept, ancestorsMap.get(dept.getId())));
}
}
return result;
}
private CorpDepartmentVO convertToVO(CorpDepartment dept, String ancestors) {
CorpDepartmentVO vo = new CorpDepartmentVO();
vo.setId(dept.getId());
vo.setName(dept.getName());
vo.setAncestors(ancestors);
return vo;
}
private void recursionFn(List<CorpDepartment> list, CorpDepartmentVO parent, Map<Long, String> ancestorsMap) {
List<CorpDepartmentVO> childList = getChildList(list, parent, ancestorsMap);
parent.setChildren(childList);
for (CorpDepartmentVO child : childList) {
if (hasChild(list, child)) {
recursionFn(list, child, ancestorsMap);
}
}
}
private List<CorpDepartmentVO> getChildList(List<CorpDepartment> list, CorpDepartmentVO parent, Map<Long, String> ancestorsMap) {
List<CorpDepartmentVO> result = new ArrayList<>();
for (CorpDepartment dept : list) {
if (dept.getParentid() != null && dept.getParentid().longValue() == parent.getId().longValue()) {
CorpDepartmentVO vo = convertToVO(dept, ancestorsMap.get(dept.getId()));
result.add(vo);
}
}
return result;
}
private boolean hasChild(List<CorpDepartment> list, CorpDepartmentVO t) {
for (CorpDepartment dept : list) {
if (dept.getParentid() != null && dept.getParentid().longValue() == t.getId().longValue()) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,112 @@
package com.ruoyi.excel.wecom.service.impl;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.excel.wecom.domain.CorpInfo;
import com.ruoyi.excel.wecom.mapper.CorpInfoMapper;
import com.ruoyi.excel.wecom.service.ICorpInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* 企业信息Service业务层处理
*/
@Service
public class CorpInfoServiceImpl implements ICorpInfoService {
private static final String CURRENT_CORP_KEY_PREFIX_ONLY = "current_corp_only:";
@Autowired
private CorpInfoMapper corpInfoMapper;
@PostConstruct
public void setCorpInfoWhenOne() {
List<CorpInfo> corpInfos = corpInfoMapper.selectList(null);
if(corpInfos.size() == 1) {
RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
String corpId = corpInfos.get(0).getCorpId();
redisCache.setCacheObject(CURRENT_CORP_KEY_PREFIX_ONLY, corpId);
}
}
/**
* 查询企业信息列表
*
* @param corpInfo 企业信息
* @return 企业信息集合
*/
@Override
public List<CorpInfo> selectCorpInfoList(CorpInfo corpInfo) {
return corpInfoMapper.selectCorpInfoList(corpInfo);
}
/**
* 根据企业ID查询企业信息
*
* @param corpId 企业ID
* @return 企业信息
*/
@Override
public CorpInfo selectCorpInfoByCorpId(String corpId) {
return corpInfoMapper.selectCorpInfoByCorpId(corpId);
}
/**
* 根据ID查询企业信息
*
* @param id 主键ID
* @return 企业信息
*/
@Override
public CorpInfo selectCorpInfoById(Long id) {
return corpInfoMapper.selectById(id);
}
/**
* 新增企业信息
*
* @param corpInfo 企业信息
* @return 结果
*/
@Override
public int insertCorpInfo(CorpInfo corpInfo) {
return corpInfoMapper.insert(corpInfo);
}
/**
* 修改企业信息
*
* @param corpInfo 企业信息
* @return 结果
*/
@Override
public int updateCorpInfo(CorpInfo corpInfo) {
return corpInfoMapper.updateById(corpInfo);
}
/**
* 批量删除企业信息
*
* @param ids 需要删除的企业信息主键集合
* @return 结果
*/
@Override
public int deleteCorpInfoByIds(Long[] ids) {
int count = 0;
for (Long id : ids) {
count += corpInfoMapper.deleteById(id);
}
return count;
}
/**
* 删除企业信息
*
* @param id 企业信息主键
* @return 结果
*/
@Override
public int deleteCorpInfoById(Long id) {
return corpInfoMapper.deleteById(id);
}
}

View File

@ -27,8 +27,8 @@ public class CustomerContactDataServiceImpl implements ICustomerContactDataServi
* @return 客户联系统计数据列表
*/
@Override
public List<CustomerContactData> selectCustomerContactDataList(Date startDate, Date endDate, String userid) {
return customerContactDataMapper.selectCustomerContactDataList(startDate, endDate, userid);
public List<CustomerContactData> selectCustomerContactDataList(String corpId,Date startDate, Date endDate, String userid) {
return customerContactDataMapper.selectCustomerContactDataList(corpId,startDate, endDate, userid);
}
/**
@ -39,8 +39,8 @@ public class CustomerContactDataServiceImpl implements ICustomerContactDataServi
* @return 客户联系统计数据VO列表
*/
@Override
public List<CustomerContactDataVO> selectCustomerContactDataVOList(Date startDate, Date endDate, String userid) {
return customerContactDataMapper.selectCustomerContactDataVOList(startDate, endDate, userid);
public List<CustomerContactDataVO> selectCustomerContactDataVOList(String corpId,Date startDate, Date endDate, String userid) {
return customerContactDataMapper.selectCustomerContactDataVOList(corpId,startDate, endDate, userid);
}
/**
@ -53,15 +53,6 @@ public class CustomerContactDataServiceImpl implements ICustomerContactDataServi
return customerContactDataMapper.selectById(id);
}
/**
* 新增客户联系统计数据
* @param customerContactData 客户联系统计数据
* @return 结果
*/
@Override
public int insertCustomerContactData(CustomerContactData customerContactData) {
return customerContactDataMapper.insert(customerContactData);
}
/**
* 修改客户联系统计数据

View File

@ -27,8 +27,8 @@ public class CustomerExportDataServiceImpl implements ICustomerExportDataService
* @return 客户导出数据列表
*/
@Override
public List<CustomerExportData> selectCustomerExportDataList(Date startDate, Date endDate, String customerName) {
return customerExportDataMapper.selectCustomerExportDataList(startDate, endDate, customerName);
public List<CustomerExportData> selectCustomerExportDataList(String corpId,Date startDate, Date endDate, String customerName) {
return customerExportDataMapper.selectCustomerExportDataList(corpId,startDate, endDate, customerName);
}
/**
@ -39,8 +39,8 @@ public class CustomerExportDataServiceImpl implements ICustomerExportDataService
* @return 客户导出数据VO列表
*/
@Override
public List<CustomerExportDataVO> selectCustomerExportDataVOList(Date startDate, Date endDate, String customerName) {
return customerExportDataMapper.selectCustomerExportDataVOList(startDate, endDate, customerName);
public List<CustomerExportDataVO> selectCustomerExportDataVOList(String corpId,Date startDate, Date endDate, String customerName) {
return customerExportDataMapper.selectCustomerExportDataVOList(corpId,startDate, endDate, customerName);
}
/**

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

@ -26,11 +26,15 @@ public class DepartmentStatisticsDataServiceImpl implements IDepartmentStatistic
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径
* @param dataType 数据类型
* @return 部门统计数据列表
*/
@Override
public List<DepartmentStatisticsData> selectDepartmentStatisticsDataList(Date startDate, Date endDate, String departmentPath) {
return departmentStatisticsDataMapper.selectDepartmentStatisticsDataList(startDate, endDate, departmentPath);
public List<DepartmentStatisticsData> selectDepartmentStatisticsDataList(String corpId, Date startDate, Date endDate, String departmentPath, String dataType) {
if ("week".equals(dataType) || "month".equals(dataType)) {
return departmentStatisticsDataMapper.selectDepartmentStatisticsDataAggregatedList(corpId, startDate, endDate, departmentPath);
}
return departmentStatisticsDataMapper.selectDepartmentStatisticsDataList(corpId, startDate, endDate, departmentPath);
}
/**
@ -38,11 +42,15 @@ public class DepartmentStatisticsDataServiceImpl implements IDepartmentStatistic
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径
* @param dataType 数据类型
* @return 部门统计数据VO列表
*/
@Override
public List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOList(Date startDate, Date endDate, String departmentPath) {
return departmentStatisticsDataMapper.selectDepartmentStatisticsDataVOList(startDate, endDate, departmentPath);
public List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOList(String corpId, Date startDate, Date endDate, String departmentPath, String dataType) {
if ("week".equals(dataType) || "month".equals(dataType)) {
return departmentStatisticsDataMapper.selectDepartmentStatisticsDataVOAggregatedList(corpId, startDate, endDate, departmentPath);
}
return departmentStatisticsDataMapper.selectDepartmentStatisticsDataVOList(corpId, startDate, endDate, departmentPath);
}
/**
@ -90,7 +98,7 @@ public class DepartmentStatisticsDataServiceImpl implements IDepartmentStatistic
}
@Override
public Map<String, BigDecimal> getSummary(Date startDate, Date endDate, String departmentPath) {
return departmentStatisticsDataMapper.getSummary(startDate,endDate,departmentPath);
public Map<String, BigDecimal> getSummary(String corpId, Date startDate, Date endDate, String departmentPath, String dataType) {
return departmentStatisticsDataMapper.getSummary(corpId, startDate, endDate, departmentPath);
}
}

View File

@ -0,0 +1,19 @@
package com.ruoyi.excel.wecom.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class CorpDepartmentVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String ancestors;
private List<CorpDepartmentVO> children;
}

View File

@ -90,19 +90,16 @@ public class CustomerExportDataVO implements Serializable {
@ExcelProperty("标签组2(公司孵化)")
@Excel(name = "标签组2(公司孵化)")
@ColumnWidth(20)
private String tagGroup2;
@ExcelProperty("标签组3(商务)")
@Excel(name = "标签组3(商务))")
@ColumnWidth(20)
private String tagGroup3;
@ExcelProperty("标签组4(成交日期)")
@Excel(name = "标签组4(成交日期)")
@ColumnWidth(20)
private String tagGroup4;

View File

@ -1,26 +1,27 @@
package com.ruoyi.excel.wecom.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.Excel;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
/**
* 客户统计数据VO
* 流量看板数据VO
* 用于EasyExcel导出客户统计数据
*/
@Getter
@Setter
public class CustomerStatisticsDataVO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty("统计日期")
@Excel(name ="统计日期")
@DateTimeFormat("yyyy-MM-dd")
@Excel(name ="日期")
@ColumnWidth(20)
private Date curDate;
private String curDate;
@ExcelProperty("重要指标")
@Excel(name ="重要指标")
@ -30,37 +31,37 @@ public class CustomerStatisticsDataVO implements Serializable {
@ExcelProperty("N组")
@Excel(name ="N组(投放)")
@ColumnWidth(15)
private String nGroup;
private String ntfGroup;
@ExcelProperty("O组(公司孵化)")
@Excel(name ="O组(公司孵化)")
@ColumnWidth(20)
private String oGroup;
private String ofhGroup;
@ExcelProperty("P组(商务)")
@Excel(name ="P组(商务)")
@ColumnWidth(15)
private String pGroup;
private String pswGroup;
@ExcelProperty("W组(A1组)")
@Excel(name ="W组(A1组)")
@ColumnWidth(15)
private String wGroup;
private String wa1Group;
@ExcelProperty("X组(B1组)")
@Excel(name ="X组(B1组)")
@ColumnWidth(15)
private String xGroup;
private String xb1Group;
@ExcelProperty("Y组(C1组)")
@Excel(name ="Y组(C1组)")
@ColumnWidth(15)
private String yGroup;
private String yc1Group;
@ExcelProperty("Z组(D1组)")
@Excel(name ="Z组(D1组)")
@ColumnWidth(15)
private String zGroup;
private String zd1Group;
@ExcelProperty("AA组(E1组)")
@Excel(name ="AA组(E1组)")
@ -82,107 +83,13 @@ public class CustomerStatisticsDataVO implements Serializable {
@ColumnWidth(15)
private String aeGroup;
public Date getCurDate() {
return curDate;
}
public void setCurDate(Date curDate) {
this.curDate = curDate;
}
private String yearWeek;
public String getIndicatorName() {
return indicatorName;
}
private String yearMonth;
public void setIndicatorName(String indicatorName) {
this.indicatorName = indicatorName;
}
private String dateRange;
public String getnGroup() {
return nGroup;
}
private Integer sortNo;
public void setnGroup(String nGroup) {
this.nGroup = nGroup;
}
public String getoGroup() {
return oGroup;
}
public void setoGroup(String oGroup) {
this.oGroup = oGroup;
}
public String getpGroup() {
return pGroup;
}
public void setpGroup(String pGroup) {
this.pGroup = pGroup;
}
public String getwGroup() {
return wGroup;
}
public void setwGroup(String wGroup) {
this.wGroup = wGroup;
}
public String getxGroup() {
return xGroup;
}
public void setxGroup(String xGroup) {
this.xGroup = xGroup;
}
public String getyGroup() {
return yGroup;
}
public void setyGroup(String yGroup) {
this.yGroup = yGroup;
}
public String getzGroup() {
return zGroup;
}
public void setzGroup(String zGroup) {
this.zGroup = zGroup;
}
public String getAaGroup() {
return aaGroup;
}
public void setAaGroup(String aaGroup) {
this.aaGroup = aaGroup;
}
public String getAcGroup() {
return acGroup;
}
public void setAcGroup(String acGroup) {
this.acGroup = acGroup;
}
public String getAdGroup() {
return adGroup;
}
public void setAdGroup(String adGroup) {
this.adGroup = adGroup;
}
public String getAeGroup() {
return aeGroup;
}
public void setAeGroup(String aeGroup) {
this.aeGroup = aeGroup;
}
}

View File

@ -18,8 +18,8 @@ import java.util.Date;
public class DepartmentStatisticsDataVO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty("统计日期")
@Excel(name ="统计日期")
@ExcelProperty("日期")
@Excel(name ="日期",dateFormat = "yyyy-MM-dd")
@DateTimeFormat("yyyy-MM-dd")
@ColumnWidth(15)
private Date statDate;
@ -44,9 +44,23 @@ public class DepartmentStatisticsDataVO implements Serializable {
@ColumnWidth(15)
private BigDecimal dailyConversionRate;
@ExcelProperty("及时单占比(当日)")
@Excel(name ="及时单占比(当日)")
@ColumnWidth(15)
private BigDecimal dailyTimelyOrderRatio;
@ExcelProperty("排序号")
@Excel(name ="排序号")
@ColumnWidth(10)
private Integer sortNo;
@ExcelProperty("非及时单占比(当日")
@Excel(name ="非及时单占比(当日")
@ColumnWidth(15)
private BigDecimal dailyNonTimelyOrderRatio;
@ExcelProperty("家长成单率(当日")
@Excel(name ="家长成单率(当日")
@ColumnWidth(15)
private BigDecimal dailyParentOrderRate;
@ExcelProperty("学生成单率(当日")
@Excel(name ="学生成单率(当日")
@ColumnWidth(15)
private BigDecimal dailyStudentOrderRate;
}

View File

@ -0,0 +1,27 @@
<?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.CorpDepartmentMapper">
<resultMap id="CorpDepartmentResult" type="com.ruoyi.excel.wecom.domain.CorpDepartment">
<id property="id" column="id"/>
<result property="corpId" column="corp_id"/>
<result property="parentid" column="parentid"/>
<result property="orderNo" column="order_no"/>
<result property="name" column="name"/>
</resultMap>
<select id="selectCorpDepartmentList" parameterType="com.ruoyi.excel.wecom.dto.CorpDepartmentDTO" resultMap="CorpDepartmentResult">
SELECT id, corp_id, parentid, order_no, name
FROM corp_department
<where>
<if test="dto.corpId != null and dto.corpId != ''">
AND corp_id = #{dto.corpId}
</if>
<if test="dto.name != null and dto.name != ''">
AND name LIKE CONCAT('%', #{dto.name}, '%')
</if>
</where>
ORDER BY order_no ASC, id ASC
</select>
</mapper>

View File

@ -0,0 +1,35 @@
<?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.CorpInfoMapper">
<resultMap id="CorpInfoResult" type="com.ruoyi.excel.wecom.domain.CorpInfo">
<id property="id" column="id"/>
<result property="corpId" column="corp_id"/>
<result property="secret" column="secret"/>
<result property="name" column="name"/>
</resultMap>
<!-- 查询企业信息列表 -->
<select id="selectCorpInfoList" parameterType="com.ruoyi.excel.wecom.domain.CorpInfo" resultMap="CorpInfoResult">
SELECT id, corp_id, secret, name
FROM corp_info
<where>
<if test="corpId != null and corpId != ''">
AND corp_id = #{corpId}
</if>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY id DESC
</select>
<!-- 根据企业ID查询企业信息 -->
<select id="selectCorpInfoByCorpId" parameterType="String" resultMap="CorpInfoResult">
SELECT id, corp_id, secret, name
FROM corp_info
WHERE corp_id = #{corpId}
LIMIT 1
</select>
</mapper>

View File

@ -4,6 +4,10 @@
<select id="selectAllUserIds" resultType="java.lang.String">
select distinct userid from corp_user
select distinct userid from corp_user where corp_id = #{corpId}
</select>
<select id="selectByUserId" resultType="com.ruoyi.excel.wecom.domain.CorpUser">
select * from corp_user where userid = #{userid}
</select>
</mapper>

View File

@ -17,6 +17,7 @@
new_contact_cnt
FROM customer_contact_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
@ -44,6 +45,8 @@
new_contact_cnt as newContactCnt
FROM customer_contact_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
@ -59,12 +62,12 @@
<!-- 根据日期查询客户联系统计数据 -->
<select id="selectByStatDate" resultType="com.ruoyi.excel.wecom.domain.CustomerContactData">
SELECT * FROM customer_contact_data WHERE stat_date = #{statDate}
SELECT * FROM customer_contact_data WHERE stat_date = #{statDate} and corp_id = #{corpId}
</select>
<!-- 根据成员ID和日期查询客户联系统计数据 -->
<select id="selectByUseridAndStatDate" resultType="com.ruoyi.excel.wecom.domain.CustomerContactData">
SELECT * FROM customer_contact_data WHERE userid = #{userid} AND stat_date = #{statDate}
SELECT * FROM customer_contact_data WHERE userid = #{userid} AND stat_date = #{statDate} and corp_id = #{corpId}
</select>
</mapper>

View File

@ -29,11 +29,12 @@
<!-- 批量插入变更日志 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO customer_data_change_log (
corp_id,
history_id, customer_id, version, field_name, field_label,
old_value, new_value, change_time
) VALUES
<foreach collection="list" item="item" separator=",">
(
<foreach collection="changeLogs" item="item" separator=",">
(#{item.corpId},
#{item.historyId}, #{item.customerId}, #{item.version},
#{item.fieldName}, #{item.fieldLabel}, #{item.oldValue},
#{item.newValue}, #{item.changeTime}

View File

@ -1,10 +1,26 @@
<?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.CustomerExportDataMapper">
<select id="countCustomerExportDataVOList" resultType="int">
SELECT count(1)
FROM customer_export_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND add_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND add_time &lt;= #{endDate}
</if>
<if test="customerName != null">
AND customer_name like concat('%',#{customerName},'%')
</if>
</where>
</select>
<select id="getDistinctDate" resultType="java.util.Date">
select distinct add_date from customer_export_data order by add_date
select distinct add_date from customer_export_data where corp_id = #{corpId} order by add_date
</select>
<!-- 查询客户导出数据VO列表(用于导出) -->
@ -44,6 +60,7 @@
tag_group18 as tagGroup18
FROM customer_export_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND add_time &gt;= #{startDate}
</if>
@ -92,6 +109,7 @@
tag_group18 as tagGroup18
FROM customer_export_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND add_time &gt;= #{startDate}
</if>
@ -109,10 +127,627 @@
<select id="selectByUniqueKey" resultType="com.ruoyi.excel.wecom.domain.CustomerExportData">
SELECT *
FROM customer_export_data
WHERE customer_name = #{customerName}
WHERE
corp_id = #{corpId}
and
customer_user_id = #{customerUserId}
AND add_user_account = #{addUserAccount}
AND add_time = #{addTime}
LIMIT 1
</select>
<select id="selectByFinishDate" 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_group4'">
AND tag_group4 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group5'">
AND tag_group5 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group6'">
AND tag_group6 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group7'">
AND tag_group7 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group8'">
AND tag_group8 is not null
</if>
<if test="attr != null and attr != '' and attr == 'tag_group9'">
AND tag_group9 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_group15'">
AND tag_group15 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="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列表(用于异步导出) -->
<select id="selectCustomerExportDataVOListByPage" resultType="com.ruoyi.excel.wecom.vo.CustomerExportDataVO">
SELECT
customer_name as customerName,
description,
add_user_name as addUserName,
add_user_account as addUserAccount,
add_user_department as addUserDepartment,
add_time as addTime,
add_date as addDate,
source,
mobile,
company,
email,
address,
position,
phone,
tag_group1 as tagGroup1,
tag_group2 as tagGroup2,
tag_group3 as tagGroup3,
tag_group4 as tagGroup4,
tag_group5 as tagGroup5,
tag_group6 as tagGroup6,
tag_group7 as tagGroup7,
tag_group8 as tagGroup8,
tag_group9 as tagGroup9,
tag_group10 as tagGroup10,
tag_group11 as tagGroup11,
tag_group12 as tagGroup12,
tag_group13 as tagGroup13,
tag_group14 as tagGroup14,
tag_group15 as tagGroup15,
tag_group16 as tagGroup16,
tag_group17 as tagGroup17,
tag_group18 as tagGroup18
FROM customer_export_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND add_time &gt;= #{startDate}
</if>
<if test="endDate != null">
AND add_time &lt;= #{endDate}
</if>
<if test="customerName != null">
AND customer_name like concat('%',#{customerName},'%')
</if>
</where>
ORDER BY add_time DESC
LIMIT #{limit} OFFSET #{offset}
</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>
<!-- 查询未分组记录的成单数(所有组字段都为空) -->
<select id="selectUngroupedFinishCount" resultType="java.lang.Long">
SELECT COUNT(*)
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>
AND tag_group1 IS NULL AND tag_group2 IS NULL AND tag_group3 IS NULL
AND tag_group10 IS NULL AND tag_group11 IS NULL AND tag_group12 IS NULL
AND tag_group13 IS NULL AND tag_group14 IS NULL AND tag_group16 IS NULL
AND tag_group17 IS NULL AND tag_group18 IS NULL
</select>
<!-- 查询未分组记录的及时单数量 -->
<select id="selectUngroupedTimelyOrderCount" resultType="java.lang.Long">
SELECT COUNT(*)
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>
AND tag_group1 IS NULL AND tag_group2 IS NULL AND tag_group3 IS NULL
AND tag_group10 IS NULL AND tag_group11 IS NULL AND tag_group12 IS NULL
AND tag_group13 IS NULL AND tag_group14 IS NULL AND tag_group16 IS NULL
AND tag_group17 IS NULL AND tag_group18 IS NULL
</select>
<!-- 查询未分组记录的非及时单数量 -->
<select id="selectUngroupedNonTimelyOrderCount" resultType="java.lang.Long">
SELECT COUNT(*)
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>
AND tag_group1 IS NULL AND tag_group2 IS NULL AND tag_group3 IS NULL
AND tag_group10 IS NULL AND tag_group11 IS NULL AND tag_group12 IS NULL
AND tag_group13 IS NULL AND tag_group14 IS NULL AND tag_group16 IS NULL
AND tag_group17 IS NULL AND tag_group18 IS NULL
</select>
</mapper>

View File

@ -6,7 +6,7 @@
<select id="selectByDate" resultType="com.ruoyi.excel.wecom.vo.CustomerStatisticsVO">
select *
from customer_statistics_data
where tag_id = #{tagId}
where tag_id = #{tagId} and corp_id = #{corpId}
</select>
<!-- 查询客户统计数据列表 -->
@ -14,6 +14,7 @@
SELECT *
FROM customer_statistics_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
@ -33,6 +34,7 @@
*
FROM customer_statistics_data
<where>
corp_id = #{corpId} and hidden_flag = false
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
@ -46,4 +48,68 @@
ORDER BY cur_date DESC,sort_no
</select>
<select id="selectDailyDataByWeek" resultType="com.ruoyi.excel.wecom.domain.CustomerStatisticsData">
SELECT * FROM customer_statistics_data
<where>
corp_id = #{corpId}
AND YEAR(cur_date) = #{year}
AND WEEK(cur_date, 1) = #{week}
<if test="indicatorName != null and indicatorName != ''">
AND indicator_name LIKE CONCAT('%', #{indicatorName}, '%')
</if>
</where>
ORDER BY cur_date, sort_no
</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 * FROM customer_statistics_data
<where>
corp_id = #{corpId}
AND DATE_FORMAT(cur_date, '%Y-%m') = #{yearMonth}
<if test="indicatorName != null and indicatorName != ''">
AND indicator_name LIKE CONCAT('%', #{indicatorName}, '%')
</if>
</where>
ORDER BY cur_date, sort_no
</select>
<!-- 调试查询检查indicator_name是否有隐藏字符 -->
<select id="selectDebugIndicatorName" resultType="java.util.Map">
SELECT id, cur_date,
CONCAT('[', indicator_name, ']') as name_with_brackets,
LENGTH(indicator_name) as name_length
FROM customer_statistics_data
WHERE corp_id = #{corpId}
AND DATE_FORMAT(cur_date, '%Y-%m') = #{yearMonth}
<if test="indicatorName != null and indicatorName != ''">
AND indicator_name LIKE CONCAT('%', #{indicatorName}, '%')
</if>
ORDER BY cur_date
</select>
<select id="selectAllDailyData" resultType="com.ruoyi.excel.wecom.domain.CustomerStatisticsData">
SELECT * FROM customer_statistics_data
<where>
corp_id = #{corpId}
<if test="indicatorName != null and indicatorName != ''">
AND indicator_name LIKE CONCAT('%', #{indicatorName}, '%')
</if>
</where>
ORDER BY cur_date, sort_no
</select>
</mapper>

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

@ -6,7 +6,7 @@
<select id="selectByDate" resultType="com.ruoyi.excel.wecom.domain.DepartmentStatisticsData">
SELECT *
FROM department_statistics_data
WHERE stat_date = #{date}
WHERE corp_id = #{corpId} and stat_date = #{date}
ORDER BY department_path, sort_no
</select>
@ -14,7 +14,7 @@
<select id="selectByDepartmentAndDateRange" resultType="com.ruoyi.excel.wecom.domain.DepartmentStatisticsData">
SELECT *
FROM department_statistics_data
WHERE department_path = #{departmentPath}
WHERE corp_id = #{corpId} and department_path = #{departmentPath}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
@ -30,7 +30,7 @@
SUM(daily_total_accepted) as total_value,
COUNT(*) as record_count
FROM department_statistics_data
WHERE department_path = #{departmentPath}
WHERE corp_id = #{corpId} and department_path = #{departmentPath}
</select>
<!-- 查询指定部门的历史累计数据(总成单数) -->
@ -39,7 +39,7 @@
SUM(daily_total_orders) as total_value,
COUNT(*) as record_count
FROM department_statistics_data
WHERE department_path = #{departmentPath}
WHERE corp_id = #{corpId} and department_path = #{departmentPath}
</select>
<!-- 查询部门统计数据VO列表(用于导出) -->
@ -52,9 +52,20 @@
daily_conversion_rate as dailyConversionRate,
daily_timely_order_ratio as dailyTimelyOrderRatio,
daily_non_timely_order_ratio as dailyNonTimelyOrderRatio,
CASE
WHEN parent_daily_count > 0
THEN ROUND(parent_daily_order_count * 100.0 / parent_daily_count, 2)
ELSE 0
END as dailyParentOrderRate,
CASE
WHEN student_daily_count > 0
THEN ROUND(student_daily_order_count * 100.0 / student_daily_count, 2)
ELSE 0
END as dailyStudentOrderRate,
sort_no as sortNo
FROM department_statistics_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
@ -75,11 +86,32 @@
daily_total_accepted as dailyTotalAccepted,
daily_total_orders as dailyTotalOrders,
daily_conversion_rate as dailyConversionRate,
daily_timely_order_ratio as dailyTimelyOrderRatio,
daily_non_timely_order_ratio as dailyNonTimelyOrderRatio,
sort_no as sortNo
daily_timely_order_ratio as dailyTimelyOrderRatio,
daily_non_timely_order_ratio as dailyNonTimelyOrderRatio,
daily_timely_count as dailyTimelyCount,
daily_non_timely_count as dailyNonTimelyCount,
parent_daily_order_count as parentDailyOrderCount,
parent_daily_count as parentDailyCount,
student_daily_order_count as studentDailyOrderCount,
student_daily_count as studentDailyCount,
CASE
WHEN parent_daily_count > 0
THEN ROUND(parent_daily_order_count * 100.0 / parent_daily_count, 2)
ELSE 0
END as dailyParentOrderRate,
CASE
WHEN student_daily_count > 0
THEN ROUND(student_daily_order_count * 100.0 / student_daily_count, 2)
ELSE 0
END as dailyStudentOrderRate,
person_flag as personFlag,
corp_name as corpName,
department_name as departmentName,
group_name as groupName,
person_name as personName
FROM department_statistics_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
@ -98,6 +130,7 @@
sum(ifnull(daily_total_accepted,0)+ifnull(manager_accepted,0)) as totalIns
from department_statistics_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
@ -110,4 +143,112 @@
</where>
</select>
<select id="selectDepartmentStatisticsDataAggregatedList"
resultType="com.ruoyi.excel.wecom.domain.DepartmentStatisticsData">
SELECT
department_path as departmentPath,
SUM(daily_total_accepted) as dailyTotalAccepted,
SUM(daily_total_orders) as dailyTotalOrders,
CASE
WHEN SUM(daily_total_accepted) > 0
THEN ROUND(SUM(daily_total_orders) * 100.0 / SUM(daily_total_accepted), 2)
ELSE 0
END as dailyConversionRate,
CASE
WHEN SUM(daily_total_orders) > 0
THEN ROUND(SUM(IFNULL(daily_timely_count, 0)) * 100.0 / SUM(daily_total_orders), 2)
ELSE 0
END as dailyTimelyOrderRatio,
CASE
WHEN SUM(daily_total_orders) > 0
THEN ROUND(SUM(IFNULL(daily_non_timely_count, 0)) * 100.0 / SUM(daily_total_orders), 2)
ELSE 0
END as dailyNonTimelyOrderRatio,
SUM(IFNULL(daily_timely_count, 0)) as dailyTimelyCount,
SUM(IFNULL(daily_non_timely_count, 0)) as dailyNonTimelyCount,
SUM(IFNULL(parent_daily_order_count, 0)) as parentDailyOrderCount,
SUM(IFNULL(parent_daily_count, 0)) as parentDailyCount,
SUM(IFNULL(student_daily_order_count, 0)) as studentDailyOrderCount,
SUM(IFNULL(student_daily_count, 0)) as studentDailyCount,
CASE
WHEN SUM(IFNULL(parent_daily_count, 0)) > 0
THEN ROUND(SUM(IFNULL(parent_daily_order_count, 0)) * 100.0 / SUM(IFNULL(parent_daily_count, 0)), 2)
ELSE 0
END as dailyParentOrderRate,
CASE
WHEN SUM(IFNULL(student_daily_count, 0)) > 0
THEN ROUND(SUM(IFNULL(student_daily_order_count, 0)) * 100.0 / SUM(IFNULL(student_daily_count, 0)), 2)
ELSE 0
END as dailyStudentOrderRate,
person_flag as personFlag,
corp_name as corpName,
department_name as departmentName,
group_name as groupName,
person_name as personName,
MIN(sort_no) as sortNo
FROM department_statistics_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND stat_date &lt;= #{endDate}
</if>
<if test="departmentPath != null and departmentPath != ''">
AND department_path LIKE CONCAT(#{departmentPath}, '%')
</if>
</where>
GROUP BY department_path, person_flag, corp_name, department_name, group_name, person_name
ORDER BY department_path, sortNo
</select>
<select id="selectDepartmentStatisticsDataVOAggregatedList" resultType="com.ruoyi.excel.wecom.vo.DepartmentStatisticsDataVO">
SELECT
department_path as departmentPath,
SUM(daily_total_accepted) as dailyTotalAccepted,
SUM(daily_total_orders) as dailyTotalOrders,
CASE
WHEN SUM(daily_total_accepted) > 0
THEN ROUND(SUM(daily_total_orders) * 100.0 / SUM(daily_total_accepted), 2)
ELSE 0
END as dailyConversionRate,
CASE
WHEN SUM(daily_total_orders) > 0
THEN ROUND(SUM(IFNULL(daily_timely_count, 0)) * 100.0 / SUM(daily_total_orders), 2)
ELSE 0
END as dailyTimelyOrderRatio,
CASE
WHEN SUM(daily_total_orders) > 0
THEN ROUND(SUM(IFNULL(daily_non_timely_count, 0)) * 100.0 / SUM(daily_total_orders), 2)
ELSE 0
END as dailyNonTimelyOrderRatio,
CASE
WHEN SUM(IFNULL(parent_daily_count, 0)) > 0
THEN ROUND(SUM(IFNULL(parent_daily_order_count, 0)) * 100.0 / SUM(IFNULL(parent_daily_count, 0)), 2)
ELSE 0
END as dailyParentOrderRate,
CASE
WHEN SUM(IFNULL(student_daily_count, 0)) > 0
THEN ROUND(SUM(IFNULL(student_daily_order_count, 0)) * 100.0 / SUM(IFNULL(student_daily_count, 0)), 2)
ELSE 0
END as dailyStudentOrderRate,
MIN(sort_no) as sortNo
FROM department_statistics_data
<where>
corp_id = #{corpId}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND stat_date &lt;= #{endDate}
</if>
<if test="departmentPath != null and departmentPath != ''">
AND department_path LIKE CONCAT(#{departmentPath}, '%')
</if>
</where>
GROUP BY department_path
ORDER BY department_path, sortNo
</select>
</mapper>

View File

@ -4,10 +4,11 @@
<resultMap id="BaseResultMap" type="com.ruoyi.excel.wecom.domain.WecomTagGroupDomain">
<id column="id" property="id" />
<result column="corp_id" property="corpId" />
<result column="tag_group_id" property="tagGroupId" />
<result column="name" property="name" />
<result column="create_time" property="createTime" />
<result column="order_no" property="order" />
<result column="order_no" property="orderNo" />
<result column="sync_time" property="syncTime" />
</resultMap>
@ -20,7 +21,7 @@
select
<include refid="Base_Column_List" />
from wecom_tag_group
where tag_group_id = #{tagGroupId}
where corp_id = #{corpId} and tag_group_id = #{tagGroupId}
</select>
</mapper>

View File

@ -4,16 +4,17 @@
<resultMap id="BaseResultMap" type="com.ruoyi.excel.wecom.domain.WecomTagDomain">
<id column="id" property="id" />
<result column="corp_id" property="corpId" />
<result column="tag_id" property="tagId" />
<result column="tag_group_id" property="tagGroupId" />
<result column="name" property="name" />
<result column="create_time" property="createTime" />
<result column="order" property="order" />
<result column="order_no" property="orderNo" />
<result column="sync_time" property="syncTime" />
</resultMap>
<sql id="Base_Column_List">
id, tag_id, tag_group_id, name, create_time, order, sync_time
id, tag_id, tag_group_id, name, create_time, order_no, sync_time
</sql>
<!-- 根据标签ID查询标签 -->
@ -21,7 +22,7 @@
select
<include refid="Base_Column_List" />
from wecom_tag
where tag_id = #{tagId}
where corp_id = #{corpId} and tag_id = #{tagId}
</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';

63
frontend/nginx.conf Normal file
View File

@ -0,0 +1,63 @@
server {
listen 80;
server_name localhost;
charset utf-8;
client_max_body_size 20M;
# 前端静态资源
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
# 后端 API 代理
location /prod-api/ {
proxy_pass http://backend:8888/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
proxy_buffering off;
proxy_request_buffering off;
}
# 开发环境 API 代理
location /dev-api/ {
proxy_pass http://backend:8888/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
proxy_buffering off;
proxy_request_buffering off;
}
# 文件上传路径代理
location /profile/ {
proxy_pass http://backend:8888/profile/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 错误页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

View File

@ -10,7 +10,7 @@
<name>ruoyi</name>
<url>http://www.ruoyi.vip</url>
<description>超脑智子测试系统</description>
<description>苏州曼普后台系统</description>
<properties>
<ruoyi.version>3.9.1</ruoyi.version>

View File

@ -1,20 +1,14 @@
package com.ruoyi.web.controller.system;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.CorpContextHolder;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
@ -23,6 +17,15 @@ import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.List;
import java.util.Set;
/**
* 登录验证
@ -32,6 +35,9 @@ import com.ruoyi.system.service.ISysMenuService;
@RestController
public class SysLoginController
{
private static final String CURRENT_CORP_KEY_PREFIX = "current_corp:";
private static final String CURRENT_CORP_KEY_PREFIX_ONLY = "current_corp_only:";
@Autowired
private SysLoginService loginService;
@ -47,6 +53,9 @@ public class SysLoginController
@Autowired
private ISysConfigService configService;
@Autowired
private RedisCache redisCache;
/**
* 登录方法
*
@ -73,7 +82,21 @@ public class SysLoginController
public AjaxResult getInfo()
{
LoginUser loginUser = SecurityUtils.getLoginUser();
SysUser user = loginUser.getUser();
// 1. 获取当前登录用户ID
Long userId = user.getUserId();
// 2. Redis 获取该用户当前使用的企业ID
String key = CURRENT_CORP_KEY_PREFIX + userId;
String corpId = redisCache.getCacheObject(key);
if (corpId != null) {
CorpContextHolder.setCurrentCorpId(corpId);
} else {
//获取唯一的corpId
corpId = redisCache.getCacheObject(CURRENT_CORP_KEY_PREFIX_ONLY);
CorpContextHolder.setCurrentCorpId(corpId);
}
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合

View File

@ -0,0 +1,27 @@
package com.ruoyi.web.controller.wocom;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.CorpContextHolder;
import com.ruoyi.excel.wecom.service.ICorpDepartmentService;
import com.ruoyi.excel.wecom.vo.CorpDepartmentVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/wecom/department")
public class CorpDepartmentController {
@Autowired
private ICorpDepartmentService corpDepartmentService;
@GetMapping("/tree")
public AjaxResult getDepartmentTree() {
String corpId = CorpContextHolder.getCurrentCorpId();
List<CorpDepartmentVO> tree = corpDepartmentService.selectCorpDepartmentTree(corpId);
return AjaxResult.success(tree);
}
}

View File

@ -0,0 +1,124 @@
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.excel.wecom.domain.CorpInfo;
import com.ruoyi.excel.wecom.service.ICorpInfoService;
import com.ruoyi.common.utils.CorpContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 企业信息Controller
*/
@RestController
@RequestMapping("/wecom/corp")
public class CorpInfoController extends BaseController {
@Autowired
private ICorpInfoService corpInfoService;
/**
* 查询企业信息列表
*/
@PreAuthorize("@ss.hasPermi('wecom:corp:list')")
@GetMapping("/list")
public TableDataInfo list(CorpInfo corpInfo) {
startPage();
List<CorpInfo> list = corpInfoService.selectCorpInfoList(corpInfo);
return getDataTable(list);
}
/**
* 获取企业信息详细信息
*/
@PreAuthorize("@ss.hasPermi('wecom:corp:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return success(corpInfoService.selectCorpInfoById(id));
}
/**
* 新增企业信息
*/
@PreAuthorize("@ss.hasPermi('wecom:corp:add')")
@Log(title = "企业信息", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody CorpInfo corpInfo) {
return toAjax(corpInfoService.insertCorpInfo(corpInfo));
}
/**
* 修改企业信息
*/
@PreAuthorize("@ss.hasPermi('wecom:corp:edit')")
@Log(title = "企业信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody CorpInfo corpInfo) {
return toAjax(corpInfoService.updateCorpInfo(corpInfo));
}
/**
* 删除企业信息
*/
@PreAuthorize("@ss.hasPermi('wecom:corp:remove')")
@Log(title = "企业信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(corpInfoService.deleteCorpInfoByIds(ids));
}
/**
* 切换当前使用的企业
*/
@PostMapping("/switch/{corpId}")
public AjaxResult switchCorp(@PathVariable("corpId") String corpId) {
// 1. 验证企业是否存在
CorpInfo corpInfo = corpInfoService.selectCorpInfoByCorpId(corpId);
if (corpInfo == null) {
return error("企业不存在");
}
// 2. 设置当前企业到 Redis
CorpContextHolder.setCurrentCorpId(corpId);
return success("切换企业成功");
}
/**
* 获取当前使用的企业信息
*/
@GetMapping("/current")
public AjaxResult getCurrentCorp() {
try {
// 1. Redis 获取当前企业ID
String corpId = CorpContextHolder.getCurrentCorpId();
// 2. 查询企业信息
CorpInfo corpInfo = corpInfoService.selectCorpInfoByCorpId(corpId);
if (corpInfo == null) {
return error("当前企业不存在,请重新切换");
}
return success(corpInfo);
} catch (RuntimeException e) {
return error(e.getMessage());
}
}
/**
* 清除当前企业上下文
*/
@PostMapping("/clear")
public AjaxResult clearCurrentCorp() {
CorpContextHolder.clear();
return success("清除成功");
}
}

View File

@ -5,6 +5,7 @@ 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.CustomerContactData;
import com.ruoyi.excel.wecom.service.ICustomerContactDataService;
@ -37,8 +38,9 @@ public class CustomerContactDataController extends BaseController {
@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 = "userid", required = false) String userid) {
String corpId = CorpContextHolder.getCurrentCorpId();
startPage();
List<CustomerContactData> list = customerContactDataService.selectCustomerContactDataList(startDate, endDate, userid);
List<CustomerContactData> list = customerContactDataService.selectCustomerContactDataList(corpId,startDate, endDate, userid);
return getDataTable(list);
}
@ -52,7 +54,8 @@ public class CustomerContactDataController extends BaseController {
@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 = "userid", required = false) String userid) {
List<CustomerContactDataVO> list = customerContactDataService.selectCustomerContactDataVOList(startDate, endDate, userid);
String corpId = CorpContextHolder.getCurrentCorpId();
List<CustomerContactDataVO> list = customerContactDataService.selectCustomerContactDataVOList(corpId,startDate, endDate, userid);
ExcelUtil<CustomerContactDataVO> util = new ExcelUtil<>(CustomerContactDataVO.class);
util.exportExcel(response, list, "客户联系统计数据");
}
@ -66,16 +69,6 @@ public class CustomerContactDataController extends BaseController {
return success(customerContactDataService.selectCustomerContactDataById(id));
}
/**
* 新增客户联系统计数据
*/
@PreAuthorize("@ss.hasPermi('wecom:customerContact:add')")
@Log(title = "客户联系统计数据", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody CustomerContactData customerContactData) {
return toAjax(customerContactDataService.insertCustomerContactData(customerContactData));
}
/**
* 修改客户联系统计数据
*/

View File

@ -2,18 +2,26 @@ 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.CustomerExportData;
import com.ruoyi.excel.wecom.service.CustomerExportAsyncService;
import com.ruoyi.excel.wecom.service.ICustomerExportDataService;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import com.ruoyi.system.domain.SysExportTask;
import com.ruoyi.system.service.ISysExportTaskService;
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.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
@ -22,11 +30,17 @@ import java.util.List;
*/
@RestController
@RequestMapping("/wecom/customerExport")
public class CustomerExportDataController extends BaseController {
public class CustomerExportDataController extends BaseController
{
@Autowired
private ICustomerExportDataService customerExportDataService;
@Autowired
private CustomerExportAsyncService customerExportAsyncService;
@Autowired
private ISysExportTaskService exportTaskService;
/**
* 查询客户统计数据列表
*/
@ -35,14 +49,17 @@ public class CustomerExportDataController extends BaseController {
public TableDataInfo list(
@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 = "customerName", required = false) String customerName) {
@RequestParam(value = "customerName", required = false) String customerName)
{
String corpId = CorpContextHolder.getCurrentCorpId();
startPage();
List<CustomerExportData> list = customerExportDataService.selectCustomerExportDataList(startDate, endDate, customerName);
List<CustomerExportData> list = customerExportDataService.selectCustomerExportDataList(corpId, startDate, endDate, customerName);
return getDataTable(list);
}
/**
* 导出客户统计数据列表
* 导出客户统计数据列表同步方式保留兼容
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@Log(title = "客户统计数据", businessType = BusinessType.EXPORT)
@ -50,11 +67,149 @@ public class CustomerExportDataController extends BaseController {
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,
@RequestParam(value = "customerName", required = false) String customerName) {
List<CustomerExportDataVO> list = customerExportDataService.selectCustomerExportDataVOList(startDate, endDate, customerName);
@RequestParam(value = "customerName", required = false) String customerName)
{
String corpId = CorpContextHolder.getCurrentCorpId();
List<CustomerExportDataVO> list = customerExportDataService.selectCustomerExportDataVOList(corpId, startDate, endDate, customerName);
ExcelUtil<CustomerExportDataVO> util = new ExcelUtil<>(CustomerExportDataVO.class);
util.exportExcel(response, list, "客户统计数据");
}
/**
* 异步导出客户统计数据列表
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@Log(title = "客户统计数据-异步导出", businessType = BusinessType.EXPORT)
@PostMapping("/exportAsync")
public AjaxResult exportAsync(
@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 = "customerName", required = false) String customerName)
{
String corpId = CorpContextHolder.getCurrentCorpId();
SysExportTask task = new SysExportTask();
task.setTaskName("客户统计数据导出");
task.setTaskType("customer_export");
task.setCorpId(corpId);
task.setCreateBy(getUsername());
Long taskId = exportTaskService.createExportTask(task);
customerExportAsyncService.executeExport(taskId, corpId, startDate, endDate, customerName);
return AjaxResult.success("导出任务已创建,请稍后查看进度", taskId);
}
/**
* 查询导出任务状态
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@GetMapping("/export/status/{taskId}")
public AjaxResult getExportStatus(@PathVariable Long taskId)
{
SysExportTask task = exportTaskService.selectExportTaskById(taskId);
if (task == null)
{
return AjaxResult.error("任务不存在");
}
return AjaxResult.success(task);
}
/**
* 下载导出文件
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@GetMapping("/export/download/{taskId}")
public void downloadExportFile(@PathVariable Long taskId, HttpServletResponse response)
{
SysExportTask task = exportTaskService.selectExportTaskById(taskId);
if (task == null)
{
return;
}
if (!SysExportTask.STATUS_SUCCESS.equals(task.getStatus()))
{
return;
}
String filePath = task.getFilePath();
if (filePath == null || filePath.isEmpty())
{
return;
}
File file = new File(filePath);
if (!file.exists())
{
return;
}
try
{
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode(task.getFileName(), StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ";filename*=utf-8''" + fileName);
response.setHeader("Content-Length", String.valueOf(file.length()));
java.io.FileInputStream fis = new java.io.FileInputStream(file);
java.io.OutputStream os = response.getOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1)
{
os.write(buffer, 0, bytesRead);
}
fis.close();
os.flush();
}
catch (Exception e)
{
logger.error("下载文件失败", e);
}
}
/**
* 查询导出任务列表
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@GetMapping("/export/list")
public TableDataInfo exportTaskList(SysExportTask task)
{
String corpId = CorpContextHolder.getCurrentCorpId();
task.setCorpId(corpId);
task.setCreateBy(getUsername());
startPage();
List<SysExportTask> list = exportTaskService.selectExportTaskList(task);
return getDataTable(list);
}
/**
* 删除导出任务
*/
@PreAuthorize("@ss.hasPermi('wecom:customerExport:export')")
@Log(title = "客户统计数据-删除导出任务", businessType = BusinessType.DELETE)
@DeleteMapping("/export/{taskId}")
public AjaxResult deleteExportTask(@PathVariable Long taskId)
{
SysExportTask task = exportTaskService.selectExportTaskById(taskId);
if (task == null)
{
return AjaxResult.error("任务不存在");
}
if (task.getFilePath() != null && !task.getFilePath().isEmpty())
{
File file = new File(task.getFilePath());
if (file.exists())
{
file.delete();
}
}
return toAjax(exportTaskService.deleteExportTaskById(taskId));
}
}

View File

@ -5,8 +5,8 @@ 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.CustomerStatisticsData;
import com.ruoyi.excel.wecom.service.ICustomerStatisticsDataService;
import com.ruoyi.excel.wecom.vo.CustomerStatisticsDataVO;
import org.springframework.beans.factory.annotation.Autowired;
@ -35,12 +35,30 @@ public class CustomerStatisticsDataController extends BaseController {
@PreAuthorize("@ss.hasPermi('wecom:customerStatistics: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 = "indicatorName", required = false) String indicatorName) {
startPage();
List<CustomerStatisticsData> list = customerStatisticsDataService.selectCustomerStatisticsDataList(startDate, endDate, indicatorName);
return getDataTable(list);
String corpId = CorpContextHolder.getCurrentCorpId();
List<CustomerStatisticsDataVO> list;
if ("week".equals(dataType)) {
list = customerStatisticsDataService.selectByWeekAggregation(corpId, year, week, indicatorName);
return getDataTable(list);
} else if ("month".equals(dataType)) {
list = customerStatisticsDataService.selectByMonthAggregation(corpId, yearMonth, indicatorName);
return getDataTable(list);
} else if ("all".equals(dataType)) {
list = customerStatisticsDataService.selectAllAggregation(corpId, indicatorName);
return getDataTable(list);
} else {
startPage();
list = customerStatisticsDataService.selectCustomerStatisticsDataVOList(corpId, startDate, endDate, indicatorName);
return getDataTable(list);
}
}
/**
@ -50,12 +68,28 @@ public class CustomerStatisticsDataController extends BaseController {
@Log(title = "客户统计数据", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response,
@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 = "indicatorName", required = false) String indicatorName) {
List<CustomerStatisticsDataVO> list = customerStatisticsDataService.selectCustomerStatisticsDataVOList(startDate, endDate, indicatorName);
String corpId = CorpContextHolder.getCurrentCorpId();
List<CustomerStatisticsDataVO> dataList;
if ("week".equals(dataType)) {
dataList = customerStatisticsDataService.selectByWeekAggregation(corpId, year, week, indicatorName);
} else if ("month".equals(dataType)) {
dataList = customerStatisticsDataService.selectByMonthAggregation(corpId, yearMonth, indicatorName);
} else if ("all".equals(dataType)) {
dataList = customerStatisticsDataService.selectAllAggregation(corpId, indicatorName);
} else {
dataList = customerStatisticsDataService.selectCustomerStatisticsDataVOList(corpId, startDate, endDate, indicatorName);
}
ExcelUtil<CustomerStatisticsDataVO> util = new ExcelUtil<>(CustomerStatisticsDataVO.class);
util.exportExcel(response, list, "流量看板数据");
util.exportExcel(response, dataList, "流量看板数据");
}
/**
@ -75,10 +109,11 @@ public class CustomerStatisticsDataController extends BaseController {
@Log(title = "客户统计数据", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestParam(value = "curDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date curDate,
@RequestParam(value = "totalCost")BigDecimal totalCost,
@RequestParam(value = "cost")BigDecimal cost,
@RequestParam(value = "type")String type,
@RequestParam(value = "attr")String attr) {
return toAjax(customerStatisticsDataService.updateCost(curDate,totalCost,attr));
String corpId = CorpContextHolder.getCurrentCorpId();
return toAjax(customerStatisticsDataService.updateCost(corpId,curDate,cost,type,attr));
}
}

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

@ -5,6 +5,7 @@ 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.DepartmentStatisticsData;
import com.ruoyi.excel.wecom.service.IDepartmentStatisticsDataService;
@ -17,7 +18,9 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -39,9 +42,19 @@ public class DepartmentStatisticsDataController extends BaseController {
public TableDataInfo list(
@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 = "departmentPath", required = false) String departmentPath) {
@RequestParam(value = "departmentPath", required = false) String departmentPath,
@RequestParam(value = "dataType", required = false) String dataType,
@RequestParam(value = "year", required = false) Integer year,
@RequestParam(value = "week", required = false) Integer week,
@RequestParam(value = "month", required = false) Integer month) {
String corpId = CorpContextHolder.getCurrentCorpId();
Date[] dateRange = calculateDateRange(dataType, year, week, month, startDate, endDate);
startDate = dateRange[0];
endDate = dateRange[1];
startPage();
List<DepartmentStatisticsData> list = departmentStatisticsDataService.selectDepartmentStatisticsDataList(startDate, endDate, departmentPath);
List<DepartmentStatisticsData> list = departmentStatisticsDataService.selectDepartmentStatisticsDataList(corpId, startDate, endDate, departmentPath, dataType);
return getDataTable(list);
}
@ -54,8 +67,18 @@ public class DepartmentStatisticsDataController extends BaseController {
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,
@RequestParam(value = "departmentPath", required = false) String departmentPath) {
List<DepartmentStatisticsDataVO> list = departmentStatisticsDataService.selectDepartmentStatisticsDataVOList(startDate, endDate, departmentPath);
@RequestParam(value = "departmentPath", required = false) String departmentPath,
@RequestParam(value = "dataType", required = false) String dataType,
@RequestParam(value = "year", required = false) Integer year,
@RequestParam(value = "week", required = false) Integer week,
@RequestParam(value = "month", required = false) Integer month) {
String corpId = CorpContextHolder.getCurrentCorpId();
Date[] dateRange = calculateDateRange(dataType, year, week, month, startDate, endDate);
startDate = dateRange[0];
endDate = dateRange[1];
List<DepartmentStatisticsDataVO> list = departmentStatisticsDataService.selectDepartmentStatisticsDataVOList(corpId, startDate, endDate, departmentPath, dataType);
ExcelUtil<DepartmentStatisticsDataVO> util = new ExcelUtil<>(DepartmentStatisticsDataVO.class);
util.exportExcel(response, list, "销售看板数据");
}
@ -78,9 +101,20 @@ public class DepartmentStatisticsDataController extends BaseController {
public AjaxResult summary(
@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 = "departmentPath", required = false) String departmentPath) {
@RequestParam(value = "departmentPath", required = false) String departmentPath,
@RequestParam(value = "dataType", required = false) String dataType,
@RequestParam(value = "year", required = false) Integer year,
@RequestParam(value = "week", required = false) Integer week,
@RequestParam(value = "month", required = false) Integer month) {
String corpId = CorpContextHolder.getCurrentCorpId();
Map<String, BigDecimal> map = departmentStatisticsDataService.getSummary(startDate, endDate, departmentPath);
Date[] dateRange = calculateDateRange(dataType, year, week, month, startDate, endDate);
startDate = dateRange[0];
endDate = dateRange[1];
Map<String, BigDecimal> map = departmentStatisticsDataService.getSummary(corpId, startDate, endDate, departmentPath, dataType);
if(map == null) {
return success(new HashMap<>());
}
BigDecimal totalOrders = map.get("totalOrders");
BigDecimal totalIns = map.get("totalIns");
if(totalOrders == null) {
@ -98,4 +132,60 @@ public class DepartmentStatisticsDataController extends BaseController {
}
return success(map);
}
/**
* 根据数据类型计算日期范围
* @param dataType 数据类型week/month/all
* @param year 年份
* @param week 周数(1-53)
* @param month 月份(1-12)
* @param startDate 原始开始日期
* @param endDate 原始结束日期
* @return 日期数组[startDate, endDate]
*/
private Date[] calculateDateRange(String dataType, Integer year, Integer week, Integer month, Date startDate, Date endDate) {
if (dataType == null || dataType.isEmpty()) {
return new Date[]{startDate, endDate};
}
if ("all".equals(dataType)) {
return new Date[]{null, null};
}
Calendar cal = Calendar.getInstance();
if ("week".equals(dataType) && year != null && week != null) {
cal.set(Calendar.YEAR, year);
cal.set(Calendar.WEEK_OF_YEAR, week);
cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
startDate = cal.getTime();
cal.add(Calendar.DAY_OF_MONTH, 6);
endDate = cal.getTime();
return new Date[]{startDate, endDate};
}
if ("month".equals(dataType) && year != null && month != null) {
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month - 1);
cal.set(Calendar.DAY_OF_MONTH, 1);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
startDate = cal.getTime();
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
endDate = cal.getTime();
return new Date[]{startDate, endDate};
}
return new Date[]{startDate, endDate};
}
}

View File

@ -4,24 +4,22 @@ import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.excel.wecom.domain.CorpDepartment;
import com.ruoyi.excel.wecom.domain.CorpUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.CorpContextHolder;
import com.ruoyi.excel.wecom.helper.HandleAllData;
import com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper;
import com.ruoyi.excel.wecom.mapper.CustomerStatisticsDataMapper;
import com.ruoyi.excel.wecom.mapper.DepartmentStatisticsDataMapper;
import com.ruoyi.excel.wecom.model.WecomConfig;
import com.ruoyi.excel.wecom.model.WecomCustomer;
import com.ruoyi.excel.wecom.model.WecomTagGroup;
import com.ruoyi.excel.wecom.service.CustomerExportService;
import com.ruoyi.excel.wecom.service.WecomContactService;
import com.ruoyi.excel.wecom.service.WecomTagService;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import com.ruoyi.excel.wecom.vo.CustomerStatisticsDataVO;
import com.ruoyi.excel.wecom.vo.DepartmentStatisticsDataVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -43,9 +41,6 @@ public class WecomContactController {
@Autowired
private HandleAllData handleAllData;
@Autowired
private CustomerExportService customerExportService;
@Autowired
private CustomerExportDataMapper customerExportDataMapper;
@ -54,246 +49,60 @@ public class WecomContactController {
@Autowired
private CustomerStatisticsDataMapper customerStatisticsDataMapper;
/**
* 获取配置了客户联系功能的成员列表
*
* @param request HttpServletRequest
* @return AjaxResult
*/
@GetMapping("/followUserList")
public AjaxResult getFollowUserList(HttpServletRequest request) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<String> userList = contactService.getFollowUserList();
return AjaxResult.success("获取成功", userList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取指定成员的客户列表
*
* @param request HttpServletRequest
* @param userid 成员ID
* @return AjaxResult
*/
@GetMapping("/externalContactList")
public AjaxResult getExternalContactList(HttpServletRequest request, @RequestParam String userid) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<String> customerList = contactService.getExternalContactList(userid);
return AjaxResult.success("获取成功", customerList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
@Autowired
private RedisCache redisCache;
/**
* 获取所有成员的客户列表
*
* @param request HttpServletRequest
* @return AjaxResult
*/
@GetMapping("/allMembersCustomers")
public AjaxResult getAllMembersCustomers(HttpServletRequest request) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<WecomContactService.MemberCustomer> memberCustomerList = contactService.getAllMembersCustomers();
return AjaxResult.success("获取成功", memberCustomerList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 批量获取客户详情
*
* @param request HttpServletRequest
* @return AjaxResult
*/
@PostMapping("/batchCustomerDetails")
public AjaxResult batchGetCustomerDetails(HttpServletRequest request, @RequestBody List<String> useridList) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
// 解析成员ID列表
List<WecomCustomer> customerList = contactService.batchGetAllCustomerDetails(useridList);
return AjaxResult.success("获取成功", customerList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取单个客户详情
*
* @param request HttpServletRequest
* @param externalUserid 外部联系人ID
* @return AjaxResult
*/
@GetMapping("/customerDetail")
public AjaxResult getCustomerDetail(HttpServletRequest request, @RequestParam String externalUserid) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
WecomCustomer customer = contactService.getCustomerDetail(externalUserid);
return AjaxResult.success("获取成功", customer);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取企业标签库
*
* @param request HttpServletRequest
* @return AjaxResult
*/
@GetMapping("/corpTagList")
public AjaxResult getCorpTagList(HttpServletRequest request) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<WecomTagGroup> tagGroupList = contactService.getCorpTagList();
// 同步标签库到数据库
boolean syncResult = wecomTagService.syncCorpTagList(tagGroupList);
if (syncResult) {
return AjaxResult.success("获取成功并同步到数据库", tagGroupList);
} else {
return AjaxResult.success("获取成功但同步到数据库失败", tagGroupList);
}
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取企业微信配置
*
* @param request HttpServletRequest
* @return WecomConfig
*/
private WecomConfig getWecomConfig(HttpServletRequest request) {
// 这里可以从请求参数配置文件或数据库中获取配置
// 暂时使用硬编码的方式实际使用时应该从配置中获取
WecomConfig config = new WecomConfig();
return config;
}
/**
* 获取子部门id列表
*
* @param request HttpServletRequest
* @param departmentId 部门ID根部门传1
* @return AjaxResult
*/
@GetMapping("/departmentList")
public AjaxResult getDepartmentList(HttpServletRequest request, @RequestParam(required = false) Long departmentId) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<CorpDepartment> deptIdList = contactService.getDepartmentList(departmentId);
return AjaxResult.success("获取成功", deptIdList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取单个部门详情
*
* @param request HttpServletRequest
* @param departmentId 部门ID
* @return AjaxResult
*/
@GetMapping("/departmentDetail")
public AjaxResult getDepartmentDetail(HttpServletRequest request, @RequestParam Long departmentId) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
Object deptDetail = contactService.getDepartmentDetail(departmentId);
return AjaxResult.success("获取成功", deptDetail);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取部门成员详情
*
* @param request HttpServletRequest
* @param departmentId 部门ID
* @param fetchChild 是否递归获取子部门成员
* @param status 成员状态0-全部1-已激活2-已禁用4-未激活5-退出企业
* @return AjaxResult
*/
@GetMapping("/departmentMemberList")
public AjaxResult getDepartmentMemberList(HttpServletRequest request, @RequestParam Long departmentId,
@RequestParam(required = false) Integer fetchChild,
@RequestParam(required = false) Integer status) {
try {
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<CorpUser> memberList = contactService.getDepartmentMemberList(departmentId, fetchChild, status);
return AjaxResult.success("获取成功", memberList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
@GetMapping("/init")
public AjaxResult init(HttpServletRequest request) throws IOException {
handleAllData.initData();
return AjaxResult.success();
try {
handleAllData.initData();
return AjaxResult.success();
} finally {
CorpContextHolder.clear();
}
}
@GetMapping("/test")
public AjaxResult test(HttpServletRequest request) throws IOException {
handleAllData.handleAllData();
return AjaxResult.success();
try {
handleAllData.handleAllData();
return AjaxResult.success();
} finally {
CorpContextHolder.clear();
}
}
@GetMapping("/createAllReportData")
public AjaxResult createAllReportData(HttpServletRequest request) throws IOException {
handleAllData.createAllReportData();
return AjaxResult.success();
try {
handleAllData.createAllReportData();
return AjaxResult.success();
} finally {
CorpContextHolder.clear();
}
}
@GetMapping("/createAllDepartmentReportData")
public AjaxResult createAllDepartmentReportData(HttpServletRequest request) throws IOException {
handleAllData.createAllDepartmentReportData();
return AjaxResult.success();
try {
handleAllData.createAllDepartmentReportData();
return AjaxResult.success();
} finally {
CorpContextHolder.clear();
}
}
@GetMapping("/createAllDepartmentReportByDate")
public AjaxResult createAllDepartmentReportByDate(@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date date) throws IOException {
handleAllData.createDepartmentReportData(date);
return AjaxResult.success();
try {
String corpId = CorpContextHolder.getCurrentCorpId();
handleAllData.createDepartmentReportData(corpId,date);
return AjaxResult.success();
} finally {
CorpContextHolder.clear();
}
}
/**
@ -309,6 +118,8 @@ public class WecomContactController {
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
try {
String corpId = CorpContextHolder.getCurrentCorpId();
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
@ -320,7 +131,7 @@ public class WecomContactController {
WriteSheet writeSheet = EasyExcel.writerSheet("客户数据").build();
// 分批查询并写入,避免内存溢出
List<CustomerExportDataVO> dataList = customerExportDataMapper.selectCustomerExportDataVOList(startDate, endDate,null);
List<CustomerExportDataVO> dataList = customerExportDataMapper.selectCustomerExportDataVOList(corpId,startDate, endDate,null);
excelWriter.write(dataList, writeSheet);
excelWriter.finish();
@ -333,6 +144,8 @@ public class WecomContactController {
} catch (IOException ioException) {
ioException.printStackTrace();
}
} finally {
CorpContextHolder.clear();
}
}
@ -351,6 +164,8 @@ public class WecomContactController {
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(required = false) String indicatorName) {
try {
String corpId = CorpContextHolder.getCurrentCorpId();
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
@ -362,7 +177,7 @@ public class WecomContactController {
WriteSheet writeSheet = EasyExcel.writerSheet("流量看板统计数据").build();
// 分批查询并写入,避免内存溢出
List<CustomerStatisticsDataVO> dataList = customerStatisticsDataMapper.selectCustomerStatisticsDataVOList(
List<CustomerStatisticsDataVO> dataList = customerStatisticsDataMapper.selectCustomerStatisticsDataVOList(corpId,
startDate, endDate, indicatorName);
excelWriter.write(dataList, writeSheet);
excelWriter.finish();
@ -376,6 +191,8 @@ public class WecomContactController {
} catch (IOException ioException) {
ioException.printStackTrace();
}
} finally {
CorpContextHolder.clear();
}
}
@ -394,6 +211,8 @@ public class WecomContactController {
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(required = false) String departmentPath) {
try {
String corpId = CorpContextHolder.getCurrentCorpId();
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
@ -405,7 +224,7 @@ public class WecomContactController {
WriteSheet writeSheet = EasyExcel.writerSheet("部门统计数据").build();
// 分批查询并写入,避免内存溢出
List<DepartmentStatisticsDataVO> dataList = departmentStatisticsDataMapper.selectDepartmentStatisticsDataVOList(
List<DepartmentStatisticsDataVO> dataList = departmentStatisticsDataMapper.selectDepartmentStatisticsDataVOList(corpId,
startDate, endDate, departmentPath);
excelWriter.write(dataList, writeSheet);
excelWriter.finish();
@ -419,6 +238,8 @@ public class WecomContactController {
} catch (IOException ioException) {
ioException.printStackTrace();
}
} finally {
CorpContextHolder.clear();
}
}

View File

@ -2,6 +2,7 @@ package com.ruoyi.web.controller.wocom;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.CorpContextHolder;
import com.ruoyi.excel.wecom.service.WecomContactService;
import com.ruoyi.excel.wecom.service.WecomStatisticsService;
import org.springframework.beans.factory.annotation.Autowired;
@ -33,7 +34,8 @@ public class WecomStatisticsController {
@PostMapping("/customer/{date}")
public AjaxResult getCustomerStatistics(@PathVariable("date") String date, @RequestParam(value = "useridList", required = false) List<String> useridList) {
try {
JSONObject statisticsData = wecomContactService.getDayUserBehaviorData(date, useridList);
String corpId = CorpContextHolder.getCurrentCorpId();
JSONObject statisticsData = wecomContactService.getDayUserBehaviorData(corpId,date, useridList);
return AjaxResult.success("获取联系客户统计数据成功", statisticsData);
} catch (IOException e) {
e.printStackTrace();
@ -51,13 +53,15 @@ public class WecomStatisticsController {
@PostMapping("/customer/{date}/save")
public AjaxResult saveCustomerStatistics(@PathVariable("date") String date, @RequestParam(value = "useridList", required = false) List<String> useridList) {
try {
JSONObject statisticsData = wecomContactService.getDayUserBehaviorData(date, useridList);
String corpId = CorpContextHolder.getCurrentCorpId();
JSONObject statisticsData = wecomContactService.getDayUserBehaviorData(corpId,date, useridList);
// 存储汇总统计数据
int summaryCount = wecomStatisticsService.handleAndSaveCustomerStatistics(date, statisticsData);
int summaryCount = wecomStatisticsService.handleAndSaveCustomerStatistics(corpId,date, statisticsData);
// 存储详细联系统计数据
int contactCount = wecomStatisticsService.handleAndSaveCustomerContactData(statisticsData);
int contactCount = wecomStatisticsService.handleAndSaveCustomerContactData(corpId,statisticsData);
return AjaxResult.success("获取并存储联系客户统计数据成功,共处理 " + summaryCount + " 条汇总数据," + contactCount + " 条详细数据", statisticsData);
} catch (IOException e) {

View File

@ -13,9 +13,9 @@ spring:
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
@ -39,7 +39,7 @@ spring:
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
webStatFilter:
enabled: true
statViewServlet:
enabled: true
@ -58,4 +58,26 @@ spring:
merge-sql: true
wall:
config:
multi-statement-allow: true
multi-statement-allow: true
redis:
# 地址
host: host.docker.internal
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms

View File

@ -6,7 +6,7 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://host.docker.internal:3316/excel-handle?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
url: jdbc:mysql://localhost:3306/excel-handle?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: jiong1114
# 从库数据源
@ -58,4 +58,25 @@ spring:
merge-sql: true
wall:
config:
multi-statement-allow: true
multi-statement-allow: true
redis:
# 地址
host: localhost
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms

Some files were not shown because too many files have changed in this diff Show More