初始化提交

This commit is contained in:
MerCry 2026-02-07 23:55:56 +08:00
commit a197c7e09b
702 changed files with 77808 additions and 0 deletions

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
######################################################################
# Build Tools
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
target/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
# IDE
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### JRebel ###
rebel.xml
### NetBeans ###
nbproject/private/
build/*
nbbuild/
dist/
nbdist/
.nb-gradle/
######################################################################
# Others
*.log
*.xml.versionsBackup
*.swp
!*/build/*.java
!*/build/*.html
!*/build/*.xml
/.idea/

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2018 RuoYi
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

109
README.md Normal file
View File

@ -0,0 +1,109 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.1</h1>
<h4 align="center">基于SpringBoot+Vue前后端分离的Java快速开发框架</h4>
<p align="center">
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.1-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
</p>
## 平台简介
超脑智子是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 前端采用Vue、Element UI。
* 后端采用Spring Boot、Spring Security、Redis & Jwt。
* 权限认证使用Jwt支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。
* 高效率开发,使用代码生成器可以一键生成前后端代码。
* 提供了技术栈Vue3 Element Plus Vite的 [RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3)版本以及技术栈TypeScript的 [RuoYi-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Vue3/tree/typescript)版本,两者保持同步更新。
* 提供了适配 Spring Boot 3 的版本分支 [RuoYi-Vue (springboot3)](https://gitee.com/y_project/RuoYi-Vue/tree/springboot3),以及特定需求的 单应用版本 [RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast) 与 Oracle数据库版本 [RuoYi-Vue-Oracle](https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle),均保持同步更新。
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
13. 代码生成前后端代码的生成java、html、xml、sql支持CRUD下载 。
14. 系统接口根据业务代码自动生成相关的api接口文档。
15. 服务监控监视当前系统CPU、内存、磁盘、堆栈等相关信息。
16. 缓存监控:对系统的缓存信息查询,命令统计等。
17. 在线构建器拖动表单元素生成相应的HTML代码。
18. 连接池监视监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈。
# 版本对比
RuoYi-Vue 前端项目的三个主要演进版本,方便你直观对比其技术栈差异(并行开发维护)。
| 项目名称 | **RuoYi-Vue** | **RuoYi-Vue3** | **RuoYi-Vue3-TypeScript** |
| :--- | :--- | :--- | :--- |
| **前端框架** | Vue 2 | Vue 3 | Vue 3 |
| **脚本语言** | JavaScript | JavaScript | TypeScript |
| **构建工具** | Vue CLI | Vite | Vite |
| **UI 组件库** | Element UI | Element Plus | Element Plus |
| **状态管理** | Vuex | Pinia | Pinia |
| **路由管理** | Vue Router 3 | Vue Router 4 | Vue Router 4 |
| **核心特点** | 1. 技术栈经典稳定<br>2. 社区资料丰富<br>3. 当前维护重心已转移 | 1. 现代前端技术栈<br>2. 开发体验与性能更优<br>3. 官方主推的活跃版本 | 1. 类型加持,减少沟通成本<br>2. 开发时有提示,效率更高<br>3. 多人协作企业级开发项目 |
| **仓库地址** | [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) | [RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3) | [RuoYi-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Vue3/tree/typescript) |
## 在线体验
- admin/admin123
- 陆陆续续收到一些打赏,为了更好的体验已用于演示服务器升级。谢谢各位小伙伴。
演示地址http://vue.ruoyi.vip
文档地址http://doc.ruoyi.vip
## 演示图
<table>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-8074972883b5ba0622e13246738ebba237a.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-9f88719cdfca9af2e58b352a20e23d43b12.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-39bf2584ec3a529b0d5a3b70d15c9b37646.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-936ec82d1f4872e1bc980927654b6007307.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-b2d62ceb95d2dd9b3fbe157bb70d26001e9.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-d67451d308b7a79ad6819723396f7c3d77a.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-8370a0d02977eebf6dbf854c8450293c937.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-49003ed83f60f633e7153609a53a2b644f7.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-d4fe726319ece268d4746602c39cffc0621.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
</tr>
</table>
## 超脑智子前后端分离交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/已满-191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [![加入QQ群](https://img.shields.io/badge/已满-174569686-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) [![加入QQ群](https://img.shields.io/badge/127358632-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=M9y5NjAl44lAL_Vh2crmEehZU_PMU6KS&authKey=ZSDz8hEREWSaPuxQV3gEwqGIaGjfRNnkB4rJjf0IvXhrSUGSGwQFmBA%2Boe8HFxyl&noverify=0&group_code=127358632) 点击按钮入群。

12
bin/clean.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 清理工程target生成路径。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean
pause

12
bin/package.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 打包Web工程生成war/jar包文件。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean package -Dmaven.test.skip=true
pause

14
bin/run.bat Normal file
View File

@ -0,0 +1,14 @@
@echo off
echo.
echo [信息] 使用Jar命令运行Web工程。
echo.
cd %~dp0
cd ../ruoyi-admin/target
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
java -jar %JAVA_OPTS% ruoyi-admin.jar
cd bin
pause

Binary file not shown.

View File

@ -0,0 +1,316 @@
# 客户数据变更追踪系统使用说明
## 一、系统概述
客户数据变更追踪系统是一个用于记录和追踪客户数据变更历史的功能模块。该系统能够:
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. 定期清理过期数据
## 十一、联系支持
如有问题或建议,请联系开发团队。

253
excel-handle/QUICKSTART.md Normal file
View File

@ -0,0 +1,253 @@
# 快速开始指南
## 📋 前置条件
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 或联系技术支持

278
excel-handle/README.md Normal file
View File

@ -0,0 +1,278 @@
# 企业微信表格数据自动抓取与统计系统
## 📋 系统概述
本系统实现了从企业微信自动抓取指定表格数据,进行数据解析、去重、统计分析的完整流程。
## 🔄 完整流程图
```
┌─────────────────────────────────────────────────────────────────┐
│ 企业微信表格数据处理流程 │
└─────────────────────────────────────────────────────────────────┘
1. 【获取访问令牌】
├─ 使用 corpId + appSecret
├─ 调用企业微信 API 获取 access_token
└─ 缓存 token有效期 7200 秒)
2. 【抓取表格数据】
├─ 使用 access_token + docId + sheetId + range
├─ 调用企业微信文档 API
└─ 获取表格原始数据JSON 格式)
3. 【数据转换】
├─ 解析 JSON 数据结构
├─ 映射到 CustomerData 实体
└─ 处理日期、文本等字段
4. 【数据去重】
├─ 计算数据哈希值MD5
├─ 基于日期的智能去重
│ ├─ 当天数据:检查缓存,跳过已处理
│ └─ 非当天数据:直接处理
└─ 记录处理状态到数据库
5. 【数据统计】
├─ 按标签组统计数量
├─ 按日期统计趋势
└─ 生成统计报表
6. 【数据存储】
├─ 保存客户数据到数据库
├─ 保存统计结果
└─ 清理过期缓存
7. 【定时任务】
└─ 每小时自动执行一次(可配置)
```
## 🎯 核心功能模块
### 1. 企业微信 API 集成
- **文件**: `WecomApiUtils.java`
- **功能**:
- 获取并缓存 access_token
- 调用企业微信文档 API 获取表格数据
- 自动处理 token 过期和刷新
### 2. 数据抓取服务
- **文件**: `WecomTableService.java`
- **功能**:
- 从企业微信抓取表格数据
- 数据格式转换JSON → CustomerData
- 智能去重处理
- 批量保存数据
### 3. 数据去重服务
- **文件**: `ProcessedDataService.java`
- **功能**:
- 基于日期的去重策略
- 数据哈希计算
- 缓存管理(内存 + 数据库)
- 自动清理过期数据
### 4. 数据统计服务
- **文件**: `CustomerDataService.java`
- **功能**:
- Excel 文件解析
- 标签统计分析
- 统计结果持久化
### 5. 定时任务
- **文件**: `WecomTableSyncTask.java`
- **功能**:
- 定时自动抓取(默认每小时)
- 异常处理和日志记录
### 6. REST API 接口
- **文件**: `WecomTableController.java`
- **接口**:
- `POST /wecom/table/sync` - 手动触发同步
- `GET /wecom/table/status` - 查询服务状态
## 📦 数据实体
### CustomerData客户数据
包含 31 个字段:
- 基本信息:客户名称、描述、手机、邮箱、企业、地址、职务、电话
- 添加信息:添加人、添加人账号、添加人部门、添加时间
- 来源信息:来源
- 标签信息18 个标签组(投放、公司孵化、商务、成交日期等)
## 🔧 必需的外部参数
### 1. 企业微信配置参数(必填)
`application.yml` 中配置:
```yaml
wecom:
# 企业 ID必填
corp-id: "your_corp_id"
# 应用密钥(必填)
app-secret: "your_app_secret"
# 文档 ID必填
doc-id: "your_doc_id"
# 工作表 ID必填
sheet-id: "your_sheet_id"
# 查询范围必填A1 表示法)
range: "A1:AE1000"
# 抓取间隔(可选,默认 60 分钟)
fetch-interval: 60
```
### 2. 参数获取方法
#### 2.1 获取 corp-id企业 ID
1. 登录企业微信管理后台https://work.weixin.qq.com/
2. 进入「我的企业」→「企业信息」
3. 复制「企业 ID」
#### 2.2 获取 app-secret应用密钥
1. 登录企业微信管理后台
2. 进入「应用管理」→ 选择或创建一个应用
3. 在应用详情页面找到「Secret」
4. 复制应用的 Secret
**注意**: 需要给应用授予「企业微信文档」的权限
#### 2.3 获取 doc-id文档 ID
1. 在企业微信中打开目标表格文档
2. 查看浏览器地址栏的 URL
3. URL 格式:`https://doc.weixin.qq.com/sheet/xxxxx`
4. `xxxxx` 部分就是 doc-id
#### 2.4 获取 sheet-id工作表 ID
方法一:通过 API 查询
- 调用企业微信文档 API 获取文档的所有工作表列表
- 从返回结果中找到目标工作表的 ID
方法二:通过开发者工具
- 打开企业微信文档
- 按 F12 打开开发者工具
- 切换工作表时查看网络请求
- 找到包含 sheet_id 的请求参数
#### 2.5 设置 range查询范围
- 使用 A1 表示法,例如:
- `A1:Z100` - 查询 A 到 Z 列,前 100 行
- `A1:AE1000` - 查询 A 到 AE 列,前 1000 行
- `A:Z` - 查询 A 到 Z 列的所有行
### 3. 数据库配置(必填)
需要在主项目的 `application.yml` 中配置数据库连接:
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: your_username
password: your_password
```
### 4. 数据库表结构(需要创建)
需要创建以下表:
- `customer_data` - 存储客户数据
- `processed_data_record` - 存储已处理数据记录(用于去重)
- `customer_tag_statistics` - 存储标签统计数据
## 🚀 使用方式
### 方式一:自动定时任务
系统启动后,定时任务会自动执行(默认每小时整点)
### 方式二:手动触发
调用 REST API
```bash
curl -X POST http://localhost:8080/wecom/table/sync
```
### 方式三:查看服务状态
```bash
curl -X GET http://localhost:8080/wecom/table/status
```
## 📊 数据处理逻辑
### 去重策略
1. **当天数据去重**
- 使用「客户名称 + 添加人账号」作为唯一键
- 计算数据的 MD5 哈希值
- 如果键和哈希值都相同,则跳过
2. **历史数据处理**
- 非当天的数据直接处理,不进行去重检查
- 适用于补录历史数据的场景
### 缓存管理
- 内存缓存:存储当天已处理的数据
- 数据库持久化:记录所有处理历史
- 自动清理:每次处理后清理非当天的缓存
## 📝 日志说明
系统会记录详细的日志信息:
- 数据抓取日志:记录抓取的数据量
- 去重日志:记录新数据、跳过数据、无效数据的数量
- 错误日志:记录所有异常信息
## ⚠️ 注意事项
1. **权限要求**
- 企业微信应用需要有「企业微信文档」的 API 权限
- 应用需要能访问目标文档
2. **API 限制**
- 企业微信 API 有调用频率限制
- access_token 有效期为 7200 秒2 小时)
3. **数据量限制**
- 单次查询的数据量不宜过大
- 建议 range 设置在 1000 行以内
4. **表格格式要求**
- 第一行必须是表头
- 列的顺序必须与 CustomerData 实体的字段映射一致
## 🔍 故障排查
### 问题 1获取 access_token 失败
- 检查 corp-id 和 app-secret 是否正确
- 检查应用是否有相应权限
- 检查网络连接是否正常
### 问题 2获取表格数据失败
- 检查 doc-id 和 sheet-id 是否正确
- 检查应用是否有文档访问权限
- 检查 range 范围是否合法
### 问题 3数据解析失败
- 检查表格列的顺序是否与代码映射一致
- 检查日期格式是否为 yyyy-MM-dd
- 查看详细的错误日志
## 📚 相关文档
- [企业微信 API 文档](https://developer.work.weixin.qq.com/document/)
- [企业微信文档 API](https://developer.work.weixin.qq.com/document/path/97459)
- [EasyExcel 文档](https://easyexcel.opensource.alibaba.com/)
## 🎉 总结
本系统提供了一个完整的企业微信表格数据自动化处理方案,包括:
- ✅ 自动抓取企业微信表格数据
- ✅ 智能去重(基于日期和数据哈希)
- ✅ 数据统计分析
- ✅ 定时任务自动执行
- ✅ REST API 手动触发
- ✅ 完整的日志记录
只需配置好企业微信的相关参数,系统即可自动运行!

65
excel-handle/pom.xml Normal file
View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>excel-handle</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<easyexcel.version>3.3.2</easyexcel.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,255 @@
-- =============================================
-- 企业微信数据管理系统 - 完整数据库表结构
-- =============================================
-- =============================================
-- 1. 企业组织架构相关表
-- =============================================
-- 企业部门表
DROP TABLE IF EXISTS `corp_department`;
CREATE TABLE `corp_department` (
`id` BIGINT(20) NOT NULL COMMENT '部门ID企业微信部门ID',
`parentid` BIGINT(20) DEFAULT NULL COMMENT '父部门ID',
`order_no` BIGINT(20) DEFAULT NULL COMMENT '部门排序',
`name` VARCHAR(200) NOT NULL COMMENT '部门名称',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
INDEX `idx_parentid` (`parentid`),
INDEX `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业部门表';
-- 企业员工表
DROP TABLE IF EXISTS `corp_user`;
CREATE TABLE `corp_user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT 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格式',
`department_name` VARCHAR(500) DEFAULT NULL COMMENT '部门名称',
`name` VARCHAR(100) NOT NULL COMMENT '员工姓名',
`mobile` VARCHAR(50) DEFAULT NULL COMMENT '手机号',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`alias` VARCHAR(100) DEFAULT NULL COMMENT '别名',
`open_userid` VARCHAR(100) DEFAULT NULL COMMENT '全局唯一ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_userid` (`userid`),
INDEX `idx_depart_id` (`depart_id`),
INDEX `idx_name` (`name`),
INDEX `idx_mobile` (`mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业员工表';
-- =============================================
-- 2. 客户数据相关表
-- =============================================
-- 客户导出数据表
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 '客户姓名',
`description` TEXT DEFAULT NULL COMMENT '描述/备注',
`gender` INT(11) DEFAULT NULL COMMENT '性别',
`add_user_name` VARCHAR(100) DEFAULT NULL COMMENT '添加人姓名',
`add_user_account` VARCHAR(100) DEFAULT NULL COMMENT '添加人账号',
`add_user_department` VARCHAR(200) DEFAULT NULL COMMENT '添加人部门',
`add_time` DATETIME DEFAULT NULL COMMENT '添加时间',
`add_date` DATE DEFAULT NULL COMMENT '添加日期',
`source` VARCHAR(50) DEFAULT NULL COMMENT '来源',
`mobile` VARCHAR(200) DEFAULT NULL COMMENT '手机号(可能多个,逗号分隔)',
`company` VARCHAR(200) DEFAULT NULL COMMENT '公司',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`address` VARCHAR(500) DEFAULT NULL COMMENT '地址',
`position` VARCHAR(100) DEFAULT NULL COMMENT '职务',
`phone` VARCHAR(50) DEFAULT NULL COMMENT '电话',
`tag_group1` VARCHAR(500) DEFAULT NULL COMMENT '标签组1-投放',
`tag_group2` VARCHAR(500) DEFAULT NULL COMMENT '标签组2-公司孵化',
`tag_group3` VARCHAR(500) DEFAULT NULL COMMENT '标签组3-商务',
`tag_group4` VARCHAR(500) DEFAULT NULL COMMENT '标签组4-成交日期',
`tag_group5` VARCHAR(500) DEFAULT NULL COMMENT '标签组5-年级组',
`tag_group6` VARCHAR(500) DEFAULT NULL COMMENT '标签组6-客户属性',
`tag_group7` VARCHAR(500) DEFAULT NULL COMMENT '标签组7-成交',
`tag_group8` VARCHAR(500) DEFAULT NULL COMMENT '标签组8-成交品牌',
`tag_group9` VARCHAR(500) DEFAULT NULL COMMENT '标签组9-线索通',
`tag_group10` VARCHAR(500) DEFAULT NULL COMMENT '标签组10-A1',
`tag_group11` VARCHAR(500) DEFAULT NULL COMMENT '标签组11-B1',
`tag_group12` VARCHAR(500) DEFAULT NULL COMMENT '标签组12-C1',
`tag_group13` VARCHAR(500) DEFAULT NULL COMMENT '标签组13-D1',
`tag_group14` VARCHAR(500) DEFAULT NULL COMMENT '标签组14-E1',
`tag_group15` VARCHAR(500) DEFAULT NULL COMMENT '标签组15-意向度',
`tag_group16` VARCHAR(500) DEFAULT NULL COMMENT '标签组16-自然流',
`tag_group17` VARCHAR(500) DEFAULT NULL COMMENT '标签组17-F1',
`tag_group18` VARCHAR(500) 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`),
UNIQUE KEY `uk_customer_unique` (`customer_name`, `add_user_account`, `add_time`),
INDEX `idx_customer_name` (`customer_name`),
INDEX `idx_add_user_account` (`add_user_account`),
INDEX `idx_add_time` (`add_time`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户导出数据表';
-- 客户导出数据历史记录表
DROP TABLE IF EXISTS `customer_export_data_history`;
CREATE TABLE `customer_export_data_history` (
`history_id` BIGINT(20) NOT NULL AUTO_INCREMENT 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)',
`change_type` VARCHAR(20) NOT NULL COMMENT '变更类型:INSERT/UPDATE/DELETE',
`change_time` DATETIME NOT NULL COMMENT '变更时间',
`customer_name` VARCHAR(100) DEFAULT NULL COMMENT '客户姓名(快照)',
`description` TEXT DEFAULT NULL COMMENT '描述(快照)',
`gender` INT(11) DEFAULT NULL COMMENT '性别(快照)',
`add_user_name` VARCHAR(100) DEFAULT NULL COMMENT '添加人姓名(快照)',
`add_user_account` VARCHAR(100) DEFAULT NULL COMMENT '添加人账号(快照)',
`add_user_department` VARCHAR(200) DEFAULT NULL COMMENT '添加人部门(快照)',
`add_time` DATETIME DEFAULT NULL COMMENT '添加时间(快照)',
`add_date` DATE DEFAULT NULL COMMENT '添加日期(快照)',
`source` VARCHAR(50) DEFAULT NULL COMMENT '来源(快照)',
`mobile` VARCHAR(200) DEFAULT NULL COMMENT '手机号(快照)',
`company` VARCHAR(200) DEFAULT NULL COMMENT '公司(快照)',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱(快照)',
`address` VARCHAR(500) DEFAULT NULL COMMENT '地址(快照)',
`position` VARCHAR(100) DEFAULT NULL COMMENT '职务(快照)',
`phone` VARCHAR(50) DEFAULT NULL COMMENT '电话(快照)',
`tag_group1` VARCHAR(500) DEFAULT NULL COMMENT '标签组1-投放(快照)',
`tag_group2` VARCHAR(500) DEFAULT NULL COMMENT '标签组2-公司孵化(快照)',
`tag_group3` VARCHAR(500) DEFAULT NULL COMMENT '标签组3-商务(快照)',
`tag_group4` VARCHAR(500) DEFAULT NULL COMMENT '标签组4-成交日期(快照)',
`tag_group5` VARCHAR(500) DEFAULT NULL COMMENT '标签组5-年级组(快照)',
`tag_group6` VARCHAR(500) DEFAULT NULL COMMENT '标签组6-客户属性(快照)',
`tag_group7` VARCHAR(500) DEFAULT NULL COMMENT '标签组7-成交(快照)',
`tag_group8` VARCHAR(500) DEFAULT NULL COMMENT '标签组8-成交品牌(快照)',
`tag_group9` VARCHAR(500) DEFAULT NULL COMMENT '标签组9-线索通(快照)',
`tag_group10` VARCHAR(500) DEFAULT NULL COMMENT '标签组10-A1(快照)',
`tag_group11` VARCHAR(500) DEFAULT NULL COMMENT '标签组11-B1(快照)',
`tag_group12` VARCHAR(500) DEFAULT NULL COMMENT '标签组12-C1(快照)',
`tag_group13` VARCHAR(500) DEFAULT NULL COMMENT '标签组13-D1(快照)',
`tag_group14` VARCHAR(500) DEFAULT NULL COMMENT '标签组14-E1(快照)',
`tag_group15` VARCHAR(500) DEFAULT NULL COMMENT '标签组15-意向度(快照)',
`tag_group16` VARCHAR(500) DEFAULT NULL COMMENT '标签组16-自然流(快照)',
`tag_group17` VARCHAR(500) DEFAULT NULL COMMENT '标签组17-F1(快照)',
`tag_group18` VARCHAR(500) DEFAULT NULL COMMENT '标签组18-G1(快照)',
PRIMARY KEY (`history_id`),
INDEX `idx_customer_id` (`customer_id`),
INDEX `idx_version` (`version`),
INDEX `idx_change_time` (`change_time`),
INDEX `idx_change_type` (`change_type`),
INDEX `idx_data_fingerprint` (`data_fingerprint`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户导出数据历史记录表';
-- 客户数据变更日志表
DROP TABLE IF EXISTS `customer_data_change_log`;
CREATE TABLE `customer_data_change_log` (
`log_id` BIGINT(20) NOT NULL AUTO_INCREMENT 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 '变更字段名称(英文)',
`field_label` VARCHAR(100) DEFAULT NULL COMMENT '变更字段中文名称',
`old_value` TEXT DEFAULT NULL COMMENT '变更前的值',
`new_value` TEXT DEFAULT NULL COMMENT '变更后的值',
`change_time` DATETIME NOT NULL COMMENT '变更时间',
`version` INT(11) NOT NULL COMMENT '数据版本号',
PRIMARY KEY (`log_id`),
INDEX `idx_history_id` (`history_id`),
INDEX `idx_customer_id` (`customer_id`),
INDEX `idx_field_name` (`field_name`),
INDEX `idx_change_time` (`change_time`),
INDEX `idx_version` (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户数据变更日志表';
-- =============================================
-- 3. 统计数据相关表
-- =============================================
-- 客户联系统计数据表
DROP TABLE IF EXISTS `customer_contact_data`;
CREATE TABLE `customer_contact_data` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`stat_time` BIGINT(20) DEFAULT NULL COMMENT '统计时间戳(秒)',
`stat_date` DATE DEFAULT NULL COMMENT '统计日期',
`chat_cnt` INT(11) DEFAULT 0 COMMENT '聊天次数',
`message_cnt` INT(11) DEFAULT 0 COMMENT '消息次数',
`reply_percentage` DOUBLE DEFAULT 0 COMMENT '回复率(%',
`avg_reply_time` INT(11) DEFAULT 0 COMMENT '平均回复时间(秒)',
`negative_feedback_cnt` INT(11) DEFAULT 0 COMMENT '负面反馈次数',
`new_apply_cnt` INT(11) DEFAULT 0 COMMENT '新申请次数',
`new_contact_cnt` INT(11) DEFAULT 0 COMMENT '新联系次数',
`userid` VARCHAR(100) DEFAULT NULL COMMENT '成员ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_stat_date` (`stat_date`),
INDEX `idx_userid` (`userid`),
INDEX `idx_stat_time` (`stat_time`),
INDEX `idx_userid_stat_date` (`userid`, `stat_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户联系统计数据表';
-- 客户统计数据表
DROP TABLE IF EXISTS `customer_statistics_data`;
CREATE TABLE `customer_statistics_data` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`cur_date` DATE DEFAULT NULL COMMENT '统计日期',
`indicator_name` VARCHAR(100) DEFAULT NULL COMMENT '重要指标名称',
`ntf_group` VARCHAR(50) DEFAULT NULL COMMENT 'N组(投放)',
`ofh_group` VARCHAR(50) DEFAULT NULL COMMENT 'O组(公司孵化)',
`psw_group` VARCHAR(50) DEFAULT NULL COMMENT 'P组(商务)',
`wa1_group` VARCHAR(50) DEFAULT NULL COMMENT 'W组(A1组)',
`xb1_group` VARCHAR(50) DEFAULT NULL COMMENT 'X组(B1组)',
`yc1_group` VARCHAR(50) DEFAULT NULL COMMENT 'Y组(C1组)',
`zd1_group` VARCHAR(50) DEFAULT NULL COMMENT 'Z组(D1组)',
`aa_group` VARCHAR(50) DEFAULT NULL COMMENT 'AA组(E1组)',
`ac_group` VARCHAR(50) DEFAULT NULL COMMENT 'AC组(自然流)',
`ad_group` VARCHAR(50) DEFAULT NULL COMMENT 'AD组(F1组)',
`ae_group` VARCHAR(50) DEFAULT NULL COMMENT 'AE组(G1组)',
`sort_no` 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`),
INDEX `idx_cur_date` (`cur_date`),
INDEX `idx_indicator_name` (`indicator_name`),
INDEX `idx_sort_no` (`sort_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户统计数据表';
-- 部门统计数据表(宽表模型)
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(500) NOT NULL COMMENT '部门路径(完整层级路径)',
`daily_total_accepted` INT(11) DEFAULT 0 COMMENT '当日总承接数',
`manager_accepted` INT(11) DEFAULT 0 COMMENT '管理员分配人数',
`daily_total_orders` INT(11) DEFAULT 0 COMMENT '当日总成单数',
`daily_conversion_rate` DECIMAL(10, 2) DEFAULT 0.00 COMMENT '当日转化率(%',
`daily_timely_order_ratio` DECIMAL(10, 2) DEFAULT 0.00 COMMENT '及时单占比(当日)',
`daily_non_timely_order_ratio` DECIMAL(10, 2) DEFAULT 0.00 COMMENT '非及时单占比(当日)',
`sort_no` 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_stat_date_department` (`stat_date`, `department_path`(255)),
INDEX `idx_stat_date` (`stat_date`),
INDEX `idx_department_path` (`department_path`(255)),
INDEX `idx_sort_no` (`sort_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门统计数据表';

131
excel-handle/sql/schema.sql Normal file
View File

@ -0,0 +1,131 @@
-- ========================================
-- 企业微信表格数据处理系统 - 数据库表结构
-- ========================================
-- 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

@ -0,0 +1,73 @@
-- 菜单 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, '');

View File

@ -0,0 +1,20 @@
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_department")
public class CorpDepartment implements Serializable {
private Long id;
private Long parentid;
private Long orderNo;
private String name;
}

View File

@ -0,0 +1,28 @@
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_user")
public class CorpUser implements Serializable {
private Long id;
private String userid;
private Long departId;
private String departmentIds;
private String departmentName;
private String name;
private String mobile;
private String email;
private String alias;
private String openUserid;
}

View File

@ -0,0 +1,82 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客户联系统计数据表
* 用于存储企业微信返回的详细联系客户统计数据
*/
@Data
@TableName("customer_contact_data")
public class CustomerContactData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long id;
/**
* 统计时间戳
*/
private Long statTime;
/**
* 统计日期
*/
private Date statDate;
/**
* 聊天次数
*/
private Integer chatCnt;
/**
* 消息次数
*/
private Integer messageCnt;
/**
* 回复率%
*/
private Double replyPercentage;
/**
* 平均回复时间
*/
private Integer avgReplyTime;
/**
* 负面反馈次数
*/
private Integer negativeFeedbackCnt;
/**
* 新申请次数
*/
private Integer newApplyCnt;
/**
* 新联系次数
*/
private Integer newContactCnt;
/**
* 成员ID
*/
private String userid;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@ -0,0 +1,65 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客户数据变更日志表
* 记录每次数据变更的具体字段变化详情
*/
@Data
@TableName("customer_data_change_log")
public class CustomerDataChangeLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 日志主键ID
*/
@TableId(type = IdType.AUTO)
private Long logId;
/**
* 关联的历史记录ID
*/
private Long historyId;
/**
* 关联的客户数据ID
*/
private Long customerId;
/**
* 变更字段名称英文字段名
*/
private String fieldName;
/**
* 变更字段中文名称
*/
private String fieldLabel;
/**
* 变更前的值
*/
private String oldValue;
/**
* 变更后的值
*/
private String newValue;
/**
* 变更时间
*/
private Date changeTime;
/**
* 数据版本号
*/
private Integer version;
}

View File

@ -0,0 +1,189 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客户导出数据实体类
*/
@Data
@TableName("customer_export_data")
public class CustomerExportData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 客户名称
*/
private String customerName;
/**
* 描述
*/
private String description;
/**
* 性别
*/
private Integer gender;
/**
* 添加人
*/
private String addUserName;
/**
* 添加人账号
*/
private String addUserAccount;
/**
* 添加人所属部门
*/
private String addUserDepartment;
/**
* 添加时间
*/
private Date addTime;
/**
* 添加日期
*/
private Date addDate;
/**
* 来源
*/
private String source;
/**
* 手机
*/
private String mobile;
/**
* 企业
*/
private String company;
/**
* 邮箱
*/
private String email;
/**
* 地址
*/
private String address;
/**
* 职务
*/
private String position;
/**
* 电话
*/
private String phone;
/**
* 标签组1(投放)
*/
private String tagGroup1;
/**
* 标签组2(公司孵化)
*/
private String tagGroup2;
/**
* 标签组3(商务)
*/
private String tagGroup3;
/**
* 标签组4(成交日期)
*/
private String tagGroup4;
/**
* 标签组5(年级组)
*/
private String tagGroup5;
/**
* 标签组6(客户属性)
*/
private String tagGroup6;
/**
* 标签组7(成交)
*/
private String tagGroup7;
/**
* 标签组8(成交品牌)
*/
private String tagGroup8;
/**
* 标签组9(线索通标签)
*/
private String tagGroup9;
/**
* 标签组10(A1组)
*/
private String tagGroup10;
/**
* 标签组11(B1组)
*/
private String tagGroup11;
/**
* 标签组12(C1组)
*/
private String tagGroup12;
/**
* 标签组13(D1组)
*/
private String tagGroup13;
/**
* 标签组14(E1组)
*/
private String tagGroup14;
/**
* 标签组15(意向度)
*/
private String tagGroup15;
/**
* 标签组16(自然流)
*/
private String tagGroup16;
/**
* 标签组17(F1组)
*/
private String tagGroup17;
/**
* 标签组18(G1组)
*/
private String tagGroup18;
}

View File

@ -0,0 +1,216 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客户导出数据历史记录表
* 用于追踪客户数据的所有历史版本
*/
@Data
@TableName("customer_export_data_history")
public class CustomerExportDataHistory implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 历史记录主键ID
*/
@TableId(type = IdType.AUTO)
private Long historyId;
/**
* 关联的客户数据IDcustomer_export_data表的主键
*/
private Long customerId;
/**
* 数据版本号从1开始递增
*/
private Integer version;
/**
* 数据指纹用于快速判断数据是否变更
* 基于所有业务字段计算的MD5哈希值
*/
private String dataFingerprint;
/**
* 变更类型INSERT-新增, UPDATE-更新, DELETE-删除
*/
private String changeType;
/**
* 变更时间
*/
private Date changeTime;
/**
* 客户名称快照
*/
private String customerName;
/**
* 描述快照
*/
private String description;
/**
* 性别快照
*/
private Integer gender;
/**
* 添加人快照
*/
private String addUserName;
/**
* 添加人账号快照
*/
private String addUserAccount;
/**
* 添加人所属部门快照
*/
private String addUserDepartment;
/**
* 添加时间快照
*/
private Date addTime;
/**
* 添加日期快照
*/
private Date addDate;
/**
* 来源快照
*/
private String source;
/**
* 手机快照
*/
private String mobile;
/**
* 企业快照
*/
private String company;
/**
* 邮箱快照
*/
private String email;
/**
* 地址快照
*/
private String address;
/**
* 职务快照
*/
private String position;
/**
* 电话快照
*/
private String phone;
/**
* 标签组1(投放)快照
*/
private String tagGroup1;
/**
* 标签组2(公司孵化)快照
*/
private String tagGroup2;
/**
* 标签组3(商务)快照
*/
private String tagGroup3;
/**
* 标签组4(成交日期)快照
*/
private String tagGroup4;
/**
* 标签组5(年级组)快照
*/
private String tagGroup5;
/**
* 标签组6(客户属性)快照
*/
private String tagGroup6;
/**
* 标签组7(成交)快照
*/
private String tagGroup7;
/**
* 标签组8(成交品牌)快照
*/
private String tagGroup8;
/**
* 标签组9(线索通标签)快照
*/
private String tagGroup9;
/**
* 标签组10(A1组)快照
*/
private String tagGroup10;
/**
* 标签组11(B1组)快照
*/
private String tagGroup11;
/**
* 标签组12(C1组)快照
*/
private String tagGroup12;
/**
* 标签组13(D1组)快照
*/
private String tagGroup13;
/**
* 标签组14(E1组)快照
*/
private String tagGroup14;
/**
* 标签组15(意向度)快照
*/
private String tagGroup15;
/**
* 标签组16(自然流)快照
*/
private String tagGroup16;
/**
* 标签组17(F1组)快照
*/
private String tagGroup17;
/**
* 标签组18(G1组)快照
*/
private String tagGroup18;
}

View File

@ -0,0 +1,64 @@
package com.ruoyi.excel.wecom.domain;
import com.alibaba.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客户统计数据VO
* 用于存储30个统计指标的结果
*/
@Data
@TableName("customer_statistics_data")
public class CustomerStatisticsData implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Date curDate;
@ExcelProperty("重要指标")
private String indicatorName;
@ExcelProperty("N组")
private String ntfGroup;
@ExcelProperty("O组(公司孵化)")
private String ofhGroup;
@ExcelProperty("P组(商务)")
private String pswGroup;
@ExcelProperty("W组(A1组)")
private String wa1Group;
@ExcelProperty("X组(B1组)")
private String xb1Group;
@ExcelProperty("Y组(C1组)")
private String yc1Group;
@ExcelProperty("Z组(D1组)")
private String zd1Group;
@ExcelProperty("AA组(E1组)")
private String aaGroup;
@ExcelProperty("AC组(自然流)")
private String acGroup;
@ExcelProperty("AD组(F1组)")
private String adGroup;
@ExcelProperty("AE组(G1组)")
private String aeGroup;
private int sortNo;
}

View File

@ -0,0 +1,66 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.IdType;
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;
/**
* 部门统计数据实体类
* 用于存储基于部门路径的统计指标
*/
@Data
@TableName("department_statistics_data")
public class DepartmentStatisticsData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 统计日期
*/
private Date statDate;
//总承接数当日
private Integer dailyTotalAccepted;
//总成单数当日
private Integer dailyTotalOrders;
//转化率当日
private BigDecimal dailyConversionRate;
//及时单占比当日
private BigDecimal dailyTimelyOrderRatio;
//非及时单占比当日
private BigDecimal dailyNonTimelyOrderRatio;
/* //即时单
private Integer dailyTimelyCount;
//非即时单
private Integer dailyNonTimelyCount;*/
//管理员分配当日
private Integer managerAccepted;
/**
* 部门路径苏州曼普/销售部/一组 苏州曼普/销售部/一组/盛宇婷
*/
private String departmentPath;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
private int sortNo;
}

View File

@ -0,0 +1,54 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 企业微信标签域模型
*/
@Data
@TableName("wecom_tag")
public class WecomTagDomain implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 标签ID
*/
private String tagId;
/**
* 标签组ID
*/
private String tagGroupId;
/**
* 标签名称
*/
private String name;
/**
* 创建时间
*/
private Date createTime;
/**
* 排序
*/
private Integer orderNo;
/**
* 同步时间
*/
private Date syncTime;
}

View File

@ -0,0 +1,49 @@
package com.ruoyi.excel.wecom.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 企业微信标签组域模型
*/
@Data
@TableName("wecom_tag_group")
public class WecomTagGroupDomain implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 标签组ID
*/
private String tagGroupId;
/**
* 标签组名称
*/
private String name;
/**
* 创建时间
*/
private Date createTime;
/**
* 排序
*/
private Integer orderNo;
/**
* 同步时间
*/
private Date syncTime;
}

View File

@ -0,0 +1,55 @@
package com.ruoyi.excel.wecom.enums;
/**
* 客户添加来源枚举
*/
public enum AddWayEnum {
UNKNOWN(0, "未知来源"),
SCAN_QR_CODE(1, "扫描二维码"),
SEARCH_MOBILE(2, "搜索手机号"),
BUSINESS_CARD(3, "名片分享"),
GROUP_CHAT(4, "群聊"),
PHONE(5, "手机通讯录"),
WECHAT_CONTACT(6, "微信联系人"),
EXTERNAL_SHARE(7, "来自微信的添加好友申请"),
INTERNAL_SHARE(8, "安装第三方应用时自动添加的客服人员"),
SEARCH_EMAIL(9, "搜索邮箱"),
VIDEO_ACCOUNT(10, "视频号添加"),
CUSTOMER_LINK(16, "通过获客链接添加"),
INTERNAL_MEMBER_SHARE(201, "内部成员共享"),
ADMIN_ASSIGN(202, "管理员/负责人分配");
private final Integer code;
private final String description;
AddWayEnum(Integer code, String description) {
this.code = code;
this.description = description;
}
public Integer getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据code获取描述
*
* @param code 来源代码
* @return 来源描述
*/
public static String getDescriptionByCode(Integer code) {
if (code == null) {
return UNKNOWN.getDescription();
}
for (AddWayEnum addWay : AddWayEnum.values()) {
if (addWay.getCode().equals(code)) {
return addWay.getDescription();
}
}
return UNKNOWN.getDescription();
}
}

View File

@ -0,0 +1,59 @@
package com.ruoyi.excel.wecom.helper;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 部门统计累加器 - 用于分页处理时累加部门维度的统计结果
*/
@Data
public class DepartmentStatisticsAccumulator {
/**
* 为每个部门路径维护统计数据
* Key: 部门路径苏州曼普/销售部/一组 苏州曼普/销售部/一组/盛宇婷
* Value: 该部门的统计数据
*/
private Map<String, DepartmentStats> departmentStatsMap = new LinkedHashMap<>();
/**
* 获取指定部门的统计数据如果不存在则创建
*/
public DepartmentStats getDepartmentStats(String departmentPath) {
return departmentStatsMap.computeIfAbsent(departmentPath, k -> new DepartmentStats());
}
/**
* 单个部门的统计数据
*/
@Data
public static class DepartmentStats {
// ========== 基础统计当日 ==========
/**
* 总承接数当日- 进粉数排除"由管理员XXX分配"
*/
private int totalAcceptCount = 0;
/**
* 总成单数当日
*/
private int totalOrderCount = 0;
/**
* 及时单数当日
*/
private int timelyOrderCount = 0;
/**
* 非及时单数当日
*/
private int nonTimelyOrderCount = 0;
/**
* "由管理员XXX分配"
* */
private int managerAcceptCount = 0;
}
}

View File

@ -0,0 +1,975 @@
package com.ruoyi.excel.wecom.helper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.excel.wecom.domain.*;
import com.ruoyi.excel.wecom.mapper.*;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Component public class HandleAllData {
@Autowired
private WecomContactService wecomContactService;
@Autowired
private WecomTagService wecomTagService;
@Autowired
private CorpDepartmentMapper departmentMapper;
@Autowired
private CustomerExportService customerExportService;
@Autowired
private CorpUserMapper userMapper;
@Autowired
private CustomerExportDataMapper customerExportDataMapper;
@Autowired
private CustomerStatisticsDataMapper dataMapper;
@Autowired
private DepartmentStatisticsDataMapper departmentStatisticsDataMapper;
/**
* 线程池配置 - 用于并行处理客户数据
*/
private final ExecutorService executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2,
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "customer-data-handler-" + threadNumber.getAndIncrement());
thread.setDaemon(false);
return thread;
}
}
);
/**
* 抓取客户列表数据多线程版本
* 注意由于使用多线程移除了 @Transactional 注解事务管理在 handleData 方法中处理
*/
public void handleAllData() throws IOException {
//暂时不考虑 员工和部门变动情况下的场景 获取所有批量详情接口数
//1.获取所有配置了客户联系功能的员工列表
List<String> followUserList = wecomContactService.getFollowUserList();
int batchSize = 30;
int totalSize = followUserList.size();
// 创建任务列表
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < totalSize; i += batchSize) {
// 计算当前批次的结束位置防止越界
int end = Math.min(i + batchSize, totalSize);
// 获取当前批次的数据
List<String> batch = followUserList.subList(i, end);
// 提交异步任务
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
processBatch(batch);
} catch (IOException e) {
throw new RuntimeException("处理批次数据失败: " + e.getMessage(), e);
}
}, executorService);
futures.add(future);
}
// 等待所有任务完成
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
} catch (Exception e) {
throw new IOException("多线程处理客户数据时发生错误: " + e.getMessage(), e);
}
}
/**
* 处理单个批次的员工客户数据
* @param batch 员工ID列表
*/
private void processBatch(List<String> batch) throws IOException {
List<WecomCustomer> wecomCustomers = new ArrayList<>();
String tmpCursor = null;
do {
tmpCursor = wecomContactService.batchGetCustomerDetails(batch, wecomCustomers, tmpCursor, 100);
// 处理当前批次的客户数据
customerExportService.handleData(wecomCustomers);
wecomCustomers.clear();
} while(tmpCursor != null);
customerExportService.handleData(wecomCustomers);
}
/**
* 关闭线程池在应用关闭时调用
*/
public void shutdown() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
/**
* 创建所有日期的流量看板数据包含当日统计- 多线程版本
*/
public void createAllReportData() {
List<Date> allDate = customerExportService.getAllDate();
// 创建任务列表
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 为每个日期创建异步任务
for (Date date : allDate) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
createReportData(date);
} catch (Exception e) {
throw new RuntimeException("处理日期 " + date + " 的报表数据失败: " + e.getMessage(), e);
}
}, executorService);
futures.add(future);
}
// 等待所有任务完成
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
} catch (Exception e) {
throw new RuntimeException("多线程处理流量看板数据时发生错误: " + e.getMessage(), e);
}
}
/**
* 创建所有部门指标数据 - 多线程版本
*/
public void createAllDepartmentReportData() {
List<Date> allDate = customerExportService.getAllDate();
// 创建任务列表
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 为每个日期创建异步任务
for (Date date : allDate) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
createDepartmentReportData(date);
} catch (Exception e) {
throw new RuntimeException("处理日期 " + date + " 的部门报表数据失败: " + e.getMessage(), e);
}
}, executorService);
futures.add(future);
}
// 等待所有任务完成
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
} catch (Exception e) {
throw new RuntimeException("多线程处理部门报表数据时发生错误: " + e.getMessage(), e);
}
}
/**
* 创建流量看板数据包含当日统计
* @param date 统计日期
*/
public void createReportData(Date date) {
List<CustomerStatisticsData> oneDateData = calculateStatistics(date);
oneDateData.forEach(item->{
dataMapper.insert(item);
});
}
/**初始化基础数据 如果需要同步 需要删除老数据数据*/
public boolean initData() throws IOException {
List<CorpDepartment> departmentList;
Map<Long,String> cacheDepartMap = new HashMap<>();
Map<Long,Long> parentCacheMap = new HashMap<>();
try {
//存入部门id
departmentList = wecomContactService.getDepartmentList(null);
//循环获取部门详情数据
for (CorpDepartment item : departmentList) {
String departmentDetail = null;
departmentDetail = wecomContactService.getDepartmentDetail(item.getId());
item.setName(departmentDetail);
cacheDepartMap.put(item.getId(),departmentDetail);
parentCacheMap.put(item.getId(), item.getParentid());
//保存部门数据
departmentMapper.insert(item);
}
} catch (Exception e) {
throw new RuntimeException("存储部门数据失败!");
}
try {
//根据部门id循环获取下面的员工信息
departmentList.forEach(item -> {
try {
List<CorpUser> departmentMemberList = wecomContactService.getDepartmentMemberList(item.getId(), null, null);
departmentMemberList.forEach(oneUser -> {
oneUser.setDepartId(item.getId());
oneUser.setDepartmentName(getDepartmentFullPath(item.getId(),cacheDepartMap,parentCacheMap));
userMapper.insert(oneUser);
});
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (Exception e) {
throw new RuntimeException("存储员工数据失败!");
}
try {
//查询所有标签信息 存储标签信息
List<WecomTagGroup> tagGroupList = wecomContactService.getCorpTagList();
// 同步标签库到数据库
wecomTagService.syncCorpTagList(tagGroupList);
} catch (Exception e) {
throw new RuntimeException("存储标签数据失败!");
}
customerExportService.refreshCache();
return true;
}
/**
* 创建部门报表数据包含当日统计
* @param date 统计日期
*/
public void createDepartmentReportData(Date date) {
List<DepartmentStatisticsData> oneDateData = calculateDepartmentStatistics(date);
oneDateData.forEach(item -> {
departmentStatisticsDataMapper.insert(item);
});
}
/**
* 计算部门历史累计指标
* @param departmentPath 部门路径
* @return 历史累计指标作为一条记录返回
*/
public List<DepartmentStatisticsData> calculateDepartmentHistoricalStatistics(String departmentPath) {
List<DepartmentStatisticsData> results = new ArrayList<>();
// 1. 查询历史总承接数
Map<String, Object> acceptSumMap = departmentStatisticsDataMapper.selectHistoricalAcceptSum(departmentPath);
int historicalAcceptCount = 0;
if (acceptSumMap != null && acceptSumMap.get("total_value") != null) {
historicalAcceptCount = ((BigDecimal) acceptSumMap.get("total_value")).intValue();
}
// 2. 查询历史总成单数
Map<String, Object> orderSumMap = departmentStatisticsDataMapper.selectHistoricalOrderSum(departmentPath);
int historicalOrderCount = 0;
if (orderSumMap != null && orderSumMap.get("total_value") != null) {
historicalOrderCount = ((BigDecimal) orderSumMap.get("total_value")).intValue();
}
// 3. 计算渗透率
BigDecimal penetrationRate = calculateRateBigDecimal(historicalOrderCount, historicalAcceptCount);
// 生成一条历史累计记录
Date now = new Date();
DepartmentStatisticsData vo = new DepartmentStatisticsData();
vo.setStatDate(now);
vo.setDepartmentPath(departmentPath);
vo.setSortNo(0);
results.add(vo);
return results;
}
/**===================================以下为私有方法============================================*/
/**
* 递归获取部门完整路径
* @param departId 部门id
* @param cacheDepartMap 部门id对应部门名称的缓存
* @param parentCacheMap 部门id对应上级部门id的缓存
* @return 完整部门路径"/"分隔例如公司/技术部/研发组
*/
private String getDepartmentFullPath(Long departId, Map<Long, String> cacheDepartMap, Map<Long, Long> parentCacheMap) {
// 如果部门id为空或不存在返回空字符串
if (departId == null || !cacheDepartMap.containsKey(departId)) {
return "";
}
// 获取当前部门名称
String currentDepartName = cacheDepartMap.get(departId);
// 获取父部门id
Long parentId = parentCacheMap.get(departId);
// 如果没有父部门或父部门id为0根部门返回当前部门名称
if (parentId == null || parentId == 0) {
return currentDepartName;
}
// 递归获取父部门路径
String parentPath = getDepartmentFullPath(parentId, cacheDepartMap, parentCacheMap);
// 拼接父部门路径和当前部门名称
if (parentPath.isEmpty()) {
return currentDepartName;
} else {
return parentPath + "/" + currentDepartName;
}
}
/**
* 统计客户数据支持日期参数
* @param targetDate 目标日期格式yyyy-MM-dd "2026-01-03" 表示1月3日
* @return 统计结果列表
*/
/**
* 计算统计数据 - 使用分页查询+单次遍历方案
* @param date 目标日期 (格式: yyyy-MM-dd)
* @return 统计结果列表
*/
private List<CustomerStatisticsData> calculateStatistics(Date date) {
// 1. 初始化累加器
StatisticsAccumulator accumulator = new StatisticsAccumulator();
// 2. 分页查询并累加统计
int pageSize = 1000; // 每页1000条
int pageNum = 1;
while (true) {
// 分页查询不在SQL层过滤在Java层过滤以保证准确性
Page<CustomerExportData> page = new Page<>(pageNum, pageSize);
Page<CustomerExportData> pageData = customerExportDataMapper.selectPage(page, null);
// 处理当前页数据
for (CustomerExportData data : pageData.getRecords()) {
// 处理该条数据累加到所有相关组的统计中
processDataRecord(data, date, accumulator);
}
// 检查是否还有下一页
if (!pageData.hasNext()) {
break;
}
pageNum++;
}
// 3. 从累加器生成最终结果
return generateStatisticsResults(date,accumulator);
}
/**
* 计算部门维度的统计数据当日
* @param targetDate 目标日期 (格式: yyyy-MM-dd)
* @return 部门统计结果列表
*/
private List<DepartmentStatisticsData> calculateDepartmentStatistics(Date targetDate) {
// 1. 初始化部门累加器
DepartmentStatisticsAccumulator accumulator = new DepartmentStatisticsAccumulator();
// 2. 分页查询并累加统计
int pageSize = 1000;
int pageNum = 1;
while (true) {
// 分页查询
Page<CustomerExportData> page = new Page<>(pageNum, pageSize);
Page<CustomerExportData> pageData = customerExportDataMapper.selectPage(page, null);
// 处理当前页数据
for (CustomerExportData data : pageData.getRecords()) {
// 获取部门路径
String departmentPath = data.getAddUserDepartment();
if (departmentPath == null || departmentPath.trim().isEmpty()) {
continue;
}
// 提取组级别路径和员工级别路径
// 例如苏州曼普/销售部/二组/盛宇婷
// 组级别苏州曼普/销售部/二组
// 员工级别苏州曼普/销售部/二组/盛宇婷
String[] pathParts = departmentPath.split("/");
// 处理组级别统计前3级
if (pathParts.length >= 3) {
String groupPath = pathParts[0] + "/" + pathParts[1] + "/" + pathParts[2];
processDepartmentRecord(data, targetDate, accumulator.getDepartmentStats(groupPath));
}
// 处理员工级别统计完整路径
if (pathParts.length >= 4) {
processDepartmentRecord(data, targetDate, accumulator.getDepartmentStats(departmentPath));
}
}
// 检查是否还有下一页
if (!pageData.hasNext()) {
break;
}
pageNum++;
}
// 3. 从累加器生成最终结果
return generateDepartmentStatisticsResults(accumulator, targetDate);
}
/**
* 组名到字段名的映射
*/
private static final Map<String, String> GROUP_FIELD_MAP = new LinkedHashMap<>();
static {
GROUP_FIELD_MAP.put("N组", "tagGroup1");
GROUP_FIELD_MAP.put("O组", "tagGroup2");
GROUP_FIELD_MAP.put("P组", "tagGroup3");
GROUP_FIELD_MAP.put("W组", "tagGroup10");
GROUP_FIELD_MAP.put("X组", "tagGroup11");
GROUP_FIELD_MAP.put("Y组", "tagGroup12");
GROUP_FIELD_MAP.put("Z组", "tagGroup13");
GROUP_FIELD_MAP.put("AA组", "tagGroup14");
GROUP_FIELD_MAP.put("AC组", "tagGroup16");
GROUP_FIELD_MAP.put("AD组", "tagGroup17");
GROUP_FIELD_MAP.put("AE组", "tagGroup18");
}
/**
* 字段反射缓存
*/
private static final Map<String, java.lang.reflect.Field> FIELD_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
/**
* 检查数据是否匹配日期和来源条件
*/
private boolean matchesDate(CustomerExportData data, Date date) {
// 条件1: 日期匹配
if (data.getAddDate() != null && date != null) {
if (date.compareTo(data.getAddDate()) != 0) {
return false;
}
} else {
return false;
}
return true;
}
private boolean matchesSource(CustomerExportData data) {
// 条件: 排除"由管理员XXX分配"
if (data.getSource() != null &&
data.getSource().contains("管理员") &&
data.getSource().contains("分配")) {
return false;
}
return true;
}
private boolean matchesQValue(CustomerExportData data,Date curDate) {
String orderDate = data.getTagGroup4(); // Q列成交日期格式2.3
// 基础校验
if (orderDate == null || orderDate.trim().isEmpty()) {
return false;
}
try {
//由于可能是多个逗号拼成的多个日期 任意一个日期符合都可以
String[] dates = orderDate.trim().split(",");
// 解析订单日期2.3 -> [2, 3]
String[] parts = dates[dates.length - 1].trim().split("[.\\-/]");
if (parts.length < 2) {
return false;
}
int orderMonth = Integer.parseInt(parts[0].trim());
int orderDay = Integer.parseInt(parts[1].trim());
// 构建成交日期基于addTime的年份
Calendar orderCal = Calendar.getInstance();
orderCal.setTime(curDate);
orderCal.set(Calendar.MONTH, orderMonth - 1);
orderCal.set(Calendar.DAY_OF_MONTH, orderDay);
orderCal.set(Calendar.HOUR_OF_DAY, 0);
orderCal.set(Calendar.MINUTE, 0);
orderCal.set(Calendar.SECOND, 0);
orderCal.set(Calendar.MILLISECOND, 0);
// 跨年处理如果成交日期早于添加日期年份+1
if (orderCal.getTime().before(curDate)) {
orderCal.add(Calendar.YEAR, 1);
}
// 标准化目标日期清除时分秒
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);
boolean b = orderCal.getTimeInMillis() == targetCal.getTimeInMillis();
if (b) {
return true;
}
} catch (Exception e) {
return false;
}
return false;
}
private boolean matchOrderCompleted(CustomerExportData data,Date curDate) {
String orderStatus = data.getTagGroup7(); // T列成交状态
// 校验成交状态
if (!orderStatus.contains("已成交及时单9元+") && !orderStatus.contains("已成交非及时单9元+")) {
return false;
}
return true;
}
/**
* 处理单条数据记录累加到所有相关组的统计中
*/
private void processDataRecord(CustomerExportData data, Date date, StatisticsAccumulator accumulator) {
// 遍历所有组
for (Map.Entry<String, String> entry : GROUP_FIELD_MAP.entrySet()) {
String groupName = entry.getKey();
String fieldName = entry.getValue();
// 获取该组的标签值
String tagValue = getFieldValue(data, fieldName);
// 如果该组标签为空跳过
if (tagValue == null || tagValue.trim().isEmpty()) {
continue;
}
// 获取该组的统计器
StatisticsAccumulator.GroupStatistics stats = accumulator.getGroupStats(groupName);
// 累加各项统计指标
accumulateGroupStatistics(data, date, stats);
}
}
/**
* 使用反射获取字段值带缓存
*/
private String getFieldValue(CustomerExportData data, String fieldName) {
try {
java.lang.reflect.Field field = FIELD_CACHE.computeIfAbsent(fieldName, fn -> {
try {
java.lang.reflect.Field f = CustomerExportData.class.getDeclaredField(fn);
f.setAccessible(true);
return f;
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
});
Object value = field.get(data);
return value == null ? "" : value.toString();
} catch (Exception e) {
return "";
}
}
/**
* 累加单条数据的统计指标
*/
private void accumulateGroupStatistics(CustomerExportData data, Date date, StatisticsAccumulator.GroupStatistics stats) {
if(matchesQValue(data,date)) {
stats.setOrderCount(stats.getOrderCount() + 1);
String orderStatus = data.getTagGroup7();
if (orderStatus != null) {
if (orderStatus.contains("已成交及时单9元+")) {
stats.setTimelyOrderCount(stats.getTimelyOrderCount() + 1);
} else if (orderStatus.contains("已成交非及时单9元+")) {
stats.setNonTimelyOrderCount(stats.getNonTimelyOrderCount() + 1);
}
}
}
// 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.setTotalCustomerAttr(stats.getTotalCustomerAttr() + 1);
if (customerAttr.contains("家长")) {
stats.setParentCount(stats.getParentCount() + 1);
//todo 存在订单未完成 但是有标签的场景
stats.setParentOrderCount(stats.getParentOrderCount() + 1);
} else if (customerAttr.contains("学生")) {
stats.setStudentCount(stats.getStudentCount() + 1);
stats.setStudentOrderCount(stats.getStudentOrderCount() + 1);
} else {
stats.setUnknownAttrCount(stats.getUnknownAttrCount() + 1);
}
}
// 4. 意向度统计
String intention = data.getTagGroup15();
if (intention != null && !intention.trim().isEmpty() && !"空白".equals(intention)) {
stats.setTotalIntention(stats.getTotalIntention() + 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.setTotalGrade(stats.getTotalGrade() + 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);
}
}
}
/**
* 判断是否为小学
*/
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<CustomerStatisticsData> generateStatisticsResults(Date curDate,StatisticsAccumulator accumulator) {
// 使用 Map 来存储每个指标对应的 CustomerStatisticsData
// key: 指标名称不带组名前缀value: CustomerStatisticsData 对象
Map<String, CustomerStatisticsData> indicatorMap = new LinkedHashMap<>();
//设置一个默认排序
int sortNo = 0;
// 为每个组生成23个统计指标
for (Map.Entry<String, StatisticsAccumulator.GroupStatistics> entry : accumulator.getGroupStatsMap().entrySet()) {
String groupName = entry.getKey();
StatisticsAccumulator.GroupStatistics stats = entry.getValue();
// 21-23. 成本相关需要手工填写
setIndicatorValue(indicatorMap,curDate, "总成本(当日)", groupName, "需手工填写",(10*sortNo++));
setIndicatorValue(indicatorMap,curDate, "单条成本(当日)", groupName, "需手工填写",(10*sortNo++));
setIndicatorValue(indicatorMap,curDate, "成单成本(当日)", groupName, "需手工填写",(10*sortNo++));
// 1. 成单数
setIndicatorValue(indicatorMap,curDate, "成单数(当日)", groupName, String.valueOf(stats.getOrderCount()),(10*sortNo++));
// 2. 进粉数
setIndicatorValue(indicatorMap,curDate, "进粉数(当日)", groupName, String.valueOf(stats.getCustomerCount()),(10*sortNo++));
// 3. 转化率
String conversionRate = calculateRate(stats.getOrderCount(), stats.getCustomerCount());
setIndicatorValue(indicatorMap,curDate, "转化率(当日)", groupName, conversionRate,(10*sortNo++));
// 4. 及时单占比
String timelyRate = calculateRate(stats.getTimelyOrderCount(), stats.getCustomerCount());
setIndicatorValue(indicatorMap,curDate, "及时单占比(当日)", groupName, timelyRate,(10*sortNo++));
// 5. 非及时单占比
String nonTimelyRate = calculateRate(stats.getNonTimelyOrderCount(), stats.getCustomerCount());
setIndicatorValue(indicatorMap,curDate, "非及时单占比(当日)", groupName, nonTimelyRate,(10*sortNo++));
// 6. 客户属性数量
setIndicatorValue(indicatorMap,curDate, "客户属性数量(当日)", groupName, String.valueOf(stats.getTotalCustomerAttr()),(10*sortNo++));
// 7. 家长占比
String parentRate = calculateRate(stats.getParentCount(), stats.getTotalCustomerAttr());
setIndicatorValue(indicatorMap,curDate, "家长占比(当日)", groupName, parentRate,(10*sortNo++));
// 8. 学生占比
String studentRate = calculateRate(stats.getStudentCount(), stats.getTotalCustomerAttr());
setIndicatorValue(indicatorMap,curDate, "学生占比(当日)", groupName, studentRate,(10*sortNo++));
// 9. 未知占比
String unknownRate = calculateRate(stats.getUnknownAttrCount(), stats.getTotalCustomerAttr());
setIndicatorValue(indicatorMap,curDate, "未知占比(当日)", groupName, unknownRate,(10*sortNo++));
// 10. 意向度数量
setIndicatorValue(indicatorMap,curDate, "意向度数量(当日)", groupName, String.valueOf(stats.getTotalIntention()),(10*sortNo++));
// 11. 主动报价占比
String activeQuoteRate = calculateRate(stats.getActiveQuoteCount(), stats.getTotalIntention());
setIndicatorValue(indicatorMap,curDate, "主动报价占比(当日)", groupName, activeQuoteRate,(10*sortNo++));
// 12. 被动报价占比
String passiveQuoteRate = calculateRate(stats.getPassiveQuoteCount(), stats.getTotalIntention());
setIndicatorValue(indicatorMap,curDate, "被动报价占比(当日)", groupName, passiveQuoteRate,(10*sortNo++));
// 13. 未开口报价占比
String noQuoteRate = calculateRate(stats.getNoQuoteCount(), stats.getTotalIntention());
setIndicatorValue(indicatorMap,curDate, "未开口报价占比(当日)", groupName, noQuoteRate,(10*sortNo++));
// 14. 已删除报价占比
String deletedQuoteRate = calculateRate(stats.getDeletedQuoteCount(), stats.getTotalIntention());
setIndicatorValue(indicatorMap,curDate, "已删除报价占比(当日)", groupName, deletedQuoteRate,(10*sortNo++));
// 15. 年级数量
setIndicatorValue(indicatorMap,curDate, "年级数量(当日)", groupName, String.valueOf(stats.getTotalGrade()),(10*sortNo++));
// 16. 小学占比
String primaryRate = calculateRate(stats.getPrimaryCount(), stats.getTotalGrade());
setIndicatorValue(indicatorMap,curDate, "小学占比(当日)", groupName, primaryRate,(10*sortNo++));
// 17. 初中占比
String middleRate = calculateRate(stats.getMiddleCount(), stats.getTotalGrade());
setIndicatorValue(indicatorMap,curDate, "初中占比(当日)", groupName, middleRate,(10*sortNo++));
// 18. 高中占比
String highRate = calculateRate(stats.getHighCount(), stats.getTotalGrade());
setIndicatorValue(indicatorMap,curDate, "高中占比(当日)", groupName, highRate,(10*sortNo++));
// 19. 家长出单占比
String parentOrderRate = calculateRate(stats.getParentOrderCount(), stats.getParentCount());
setIndicatorValue(indicatorMap,curDate, "家长出单占比(当日)", groupName, parentOrderRate,(10*sortNo++));
// 20. 学生出单占比
String studentOrderRate = calculateRate(stats.getStudentOrderCount(), stats.getStudentCount());
setIndicatorValue(indicatorMap,curDate, "学生出单占比(当日)", groupName, studentOrderRate,(10*sortNo++));
}
// Map 转换为 List 返回
return new ArrayList<>(indicatorMap.values());
}
/**
* 设置指标值到对应的 CustomerStatisticsData 对象
* @param indicatorMap 指标映射表
* @param indicatorName 指标名称不带组名前缀
* @param groupName 组名
* @param value 指标值
*/
private void setIndicatorValue(Map<String, CustomerStatisticsData> indicatorMap,Date curDate,
String indicatorName, String groupName, String value,int sortNo) {
// 获取或创建该指标对应的 CustomerStatisticsData 对象
CustomerStatisticsData vo = indicatorMap.computeIfAbsent(indicatorName, k -> {
CustomerStatisticsData newVo = new CustomerStatisticsData();
newVo.setIndicatorName(indicatorName);
newVo.setCurDate(curDate);
newVo.setSortNo(sortNo);
return newVo;
});
// 根据组名设置对应的字段值
setGroupValue(vo, groupName, value);
}
/**
* 根据组名设置对应字段的值
* @param vo CustomerStatisticsData 对象
* @param groupName 组名
* @param value 要设置的值
*/
private void setGroupValue(CustomerStatisticsData vo, String groupName, String value) {
switch (groupName) {
case "N组":
vo.setNtfGroup(value);
break;
case "O组":
vo.setOfhGroup(value);
break;
case "P组":
vo.setPswGroup(value);
break;
case "W组":
vo.setWa1Group(value);
break;
case "X组":
vo.setXb1Group(value);
break;
case "Y组":
vo.setYc1Group(value);
break;
case "Z组":
vo.setZd1Group(value);
break;
case "AA组":
vo.setAaGroup(value);
break;
case "AC组":
vo.setAcGroup(value);
break;
case "AD组":
vo.setAdGroup(value);
break;
case "AE组":
vo.setAeGroup(value);
break;
}
}
/**
* 计算百分比
*/
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 void processDepartmentRecord(CustomerExportData data, Date targetDate,
DepartmentStatisticsAccumulator.DepartmentStats stats) {
if(matchesQValue(data,targetDate)) {
stats.setTotalOrderCount(stats.getTotalOrderCount() + 1);
// 及时单和非及时单统计
String orderStatus = data.getTagGroup7();
if (orderStatus != null) {
String[] split = orderStatus.split(",");
if (split[split.length-1].contains("已成交及时单9元+")) {
stats.setTimelyOrderCount(stats.getTimelyOrderCount() + 1);
} else if (split[split.length-1].contains("已成交非及时单9元+")) {
stats.setNonTimelyOrderCount(stats.getNonTimelyOrderCount() + 1);
}
}
}
if (matchesDate(data,targetDate)) {
if(matchesSource(data)) {
// 2. 总承接数当日- 排除"由管理员XXX分配"
stats.setTotalAcceptCount(stats.getTotalAcceptCount() + 1);
} else {
// 由管理员XXX分配"
stats.setManagerAcceptCount(stats.getManagerAcceptCount() + 1);
}
}
}
/**
* 从累加器生成最终部门统计结果
* 每一行数据代表一个部门或员工包含所有指标维度
*/
private List<DepartmentStatisticsData> generateDepartmentStatisticsResults(
DepartmentStatisticsAccumulator accumulator, Date statDate) {
List<DepartmentStatisticsData> results = new ArrayList<>();
int sortNo = 0;
// 为每个部门生成一条记录包含所有指标
for (Map.Entry<String, DepartmentStatisticsAccumulator.DepartmentStats> entry :
accumulator.getDepartmentStatsMap().entrySet()) {
String departmentPath = entry.getKey();
DepartmentStatisticsAccumulator.DepartmentStats stats = entry.getValue();
// 创建一个DepartmentStatisticsData对象包含所有指标
DepartmentStatisticsData vo = new DepartmentStatisticsData();
vo.setStatDate(statDate);
vo.setDepartmentPath(departmentPath);
// 设置各项指标
// 1. 总承接数当日
vo.setDailyTotalAccepted(stats.getTotalAcceptCount());
//设置管理员分配人数
vo.setManagerAccepted(stats.getManagerAcceptCount());
// 2. 总成单数当日
vo.setDailyTotalOrders(stats.getTotalOrderCount());
// 3. 转化率当日
BigDecimal conversionRate = calculateRateBigDecimal(stats.getTotalOrderCount(), stats.getTotalAcceptCount());
vo.setDailyConversionRate(conversionRate);
// 4. 及时单占比当日
BigDecimal timelyRate = calculateRateBigDecimal(stats.getTimelyOrderCount(), stats.getTotalOrderCount());
vo.setDailyTimelyOrderRatio(timelyRate);
// 5. 非及时单占比当日
BigDecimal nonTimelyRate = calculateRateBigDecimal(stats.getNonTimelyOrderCount(), stats.getTotalOrderCount());
vo.setDailyNonTimelyOrderRatio(nonTimelyRate);
results.add(vo);
}
return results;
}
/**
* 计算百分比返回BigDecimal类型
*/
private BigDecimal calculateRateBigDecimal(int count, int total) {
if (total == 0) {
return BigDecimal.ZERO;
}
return new BigDecimal(count)
.multiply(new BigDecimal(100))
.divide(new BigDecimal(total), 2, RoundingMode.HALF_UP);
}
}

View File

@ -0,0 +1,151 @@
package com.ruoyi.excel.wecom.helper;
import lombok.Data;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 统计累加器 - 用于分页处理时累加统计结果
*/
@Data
public class StatisticsAccumulator {
/**
* 为每个组维护统计数据
* Key: 组名N组O组等
* Value: 该组的统计数据
*/
private Map<String, GroupStatistics> groupStatsMap = new LinkedHashMap<>();
/**
* 初始化所有需要统计的组
*/
public StatisticsAccumulator() {
// 需要统计的11个组
String[] groupNames = {"N组", "O组", "P组", "W组", "X组", "Y组", "Z组", "AA组", "AC组", "AD组", "AE组"};
for (String groupName : groupNames) {
groupStatsMap.put(groupName, new GroupStatistics());
}
}
/**
* 获取指定组的统计数据
*/
public GroupStatistics getGroupStats(String groupName) {
return groupStatsMap.get(groupName);
}
/**
* 单个组的统计数据
*/
@Data
public static class GroupStatistics {
// ========== 基础统计 ==========
/**
* 成单数
*/
private int orderCount = 0;
/**
* 进粉数
*/
private int customerCount = 0;
/**
* 及时单数
*/
private int timelyOrderCount = 0;
/**
* 非及时单数
*/
private int nonTimelyOrderCount = 0;
// ========== 客户属性统计 ==========
/**
* 客户属性总数非空
*/
private int totalCustomerAttr = 0;
/**
* 家长数
*/
private int parentCount = 0;
/**
* 学生数
*/
private int studentCount = 0;
/**
* 未知空白
*/
private int unknownAttrCount = 0;
// ========== 意向度统计 ==========
/**
* 意向度总数非空
*/
private int totalIntention = 0;
/**
* 主动报价数
*/
private int activeQuoteCount = 0;
/**
* 被动报价数
*/
private int passiveQuoteCount = 0;
/**
* 未开口数
*/
private int noQuoteCount = 0;
/**
* 已删除数
*/
private int deletedQuoteCount = 0;
// ========== 年级统计 ==========
/**
* 年级总数非空
*/
private int totalGrade = 0;
/**
* 小学数
*/
private int primaryCount = 0;
/**
* 初中数
*/
private int middleCount = 0;
/**
* 高中数
*/
private int highCount = 0;
// ========== 出单占比统计 ==========
/**
* 家长出单数
*/
private int parentOrderCount = 0;
/**
* 学生出单数
*/
private int studentOrderCount = 0;
// ========== 成本数据 ==========
/**
* 总成本手工填写
*/
private BigDecimal totalCost = BigDecimal.ZERO;
}
}

View File

@ -0,0 +1,16 @@
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 org.apache.ibatis.annotations.Mapper;
/**
* 部门mapper
*/
@Mapper
public interface CorpDepartmentMapper extends BaseMapper<CorpDepartment> {
}

View File

@ -0,0 +1,18 @@
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 java.util.List;
/**
* 用户mapper
*/
@Mapper
public interface CorpUserMapper extends BaseMapper<CorpUser> {
List<String> selectAllUserIds();
}

View File

@ -0,0 +1,60 @@
package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CustomerContactData;
import com.ruoyi.excel.wecom.vo.CustomerContactDataVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* 客户联系统计数据Mapper
*/
@Mapper
public interface CustomerContactDataMapper extends BaseMapper<CustomerContactData> {
/**
* 根据日期查询客户联系统计数据
*
* @param statDate 统计日期
* @return 客户联系统计数据列表
*/
List<CustomerContactData> selectByStatDate(Date statDate);
/**
* 根据成员ID和日期查询客户联系统计数据
*
* @param userid 成员ID
* @param statDate 统计日期
* @return 客户联系统计数据列表
*/
List<CustomerContactData> selectByUseridAndStatDate(String userid, Date statDate);
/**
* 查询客户联系统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param userid 成员ID
* @return 客户联系统计数据列表
*/
List<CustomerContactData> selectCustomerContactDataList(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("userid") String userid
);
/**
* 查询客户联系统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param userid 成员ID
* @return 客户联系统计数据VO列表
*/
List<CustomerContactDataVO> selectCustomerContactDataVOList(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("userid") String userid
);
}

View File

@ -0,0 +1,46 @@
package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CustomerDataChangeLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 客户数据变更日志Mapper
*/
@Mapper
public interface CustomerDataChangeLogMapper extends BaseMapper<CustomerDataChangeLog> {
/**
* 根据客户ID查询所有变更日志按时间倒序
* @param customerId 客户ID
* @return 变更日志列表
*/
List<CustomerDataChangeLog> selectChangeLogByCustomerId(@Param("customerId") Long customerId);
/**
* 根据历史记录ID查询变更日志
* @param historyId 历史记录ID
* @return 变更日志列表
*/
List<CustomerDataChangeLog> selectChangeLogByHistoryId(@Param("historyId") Long historyId);
/**
* 根据客户ID和版本号查询变更日志
* @param customerId 客户ID
* @param version 版本号
* @return 变更日志列表
*/
List<CustomerDataChangeLog> selectChangeLogByCustomerIdAndVersion(
@Param("customerId") Long customerId,
@Param("version") Integer version);
/**
* 批量插入变更日志
* @param changeLogs 变更日志列表
* @return 插入数量
*/
int batchInsert(@Param("changeLogs") List<CustomerDataChangeLog> changeLogs);
}

View File

@ -0,0 +1,49 @@
package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CustomerExportDataHistory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 客户导出数据历史记录Mapper
*/
@Mapper
public interface CustomerExportDataHistoryMapper extends BaseMapper<CustomerExportDataHistory> {
/**
* 根据客户ID查询最新版本号
* @param customerId 客户ID
* @return 最新版本号如果没有历史记录则返回0
*/
Integer selectMaxVersionByCustomerId(@Param("customerId") Long customerId);
/**
* 根据客户ID和版本号查询历史记录
* @param customerId 客户ID
* @param version 版本号
* @return 历史记录
*/
CustomerExportDataHistory selectByCustomerIdAndVersion(
@Param("customerId") Long customerId,
@Param("version") Integer version);
/**
* 根据客户ID查询所有历史记录按版本号倒序
* @param customerId 客户ID
* @return 历史记录列表
*/
List<CustomerExportDataHistory> selectHistoryByCustomerId(@Param("customerId") Long customerId);
/**
* 根据数据指纹查询是否存在相同数据
* @param customerId 客户ID
* @param dataFingerprint 数据指纹
* @return 历史记录
*/
CustomerExportDataHistory selectByCustomerIdAndFingerprint(
@Param("customerId") Long customerId,
@Param("dataFingerprint") String dataFingerprint);
}

View File

@ -0,0 +1,57 @@
package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CustomerExportData;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* 客户导出数据Mapper
*/
@Mapper
public interface CustomerExportDataMapper extends BaseMapper<CustomerExportData> {
List<Date> getDistinctDate();
/**
* 查询客户导出数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称(可选)
* @return 客户导出数据列表
*/
List<CustomerExportData> selectCustomerExportDataList(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("customerName") String customerName
);
/**
* 查询客户导出数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称(可选)
* @return 客户导出数据VO列表
*/
List<CustomerExportDataVO> selectCustomerExportDataVOList(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("customerName") String customerName
);
/**
* 根据客户唯一标识查询客户数据客户名称+添加人账号+添加时间
* @param customerName 客户名称
* @param addUserAccount 添加人账号
* @param addTime 添加时间
* @return 客户导出数据
*/
CustomerExportData selectByUniqueKey(
@Param("customerName") String customerName,
@Param("addUserAccount") String addUserAccount,
@Param("addTime") Date addTime
);
}

View File

@ -0,0 +1,48 @@
package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsData;
import com.ruoyi.excel.wecom.vo.CustomerStatisticsDataVO;
import com.ruoyi.excel.wecom.vo.CustomerStatisticsVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* 用户mapper
*/
@Mapper
public interface CustomerStatisticsDataMapper extends BaseMapper<CustomerStatisticsData> {
List<CustomerStatisticsVO> selectByDate(Date date);
/**
* 查询客户统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param indicatorName 指标名称
* @return 客户统计数据列表
*/
List<CustomerStatisticsData> selectCustomerStatisticsDataList(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("indicatorName") String indicatorName
);
/**
* 查询客户统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param indicatorName 指标名称
* @return 客户统计数据VO列表
*/
List<CustomerStatisticsDataVO> selectCustomerStatisticsDataVOList(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("indicatorName") String indicatorName
);
}

View File

@ -0,0 +1,83 @@
package com.ruoyi.excel.wecom.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.excel.wecom.domain.DepartmentStatisticsData;
import com.ruoyi.excel.wecom.vo.DepartmentStatisticsDataVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 部门统计数据Mapper
*/
@Mapper
public interface DepartmentStatisticsDataMapper extends BaseMapper<DepartmentStatisticsData> {
/**
* 根据日期查询统计数据
* @param date 统计日期
* @return 统计数据列表
*/
List<DepartmentStatisticsData> selectByDate(@Param("date") Date date);
/**
* 根据部门路径和日期范围查询统计数据
* @param departmentPath 部门路径
* @param startDate 开始日期
* @param endDate 结束日期
* @return 统计数据列表
*/
List<DepartmentStatisticsData> selectByDepartmentAndDateRange(
@Param("departmentPath") String departmentPath,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate
);
/**
* 查询指定部门的历史累计承接数
* @param departmentPath 部门路径
* @return 累计值
*/
Map<String, Object> selectHistoricalAcceptSum(@Param("departmentPath") String departmentPath);
/**
* 查询指定部门的历史累计成单数
* @param departmentPath 部门路径
* @return 累计值
*/
Map<String, Object> selectHistoricalOrderSum(@Param("departmentPath") String departmentPath);
/**
* 查询部门统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径(可选)
* @return 部门统计数据列表
*/
List<DepartmentStatisticsData> selectDepartmentStatisticsDataList(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("departmentPath") String departmentPath
);
/**
* 查询部门统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径(可选)
* @return 部门统计数据VO列表
*/
List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOList(
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("departmentPath") String departmentPath
);
Map<String, BigDecimal> getSummary(@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("departmentPath") String departmentPath);
}

View File

@ -0,0 +1,21 @@
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;
/**
* 企业微信标签组Mapper
*/
@Mapper
public interface WecomTagGroupMapper extends BaseMapper<WecomTagGroupDomain> {
/**
* 根据标签组ID查询标签组
*
* @param tagGroupId 标签组ID
* @return 标签组
*/
WecomTagGroupDomain selectByTagGroupId(String tagGroupId);
}

View File

@ -0,0 +1,21 @@
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;
/**
* 企业微信标签Mapper
*/
@Mapper
public interface WecomTagMapper extends BaseMapper<WecomTagDomain> {
/**
* 根据标签ID查询标签
*
* @param tagId 标签ID
* @return 标签
*/
WecomTagDomain selectByTagId(String tagId);
}

View File

@ -0,0 +1,55 @@
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

@ -0,0 +1,383 @@
package com.ruoyi.excel.wecom.model;
import java.util.List;
/**
* 企业微信客户详情模型
*/
public class WecomCustomer {
private ExternalContact externalContact;
private FollowInfo followInfo;
public ExternalContact getExternalContact() {
return externalContact;
}
public void setExternalContact(ExternalContact externalContact) {
this.externalContact = externalContact;
}
public FollowInfo getFollowInfo() {
return followInfo;
}
public void setFollowInfo(FollowInfo followInfo) {
this.followInfo = followInfo;
}
/**
* 外部联系人基本信息
*/
public static class ExternalContact {
private String externalUserid;
private String name;
private String position;
private String avatar;
private String corpName;
private String corpFullName;
private Integer type;
private Integer gender;
private String unionid;
private ExternalProfile externalProfile;
public String getExternalUserid() {
return externalUserid;
}
public void setExternalUserid(String externalUserid) {
this.externalUserid = externalUserid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getCorpName() {
return corpName;
}
public void setCorpName(String corpName) {
this.corpName = corpName;
}
public String getCorpFullName() {
return corpFullName;
}
public void setCorpFullName(String corpFullName) {
this.corpFullName = corpFullName;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public String getUnionid() {
return unionid;
}
public void setUnionid(String unionid) {
this.unionid = unionid;
}
public ExternalProfile getExternalProfile() {
return externalProfile;
}
public void setExternalProfile(ExternalProfile externalProfile) {
this.externalProfile = externalProfile;
}
}
/**
* 外部个人资料
*/
public static class ExternalProfile {
private List<ExternalAttr> externalAttr;
public List<ExternalAttr> getExternalAttr() {
return externalAttr;
}
public void setExternalAttr(List<ExternalAttr> externalAttr) {
this.externalAttr = externalAttr;
}
}
/**
* 外部属性
*/
public static class ExternalAttr {
private Integer type;
private String name;
private Text text;
private Web web;
private Miniprogram miniprogram;
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Text getText() {
return text;
}
public void setText(Text text) {
this.text = text;
}
public Web getWeb() {
return web;
}
public void setWeb(Web web) {
this.web = web;
}
public Miniprogram getMiniprogram() {
return miniprogram;
}
public void setMiniprogram(Miniprogram miniprogram) {
this.miniprogram = miniprogram;
}
}
/**
* 文本属性
*/
public static class Text {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
/**
* 网页属性
*/
public static class Web {
private String url;
private String title;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
/**
* 小程序属性
*/
public static class Miniprogram {
private String appid;
private String pagepath;
private String title;
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getPagepath() {
return pagepath;
}
public void setPagepath(String pagepath) {
this.pagepath = pagepath;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
/**
* 跟进信息
*/
public static class FollowInfo {
private String userid;
private String remark;
private String description;
private Long createtime;
private List<String> tagId;
private String remarkCorpName;
private List<String> remarkMobiles;
private String operUserid;
private Integer addWay;
private String state;
public String getUserid() {
return userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Long getCreatetime() {
return createtime;
}
public void setCreatetime(Long createtime) {
this.createtime = createtime;
}
public List<String> getTagId() {
return tagId;
}
public void setTagId(List<String> tagId) {
this.tagId = tagId;
}
public String getRemarkCorpName() {
return remarkCorpName;
}
public void setRemarkCorpName(String remarkCorpName) {
this.remarkCorpName = remarkCorpName;
}
public List<String> getRemarkMobiles() {
return remarkMobiles;
}
public void setRemarkMobiles(List<String> remarkMobiles) {
this.remarkMobiles = remarkMobiles;
}
public String getOperUserid() {
return operUserid;
}
public void setOperUserid(String operUserid) {
this.operUserid = operUserid;
}
public Integer getAddWay() {
return addWay;
}
public void setAddWay(Integer addWay) {
this.addWay = addWay;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
/**
* 视频号信息
*/
public static class WechatChannels {
private String nickname;
private Integer source;
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public Integer getSource() {
return source;
}
public void setSource(Integer source) {
this.source = source;
}
}
}

View File

@ -0,0 +1,16 @@
package com.ruoyi.excel.wecom.model;
import lombok.Getter;
import lombok.Setter;
/**
* 企业微信标签类
*/
@Getter
@Setter
public class WecomTag {
private String id;
private String name;
private Long createTime;
private Integer order;
}

View File

@ -0,0 +1,19 @@
package com.ruoyi.excel.wecom.model;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 企业微信标签组类
*/
@Getter
@Setter
public class WecomTagGroup {
private String groupId;
private String groupName;
private Long createTime;
private Integer order;
private List<WecomTag> tag;
}

View File

@ -0,0 +1,36 @@
package com.ruoyi.excel.wecom.model;
import java.util.List;
/**
* 企业微信标签库响应类
*/
public class WecomTagLibraryResponse {
private Integer errcode;
private String errmsg;
private List<WecomTagGroup> tagGroup;
public Integer getErrcode() {
return errcode;
}
public void setErrcode(Integer errcode) {
this.errcode = errcode;
}
public String getErrmsg() {
return errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
public List<WecomTagGroup> getTagGroup() {
return tagGroup;
}
public void setTagGroup(List<WecomTagGroup> tagGroup) {
this.tagGroup = tagGroup;
}
}

View File

@ -0,0 +1,322 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.excel.wecom.domain.CustomerDataChangeLog;
import com.ruoyi.excel.wecom.domain.CustomerExportData;
import com.ruoyi.excel.wecom.domain.CustomerExportDataHistory;
import com.ruoyi.excel.wecom.mapper.CustomerDataChangeLogMapper;
import com.ruoyi.excel.wecom.mapper.CustomerExportDataHistoryMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Field;
import java.security.MessageDigest;
import java.util.*;
/**
* 客户数据变更追踪服务
* 提供数据指纹计算变更检测历史记录保存等功能
*/
@Service
public class CustomerDataChangeTrackingService {
@Autowired
private CustomerExportDataHistoryMapper historyMapper;
@Autowired
private CustomerDataChangeLogMapper changeLogMapper;
/**
* 字段中文名称映射
*/
private static final Map<String, String> FIELD_LABEL_MAP = new HashMap<>();
static {
FIELD_LABEL_MAP.put("customerName", "客户名称");
FIELD_LABEL_MAP.put("description", "描述");
FIELD_LABEL_MAP.put("gender", "性别");
FIELD_LABEL_MAP.put("addUserName", "添加人");
FIELD_LABEL_MAP.put("addUserAccount", "添加人账号");
FIELD_LABEL_MAP.put("addUserDepartment", "添加人所属部门");
FIELD_LABEL_MAP.put("addTime", "添加时间");
FIELD_LABEL_MAP.put("addDate", "添加日期");
FIELD_LABEL_MAP.put("source", "来源");
FIELD_LABEL_MAP.put("mobile", "手机");
FIELD_LABEL_MAP.put("company", "企业");
FIELD_LABEL_MAP.put("email", "邮箱");
FIELD_LABEL_MAP.put("address", "地址");
FIELD_LABEL_MAP.put("position", "职务");
FIELD_LABEL_MAP.put("phone", "电话");
FIELD_LABEL_MAP.put("tagGroup1", "标签组1(投放)");
FIELD_LABEL_MAP.put("tagGroup2", "标签组2(公司孵化)");
FIELD_LABEL_MAP.put("tagGroup3", "标签组3(商务)");
FIELD_LABEL_MAP.put("tagGroup4", "标签组4(成交日期)");
FIELD_LABEL_MAP.put("tagGroup5", "标签组5(年级组)");
FIELD_LABEL_MAP.put("tagGroup6", "标签组6(客户属性)");
FIELD_LABEL_MAP.put("tagGroup7", "标签组7(成交)");
FIELD_LABEL_MAP.put("tagGroup8", "标签组8(成交品牌)");
FIELD_LABEL_MAP.put("tagGroup9", "标签组9(线索通标签)");
FIELD_LABEL_MAP.put("tagGroup10", "标签组10(A1组)");
FIELD_LABEL_MAP.put("tagGroup11", "标签组11(B1组)");
FIELD_LABEL_MAP.put("tagGroup12", "标签组12(C1组)");
FIELD_LABEL_MAP.put("tagGroup13", "标签组13(D1组)");
FIELD_LABEL_MAP.put("tagGroup14", "标签组14(E1组)");
FIELD_LABEL_MAP.put("tagGroup15", "标签组15(意向度)");
FIELD_LABEL_MAP.put("tagGroup16", "标签组16(自然流)");
FIELD_LABEL_MAP.put("tagGroup17", "标签组17(F1组)");
FIELD_LABEL_MAP.put("tagGroup18", "标签组18(G1组)");
}
/**
* 计算数据指纹MD5哈希值
* 基于所有业务字段生成唯一标识用于快速判断数据是否变更
*
* @param data 客户导出数据
* @return MD5哈希值
*/
public String calculateDataFingerprint(CustomerExportData data) {
if (data == null) {
return null;
}
try {
// 构建用于计算指纹的字符串排除id字段
StringBuilder sb = new StringBuilder();
sb.append(data.getCustomerName()).append("|");
sb.append(data.getDescription()).append("|");
sb.append(data.getGender()).append("|");
sb.append(data.getAddUserName()).append("|");
sb.append(data.getAddUserAccount()).append("|");
sb.append(data.getAddUserDepartment()).append("|");
sb.append(data.getAddTime()).append("|");
sb.append(data.getAddDate()).append("|");
sb.append(data.getSource()).append("|");
sb.append(data.getMobile()).append("|");
sb.append(data.getCompany()).append("|");
sb.append(data.getEmail()).append("|");
sb.append(data.getAddress()).append("|");
sb.append(data.getPosition()).append("|");
sb.append(data.getPhone()).append("|");
sb.append(data.getTagGroup1()).append("|");
sb.append(data.getTagGroup2()).append("|");
sb.append(data.getTagGroup3()).append("|");
sb.append(data.getTagGroup4()).append("|");
sb.append(data.getTagGroup5()).append("|");
sb.append(data.getTagGroup6()).append("|");
sb.append(data.getTagGroup7()).append("|");
sb.append(data.getTagGroup8()).append("|");
sb.append(data.getTagGroup9()).append("|");
sb.append(data.getTagGroup10()).append("|");
sb.append(data.getTagGroup11()).append("|");
sb.append(data.getTagGroup12()).append("|");
sb.append(data.getTagGroup13()).append("|");
sb.append(data.getTagGroup14()).append("|");
sb.append(data.getTagGroup15()).append("|");
sb.append(data.getTagGroup16()).append("|");
sb.append(data.getTagGroup17()).append("|");
sb.append(data.getTagGroup18());
// 计算MD5
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(sb.toString().getBytes("UTF-8"));
// 转换为16进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : digest) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException("计算数据指纹失败", e);
}
}
/**
* 检查数据是否发生变更
*
* @param customerId 客户ID
* @param newData 新数据
* @return true-数据已变更false-数据未变更
*/
public boolean hasDataChanged(Long customerId, CustomerExportData newData) {
if (customerId == null || newData == null) {
return true;
}
// 计算新数据的指纹
String newFingerprint = calculateDataFingerprint(newData);
// 查询最新的历史记录
Integer maxVersion = historyMapper.selectMaxVersionByCustomerId(customerId);
if (maxVersion == null || maxVersion == 0) {
// 没有历史记录说明是新数据
return true;
}
// 查询最新版本的历史记录
CustomerExportDataHistory latestHistory = historyMapper.selectByCustomerIdAndVersion(customerId, maxVersion);
if (latestHistory == null) {
return true;
}
// 比较指纹
return !newFingerprint.equals(latestHistory.getDataFingerprint());
}
/**
* 保存数据变更记录包括历史快照和变更日志
*
* @param customerId 客户ID
* @param newData 新数据
* @param changeType 变更类型INSERT-新增, UPDATE-更新, DELETE-删除
* @return 历史记录ID
*/
@Transactional(rollbackFor = Exception.class)
public Long saveDataChange(Long customerId, CustomerExportData newData, String changeType) {
if (customerId == null || newData == null) {
return null;
}
// 1. 获取当前最新版本号
Integer maxVersion = historyMapper.selectMaxVersionByCustomerId(customerId);
int newVersion = (maxVersion == null ? 0 : maxVersion) + 1;
// 2. 计算数据指纹
String dataFingerprint = calculateDataFingerprint(newData);
// 3. 创建历史记录
CustomerExportDataHistory history = new CustomerExportDataHistory();
BeanUtils.copyProperties(newData, history);
history.setHistoryId(null); // 清除ID让数据库自动生成
history.setCustomerId(customerId);
history.setVersion(newVersion);
history.setDataFingerprint(dataFingerprint);
history.setChangeType(changeType);
history.setChangeTime(new Date());
// 4. 保存历史记录
historyMapper.insert(history);
// 5. 如果是更新操作记录具体的字段变更
if ("UPDATE".equals(changeType) && newVersion > 1) {
// 获取上一个版本的数据
CustomerExportDataHistory previousHistory = historyMapper.selectByCustomerIdAndVersion(customerId, newVersion - 1);
if (previousHistory != null) {
// 比较字段变更
List<CustomerDataChangeLog> changeLogs = compareAndGenerateChangeLogs(
history.getHistoryId(), customerId, newVersion, previousHistory, history);
// 批量保存变更日志
if (!changeLogs.isEmpty()) {
changeLogMapper.batchInsert(changeLogs);
}
}
}
return history.getHistoryId();
}
/**
* 比较两个版本的数据生成变更日志
*
* @param historyId 历史记录ID
* @param customerId 客户ID
* @param version 版本号
* @param oldData 旧数据
* @param newData 新数据
* @return 变更日志列表
*/
private List<CustomerDataChangeLog> compareAndGenerateChangeLogs(
Long historyId, Long customerId, Integer version,
CustomerExportDataHistory oldData, CustomerExportDataHistory newData) {
List<CustomerDataChangeLog> changeLogs = new ArrayList<>();
Date changeTime = new Date();
try {
// 获取所有字段
Field[] fields = CustomerExportDataHistory.class.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
// 跳过不需要比较的字段
if (shouldSkipField(fieldName)) {
continue;
}
// 获取旧值和新值
Object oldValue = field.get(oldData);
Object newValue = field.get(newData);
// 比较值是否变更
if (!Objects.equals(oldValue, newValue)) {
CustomerDataChangeLog changeLog = new CustomerDataChangeLog();
changeLog.setHistoryId(historyId);
changeLog.setCustomerId(customerId);
changeLog.setFieldName(fieldName);
changeLog.setFieldLabel(FIELD_LABEL_MAP.getOrDefault(fieldName, fieldName));
changeLog.setOldValue(oldValue != null ? oldValue.toString() : null);
changeLog.setNewValue(newValue != null ? newValue.toString() : null);
changeLog.setChangeTime(changeTime);
changeLog.setVersion(version);
changeLogs.add(changeLog);
}
}
} catch (Exception e) {
throw new RuntimeException("比较数据变更失败", e);
}
return changeLogs;
}
/**
* 判断字段是否需要跳过比较
*/
private boolean shouldSkipField(String fieldName) {
return "serialVersionUID".equals(fieldName)
|| "historyId".equals(fieldName)
|| "customerId".equals(fieldName)
|| "version".equals(fieldName)
|| "dataFingerprint".equals(fieldName)
|| "changeType".equals(fieldName)
|| "changeTime".equals(fieldName);
}
/**
* 查询客户的所有历史版本
*
* @param customerId 客户ID
* @return 历史记录列表
*/
public List<CustomerExportDataHistory> getCustomerHistory(Long customerId) {
return historyMapper.selectHistoryByCustomerId(customerId);
}
/**
* 查询客户的所有变更日志
*
* @param customerId 客户ID
* @return 变更日志列表
*/
public List<CustomerDataChangeLog> getCustomerChangeLogs(Long customerId) {
return changeLogMapper.selectChangeLogByCustomerId(customerId);
}
/**
* 查询指定版本的变更日志
*
* @param customerId 客户ID
* @param version 版本号
* @return 变更日志列表
*/
public List<CustomerDataChangeLog> getChangeLogsByVersion(Long customerId, Integer version) {
return changeLogMapper.selectChangeLogByCustomerIdAndVersion(customerId, version);
}
}

View File

@ -0,0 +1,338 @@
package com.ruoyi.excel.wecom.service;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
/**
* 客户导出服务类
*/
@Service
public class CustomerExportService {
@Autowired
private CorpUserMapper corpUserMapper;
@Autowired
private CorpDepartmentMapper corpDepartmentMapper;
@Autowired
private WecomTagMapper wecomTagMapper;
@Autowired
private WecomTagGroupMapper wecomTagGroupMapper;
@Autowired
private CustomerExportDataMapper customerExportDataMapper;
@Autowired
private CustomerDataChangeTrackingService changeTrackingService;
// 缓存映射
private Map<String, CorpUser> userIdToUserMap = new HashMap<>();
private Map<Long, String> 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<>();
/**
* 初始化缓存数据
* 在服务启动时自动执行
*/
@PostConstruct
public void initCache() {
System.out.println("开始初始化缓存数据...");
// 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() + " 个用户");
// 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() + " 个部门");
// 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() + " 个标签");
// 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() + " 个标签组");
// 5. 构建标签组到标签列表的映射
for (WecomTagDomain tag : tagList) {
String groupId = tag.getTagGroupId();
tagGroupIdToTagsMap.computeIfAbsent(groupId, k -> new ArrayList<>()).add(tag);
}
System.out.println("缓存初始化完成!");
}
public List<Date> getAllDate() {
return customerExportDataMapper.getDistinctDate();
}
/**
* 处理客户数据并保存到数据库
* 集成数据变更追踪功能
* 1. 检查数据是否发生变更通过数据指纹
* 2. 如果数据变更保存历史快照和变更日志
* 3. 更新或插入当前数据
*
* @param customerList 客户列表
* @return 处理成功的数量
*/
public int handleData(List<WecomCustomer> customerList) {
if (customerList == null || customerList.isEmpty()) {
System.out.println("客户列表为空,无需处理");
return 0;
}
int successCount = 0;
int insertCount = 0;
int updateCount = 0;
int unchangedCount = 0;
System.out.println("开始处理 " + customerList.size() + " 个客户数据...");
for (WecomCustomer customer : customerList) {
try {
CustomerExportData exportData = convertToExportData(customer);
if (exportData != null) {
// 根据唯一标识客户名称+添加人账号+添加时间查询是否已存在
CustomerExportData existingData = customerExportDataMapper.selectByUniqueKey(
exportData.getCustomerName(),
exportData.getAddUserAccount(),
exportData.getAddTime()
);
if (existingData == null) {
// 新增客户
customerExportDataMapper.insert(exportData);
// 保存新增记录到历史表
changeTrackingService.saveDataChange(exportData.getId(), exportData, "INSERT");
insertCount++;
successCount++;
System.out.println("新增客户: " + exportData.getCustomerName());
} else {
// 检查数据是否发生变更
boolean hasChanged = changeTrackingService.hasDataChanged(existingData.getId(), exportData);
if (hasChanged) {
// 数据已变更更新数据库
exportData.setId(existingData.getId());
customerExportDataMapper.updateById(exportData);
// 保存变更记录到历史表和变更日志表
changeTrackingService.saveDataChange(existingData.getId(), exportData, "UPDATE");
updateCount++;
successCount++;
System.out.println("更新客户: " + exportData.getCustomerName() + " (数据已变更)");
} else {
// 数据未变更跳过更新
unchangedCount++;
System.out.println("跳过客户: " + exportData.getCustomerName() + " (数据未变更)");
}
}
}
} catch (Exception e) {
System.err.println("处理客户数据失败: " + 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 + "");
return successCount;
}
/**
* 将企业微信客户数据转换为导出数据实体
*
* @param customer 企业微信客户数据
* @return 导出数据实体
*/
private CustomerExportData convertToExportData(WecomCustomer customer) {
if (customer == null) {
return null;
}
CustomerExportData exportData = new CustomerExportData();
WecomCustomer.ExternalContact externalContact = customer.getExternalContact();
//设置性别
if(externalContact != null) {
exportData.setGender(externalContact.getGender());
}
// 获取跟进信息
WecomCustomer.FollowInfo followInfo = customer.getFollowInfo();
if (followInfo != null) {
exportData.setCustomerName(followInfo.getRemark());
exportData.setDescription(followInfo.getDescription());
// 添加人信息
String userId = followInfo.getUserid();
CorpUser user = userIdToUserMap.get(userId);
if (user != null) {
exportData.setAddUserName(user.getName());
exportData.setAddUserAccount(user.getUserid());
// 获取部门信息
Long departId = user.getDepartId();
if (departId != null) {
exportData.setAddUserDepartment(user.getDepartmentName());
}
}
// 添加时间
Long createTime = followInfo.getCreatetime();
if (createTime != null) {
exportData.setAddTime(new Date(createTime * 1000));
exportData.setAddDate(new Date(createTime * 1000));
}
// 来源
Integer addWay = followInfo.getAddWay();
exportData.setSource(AddWayEnum.getDescriptionByCode(addWay));
// 备注信息
exportData.setMobile(followInfo.getRemarkMobiles() != null && !followInfo.getRemarkMobiles().isEmpty()
? String.join(",", followInfo.getRemarkMobiles()) : null);
exportData.setCompany(followInfo.getRemarkCorpName());
// 处理标签 - 按标签组分类
List<String> tagIds = followInfo.getTagId();
if (tagIds != null && !tagIds.isEmpty()) {
processCustomerTags(exportData, tagIds);
}
}
return exportData;
}
/**
* 处理客户标签按标签组分类填充到对应字段
*
* @param exportData 导出数据实体
* @param tagIds 标签ID列表
*/
private void processCustomerTags(CustomerExportData exportData, List<String> tagIds) {
// 按标签组分组
Map<String, List<String>> groupedTags = new HashMap<>();
for (String tagId : tagIds) {
WecomTagDomain tag = tagIdToTagMap.get(tagId);
if (tag != null) {
String groupId = tag.getTagGroupId();
groupedTags.computeIfAbsent(groupId, k -> new ArrayList<>()).add(tag.getName());
}
}
// 将标签填充到对应的标签组字段
for (Map.Entry<String, List<String>> entry : groupedTags.entrySet()) {
String groupId = entry.getKey();
String tagNames = String.join(",", entry.getValue());
WecomTagGroupDomain tagGroup = tagGroupIdToGroupMap.get(groupId);
if (tagGroup != null) {
String groupName = tagGroup.getName();
// 根据标签组名称填充到对应字段
// 这里需要根据实际的标签组名称进行映射
fillTagGroupField(exportData, groupName, tagNames);
}
}
}
/**
* 根据标签组名称填充到对应的字段
*
* @param exportData 导出数据实体
* @param groupName 标签组名称
* @param tagNames 标签名称逗号分隔
*/
private void fillTagGroupField(CustomerExportData exportData, String groupName, String tagNames) {
// 根据标签组名称映射到对应字段
// 这里的映射关系需要根据实际业务调整
if (groupName.contains("投放")) {
exportData.setTagGroup1(tagNames);
} else if (groupName.contains("公司孵化")) {
exportData.setTagGroup2(tagNames);
} else if (groupName.contains("商务")) {
exportData.setTagGroup3(tagNames);
} else if (groupName.contains("成交日期")) {
exportData.setTagGroup4(tagNames);
} 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("线索通")) {
exportData.setTagGroup9(tagNames);
} else if (groupName.contains("A1")) {
exportData.setTagGroup10(tagNames);
} else if (groupName.contains("B1")) {
exportData.setTagGroup11(tagNames);
} else if (groupName.contains("C1")) {
exportData.setTagGroup12(tagNames);
} else if (groupName.contains("D1")) {
exportData.setTagGroup13(tagNames);
} else if (groupName.contains("E1")) {
exportData.setTagGroup14(tagNames);
} else if (groupName.contains("意向度")) {
exportData.setTagGroup15(tagNames);
} else if (groupName.contains("自然流")) {
exportData.setTagGroup16(tagNames);
} else if (groupName.contains("F1")) {
exportData.setTagGroup17(tagNames);
} else if (groupName.contains("G1")) {
exportData.setTagGroup18(tagNames);
}
}
/**
* 手动刷新缓存
*/
public void refreshCache() {
initCache();
}
/**
* 获取缓存统计信息
*
* @return 缓存统计信息
*/
public Map<String, Integer> getCacheStats() {
Map<String, Integer> stats = new HashMap<>();
stats.put("users", userIdToUserMap.size());
stats.put("departments", departmentIdToNameMap.size());
stats.put("tags", tagIdToTagMap.size());
stats.put("tagGroups", tagGroupIdToGroupMap.size());
return stats;
}
}

View File

@ -0,0 +1,59 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.excel.wecom.domain.CustomerContactData;
import com.ruoyi.excel.wecom.vo.CustomerContactDataVO;
import java.util.Date;
import java.util.List;
/**
* 客户联系统计数据Service接口
*/
public interface ICustomerContactDataService {
/**
* 查询客户联系统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param userid 成员ID
* @return 客户联系统计数据列表
*/
List<CustomerContactData> selectCustomerContactDataList(Date startDate, Date endDate, String userid);
/**
* 查询客户联系统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param userid 成员ID
* @return 客户联系统计数据VO列表
*/
List<CustomerContactDataVO> selectCustomerContactDataVOList(Date startDate, Date endDate, String userid);
/**
* 根据ID查询客户联系统计数据
* @param id 主键ID
* @return 客户联系统计数据
*/
CustomerContactData selectCustomerContactDataById(Long id);
/**
* 新增客户联系统计数据
* @param customerContactData 客户联系统计数据
* @return 结果
*/
int insertCustomerContactData(CustomerContactData customerContactData);
/**
* 修改客户联系统计数据
* @param customerContactData 客户联系统计数据
* @return 结果
*/
int updateCustomerContactData(CustomerContactData customerContactData);
/**
* 批量删除客户联系统计数据
* @param ids 需要删除的数据ID
* @return 结果
*/
int deleteCustomerContactDataByIds(Long[] ids);
}

View File

@ -0,0 +1,59 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.excel.wecom.domain.CustomerExportData;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import java.util.Date;
import java.util.List;
/**
* 客户导出数据Service接口
*/
public interface ICustomerExportDataService {
/**
* 查询客户导出数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称
* @return 客户导出数据列表
*/
List<CustomerExportData> selectCustomerExportDataList(Date startDate, Date endDate, String customerName);
/**
* 查询客户导出数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称
* @return 客户导出数据VO列表
*/
List<CustomerExportDataVO> selectCustomerExportDataVOList(Date startDate, Date endDate, String customerName);
/**
* 根据ID查询客户导出数据
* @param id 主键ID
* @return 客户导出数据
*/
CustomerExportData selectCustomerExportDataById(Long id);
/**
* 新增客户导出数据
* @param customerExportData 客户导出数据
* @return 结果
*/
int insertCustomerExportData(CustomerExportData customerExportData);
/**
* 修改客户导出数据
* @param customerExportData 客户导出数据
* @return 结果
*/
int updateCustomerExportData(CustomerExportData customerExportData);
/**
* 批量删除客户导出数据
* @param ids 需要删除的数据ID
* @return 结果
*/
int deleteCustomerExportDataByIds(Long[] ids);
}

View File

@ -0,0 +1,62 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsData;
import com.ruoyi.excel.wecom.vo.CustomerStatisticsDataVO;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 客户统计数据Service接口
*/
public interface ICustomerStatisticsDataService {
/**
* 查询客户统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param indicatorName 指标名称
* @return 客户统计数据列表
*/
List<CustomerStatisticsData> selectCustomerStatisticsDataList(Date startDate, Date endDate, String indicatorName);
/**
* 查询客户统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param indicatorName 指标名称
* @return 客户统计数据VO列表
*/
List<CustomerStatisticsDataVO> selectCustomerStatisticsDataVOList(Date startDate, Date endDate, String indicatorName);
/**
* 根据ID查询客户统计数据
* @param id 主键ID
* @return 客户统计数据
*/
CustomerStatisticsData selectCustomerStatisticsDataById(Long id);
/**
* 新增客户统计数据
* @param customerStatisticsData 客户统计数据
* @return 结果
*/
int insertCustomerStatisticsData(CustomerStatisticsData customerStatisticsData);
/**
* 修改客户统计数据
* @param customerStatisticsData 客户统计数据
* @return 结果
*/
int updateCustomerStatisticsData(CustomerStatisticsData customerStatisticsData);
/**
* 批量删除客户统计数据
* @param ids 需要删除的数据ID
* @return 结果
*/
int deleteCustomerStatisticsDataByIds(Long[] ids);
int updateCost(Date curDate, BigDecimal totalCost, String titleAttr);
}

View File

@ -0,0 +1,63 @@
package com.ruoyi.excel.wecom.service;
import com.ruoyi.excel.wecom.domain.DepartmentStatisticsData;
import com.ruoyi.excel.wecom.vo.DepartmentStatisticsDataVO;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 部门统计数据Service接口
*/
public interface IDepartmentStatisticsDataService {
/**
* 查询部门统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径
* @return 部门统计数据列表
*/
List<DepartmentStatisticsData> selectDepartmentStatisticsDataList(Date startDate, Date endDate, String departmentPath);
/**
* 查询部门统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径
* @return 部门统计数据VO列表
*/
List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOList(Date startDate, Date endDate, String departmentPath);
/**
* 根据ID查询部门统计数据
* @param id 主键ID
* @return 部门统计数据
*/
DepartmentStatisticsData selectDepartmentStatisticsDataById(Long id);
/**
* 新增部门统计数据
* @param departmentStatisticsData 部门统计数据
* @return 结果
*/
int insertDepartmentStatisticsData(DepartmentStatisticsData departmentStatisticsData);
/**
* 修改部门统计数据
* @param departmentStatisticsData 部门统计数据
* @return 结果
*/
int updateDepartmentStatisticsData(DepartmentStatisticsData departmentStatisticsData);
/**
* 批量删除部门统计数据
* @param ids 需要删除的数据ID
* @return 结果
*/
int deleteDepartmentStatisticsDataByIds(Long[] ids);
Map<String, BigDecimal> getSummary(Date startDate, Date endDate, String departmentPath);
}

View File

@ -0,0 +1,103 @@
package com.ruoyi.excel.wecom.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.excel.wecom.model.WecomConfig;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
/**
* 企业微信基础服务类
*/
public class WecomBaseService {
private WecomConfig wecomConfig;
public WecomBaseService(WecomConfig wecomConfig) {
this.wecomConfig = wecomConfig;
}
/**
* 获取accessToken
*
* @return accessToken
* @throws IOException IO异常
*/
public String getAccessToken() throws IOException {
if (!wecomConfig.isAccessTokenExpired()) {
return wecomConfig.getAccessToken();
}
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + wecomConfig.getCorpId() + "&corpsecret=" + wecomConfig.getCorpSecret();
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);
return accessToken;
} else {
throw new RuntimeException("获取accessToken失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 发送GET请求
*
* @param url 请求URL
* @return 响应结果
* @throws IOException IO异常
*/
public String doGet(String url) throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, "UTF-8");
} finally {
response.close();
httpClient.close();
}
}
/**
* 发送POST请求
*
* @param url 请求URL
* @param data 请求数据
* @return 响应结果
* @throws IOException IO异常
*/
public String doPost(String url, String data) throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(data, "UTF-8"));
httpPost.setHeader("Content-Type", "application/json");
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, "UTF-8");
} finally {
response.close();
httpClient.close();
}
}
public WecomConfig getWecomConfig() {
return wecomConfig;
}
public void setWecomConfig(WecomConfig wecomConfig) {
this.wecomConfig = wecomConfig;
}
}

View File

@ -0,0 +1,412 @@
package com.ruoyi.excel.wecom.service;
import com.alibaba.fastjson.JSON;
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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
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();
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);
if (jsonObject.getInteger("errcode") == 0) {
return jsonObject.getJSONArray("follow_user").toJavaList(String.class);
} else {
throw new RuntimeException("获取配置了客户联系功能的成员列表失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 获取指定成员的客户列表
*
* @param userid 成员ID
* @return 客户外部联系人ID列表
* @throws IOException IO异常
*/
public List<String> getExternalContactList(String userid) throws IOException {
String accessToken = getAccessToken();
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);
if (jsonObject.getInteger("errcode") == 0) {
return jsonObject.getJSONArray("external_userid").toJavaList(String.class);
} else {
throw new RuntimeException("获取成员客户列表失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 获取所有成员的客户列表
*
* @return 成员客户映射关系
* @throws IOException IO异常
*/
public List<MemberCustomer> getAllMembersCustomers() throws IOException {
List<String> userList = getFollowUserList();
List<MemberCustomer> result = new ArrayList<>();
for (String userid : userList) {
List<String> customerList = getExternalContactList(userid);
MemberCustomer memberCustomer = new MemberCustomer();
memberCustomer.setUserid(userid);
memberCustomer.setExternalUserids(customerList);
result.add(memberCustomer);
}
return result;
}
/**
* 批量获取客户详情
*
* @param useridList 成员ID列表最多100个
* @param cursor 分页游标首次调用传空
* @param limit 分页大小默认100
* @return 客户详情列表
* @throws IOException IO异常
*/
public String batchGetCustomerDetails(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 url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user?access_token=" + accessToken;
JSONObject requestBody = new JSONObject();
requestBody.put("userid_list", useridList);
if (cursor != null) {
requestBody.put("cursor", cursor);
}
if (limit != null) {
requestBody.put("limit", limit);
}
String response = doPost(url, requestBody.toJSONString());
JSONObject jsonObject = JSON.parseObject(response);
if (jsonObject.getInteger("errcode") == 0) {
List<WecomCustomer> customerList = new ArrayList<>();
if (jsonObject.containsKey("external_contact_list")) {
customerList = jsonObject.getJSONArray("external_contact_list").toJavaList(WecomCustomer.class);
finalResult.addAll(customerList);
}
String nextCursor = null;
if (finalResult.size() >= 2000) {
if (jsonObject.containsKey("next_cursor") && StringUtils.isNotEmpty(jsonObject.getString("next_cursor"))) {
nextCursor = jsonObject.getString("next_cursor");
return nextCursor;
} else {
return null;
}
} else if (jsonObject.containsKey("next_cursor") && StringUtils.isNotEmpty(jsonObject.getString("next_cursor"))) {
nextCursor = jsonObject.getString("next_cursor");
return batchGetCustomerDetails(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"));
}
}
/**
* 批量获取所有客户详情
*
* @param useridList 成员ID列表
* @return 客户详情列表
* @throws IOException IO异常
*/
public List<WecomCustomer> batchGetAllCustomerDetails(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);
}
return allCustomers;
}
/**
* 获取单个客户详情
*
* @param externalUserid 外部联系人ID
* @return 客户详情
* @throws IOException IO异常
*/
public WecomCustomer getCustomerDetail(String externalUserid) throws IOException {
String accessToken = getAccessToken();
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);
if (jsonObject.getInteger("errcode") == 0) {
WecomCustomer customer = new WecomCustomer();
customer.setExternalContact(jsonObject.getObject("external_contact", WecomCustomer.ExternalContact.class));
if (jsonObject.containsKey("follow_info")) {
customer.setFollowInfo(jsonObject.getObject("follow_info", WecomCustomer.FollowInfo.class));
}
return customer;
} else {
throw new RuntimeException("获取客户详情失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 获取企业标签库
*
* @return 标签库列表
* @throws IOException IO异常
*/
public List<WecomTagGroup> getCorpTagList() throws IOException {
String accessToken = getAccessToken();
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);
if (jsonObject.getInteger("errcode") == 0) {
List<WecomTagGroup> tagGroupList = new ArrayList<>();
if (jsonObject.containsKey("tag_group")) {
tagGroupList = jsonObject.getJSONArray("tag_group").toJavaList(WecomTagGroup.class);
}
return tagGroupList;
} else {
throw new RuntimeException("获取企业标签库失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 成员客户映射类
*/
public static class MemberCustomer {
private String userid;
private List<String> externalUserids;
public String getUserid() {
return userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
public List<String> getExternalUserids() {
return externalUserids;
}
public void setExternalUserids(List<String> externalUserids) {
this.externalUserids = externalUserids;
}
}
/**
* 获取子部门id列表
*
* @param departmentId 部门ID根部门传1
* @return 子部门ID列表
* @throws IOException IO异常
*/
public List<CorpDepartment> getDepartmentList(Long departmentId) throws IOException {
String accessToken = getAccessToken();
String url = "https://qyapi.weixin.qq.com/cgi-bin/department/simplelist?access_token=" + accessToken;
if (departmentId != null) {
url += "&id=" + departmentId;
}
String response = doGet(url);
JSONObject jsonObject = JSON.parseObject(response);
if (jsonObject.getInteger("errcode") == 0) {
List<CorpDepartment> departmentIds = new ArrayList<>();
if (jsonObject.containsKey("department_id")) {
departmentIds = jsonObject.getJSONArray("department_id").toJavaList(CorpDepartment.class);
}
return departmentIds;
} else {
throw new RuntimeException("获取子部门id列表失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 获取单个部门详情
*
* @param departmentId 部门ID
* @return 部门详情
* @throws IOException IO异常
*/
public String getDepartmentDetail(Long departmentId) throws IOException {
String accessToken = getAccessToken();
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);
if (jsonObject.getInteger("errcode") == 0) {
if (jsonObject.containsKey("department")) {
JSONObject departInfo = jsonObject.getJSONObject("department");
if(departInfo.containsKey("name")) {
return departInfo.get("name").toString();
}
}
throw new RuntimeException("获取部门详情失败: " + jsonObject.getString("errmsg"));
} else {
throw new RuntimeException("获取部门详情失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 获取部门成员详情
*
* @param departmentId 部门ID
* @param fetchChild 是否递归获取子部门成员
* @param status 成员状态0-全部1-已激活2-已禁用4-未激活5-退出企业
* @return 部门成员详情列表
* @throws IOException IO异常
*/
public List<CorpUser> getDepartmentMemberList(Long departmentId, Integer fetchChild, Integer status) throws IOException {
String accessToken = getAccessToken();
String url = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=" + accessToken + "&department_id=" + departmentId;
if (fetchChild != null) {
url += "&fetch_child=" + fetchChild;
}
if (status != null) {
url += "&status=" + status;
}
String response = doGet(url);
JSONObject jsonObject = JSON.parseObject(response);
if (jsonObject.getInteger("errcode") == 0) {
List<CorpUser> memberList = new ArrayList<>();
if (jsonObject.containsKey("userlist")) {
List<JSONObject> userList = jsonObject.getJSONArray("userlist").toJavaList(JSONObject.class);
for (JSONObject user : userList) {
CorpUser member = new CorpUser();
member.setUserid(user.getString("userid"));
member.setName(user.getString("name"));
if (user.containsKey("department")) {
member.setDepartmentIds(user.getJSONArray("department").toJSONString());
}
if (user.containsKey("open_userid")) {
member.setOpenUserid(user.getString("open_userid"));
}
memberList.add(member);
}
}
return memberList;
} else {
throw new RuntimeException("获取部门成员详情失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 获取联系客户统计数据
*
* @param startTime 开始时间戳
* @param endTime 结束时间戳
* @param useridList 成员ID列表
* @return 统计数据
* @throws IOException IO异常
*/
public JSONObject getUserBehaviorData(Long startTime, Long endTime, List<String> useridList) throws IOException {
String accessToken = getAccessToken();
String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_user_behavior_data?access_token=" + accessToken;
JSONObject requestBody = new JSONObject();
requestBody.put("start_time", startTime);
requestBody.put("end_time", endTime);
if (useridList != null && !useridList.isEmpty()) {
requestBody.put("userid_list", useridList);
}
String response = doPost(url, requestBody.toJSONString());
JSONObject jsonObject = JSON.parseObject(response);
if (jsonObject.getInteger("errcode") == 0) {
return jsonObject;
} else {
throw new RuntimeException("获取联系客户统计数据失败: " + jsonObject.getString("errmsg"));
}
}
/**
* 获取某一天的联系客户统计数据
*
* @param date 日期格式yyyy-MM-dd
* @param useridList 成员ID列表
* @return 统计数据
* @throws IOException IO异常
*/
public JSONObject getDayUserBehaviorData(String date, List<String> useridList) throws IOException {
try {
// 解析日期字符串为Date对象
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
Date targetDate = sdf.parse(date);
// 计算当天的开始时间戳00:00:00
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.setTime(targetDate);
calendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
calendar.set(java.util.Calendar.MINUTE, 0);
calendar.set(java.util.Calendar.SECOND, 0);
Long startTime = calendar.getTimeInMillis() / 1000;
// 计算当天的结束时间戳23:59:59
calendar.set(java.util.Calendar.HOUR_OF_DAY, 23);
calendar.set(java.util.Calendar.MINUTE, 59);
calendar.set(java.util.Calendar.SECOND, 59);
Long endTime = calendar.getTimeInMillis() / 1000;
// 调用获取统计数据的方法
return getUserBehaviorData(startTime, endTime, useridList);
} catch (java.text.ParseException e) {
throw new RuntimeException("日期格式错误请使用yyyy-MM-dd格式", e);
}
}
}

View File

@ -0,0 +1,309 @@
package com.ruoyi.excel.wecom.service;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.excel.wecom.domain.CustomerContactData;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsData;
import com.ruoyi.excel.wecom.mapper.CustomerContactDataMapper;
import com.ruoyi.excel.wecom.mapper.CustomerStatisticsDataMapper;
import com.ruoyi.excel.wecom.vo.CustomerStatisticsVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* 企业微信统计数据服务类
*/
@Service
public class WecomStatisticsService {
@Autowired
private CustomerStatisticsDataMapper customerStatisticsDataMapper;
@Autowired
private CustomerContactDataMapper customerContactDataMapper;
/**
* 处理并存储联系客户统计数据
*
* @param date 日期
* @param statisticsData 统计数据
* @return 处理成功的数量
*/
public int handleAndSaveCustomerStatistics(String date, JSONObject statisticsData) {
if (statisticsData == null || !statisticsData.containsKey("behavior_data")) {
return 0;
}
JSONArray behaviorDataArray = statisticsData.getJSONArray("behavior_data");
if (behaviorDataArray == null || behaviorDataArray.isEmpty()) {
return 0;
}
int successCount = 0;
try {
Date curDate = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(date);
for (int i = 0; i < behaviorDataArray.size(); i++) {
JSONObject behaviorData = behaviorDataArray.getJSONObject(i);
String userid = behaviorData.getString("userid");
JSONObject data = behaviorData.getJSONObject("data");
// 处理每个统计指标
successCount += handleStatisticsIndicators(curDate, userid, data);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("处理联系客户统计数据失败: " + e.getMessage());
}
return successCount;
}
/**
* 处理统计指标并保存到数据库
*
* @param curDate 当前日期
* @param userid 成员ID
* @param data 统计数据
* @return 处理成功的数量
*/
private int handleStatisticsIndicators(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"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理聊天次数
if (data.containsKey("chat_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "聊天次数", userid, data.getInteger("chat_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理发送消息数
if (data.containsKey("msg_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "发送消息数", userid, data.getInteger("msg_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理语音通话时长
if (data.containsKey("voice_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "语音通话时长(秒)", userid, data.getInteger("voice_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理视频通话时长
if (data.containsKey("video_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "视频通话时长(秒)", userid, data.getInteger("video_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
// 处理拜访次数
if (data.containsKey("visit_cnt")) {
CustomerStatisticsData statisticsData = createCustomerStatisticsData(curDate, "拜访次数", userid, data.getInteger("visit_cnt"));
customerStatisticsDataMapper.insert(statisticsData);
successCount++;
}
return successCount;
}
/**
* 创建客户统计数据实体
*
* @param curDate 当前日期
* @param indicatorName 指标名称
* @param userid 成员ID
* @param value 指标值
* @return 客户统计数据实体
*/
private CustomerStatisticsData createCustomerStatisticsData(Date curDate, String indicatorName, String userid, Integer value) {
CustomerStatisticsData statisticsData = new CustomerStatisticsData();
statisticsData.setCurDate(curDate);
statisticsData.setIndicatorName(indicatorName);
// 这里需要根据userid映射到对应的部门组
// 示例暂时将所有数据存储到N组
statisticsData.setNtfGroup(value != null ? value.toString() : "0");
// 其他组暂时设置为空
statisticsData.setOfhGroup("");
statisticsData.setPswGroup("");
statisticsData.setWa1Group("");
statisticsData.setXb1Group("");
statisticsData.setYc1Group("");
statisticsData.setZd1Group("");
statisticsData.setAaGroup("");
statisticsData.setAcGroup("");
statisticsData.setAdGroup("");
statisticsData.setAeGroup("");
return statisticsData;
}
/**
* 根据日期获取客户统计数据
*
* @param date 日期
* @return 客户统计数据列表
*/
public List<CustomerStatisticsVO> getCustomerStatisticsByDate(Date date) {
return customerStatisticsDataMapper.selectByDate(date);
}
/**
* 处理并存储详细的客户联系统计数据
*
* @param statisticsData 统计数据
* @return 处理成功的数量
*/
public int handleAndSaveCustomerContactData(JSONObject statisticsData) {
if (statisticsData == null || !statisticsData.containsKey("behavior_data")) {
return 0;
}
JSONArray behaviorDataArray = statisticsData.getJSONArray("behavior_data");
if (behaviorDataArray == null || behaviorDataArray.isEmpty()) {
return 0;
}
int successCount = 0;
try {
for (int i = 0; i < behaviorDataArray.size(); i++) {
JSONObject behaviorData = behaviorDataArray.getJSONObject(i);
String userid = behaviorData.getString("userid");
JSONObject data = behaviorData.getJSONObject("data");
// 处理每个成员的详细统计数据
successCount += handleCustomerContactData(userid, data);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("处理客户联系统计数据失败: " + e.getMessage());
}
return successCount;
}
/**
* 处理单个成员的客户联系统计数据
*
* @param userid 成员ID
* @param data 统计数据
* @return 处理成功的数量
*/
private int handleCustomerContactData(String userid, JSONObject data) {
int successCount = 0;
// 处理每个统计时间点的数据
JSONArray statDataArray = data.getJSONArray("stat_data");
if (statDataArray == null || statDataArray.isEmpty()) {
return 0;
}
for (int i = 0; i < statDataArray.size(); i++) {
JSONObject statData = statDataArray.getJSONObject(i);
CustomerContactData contactData = createCustomerContactData(userid, statData);
if (contactData != null) {
customerContactDataMapper.insert(contactData);
successCount++;
}
}
return successCount;
}
/**
* 创建客户联系统计数据实体
*
* @param userid 成员ID
* @param statData 统计数据
* @return 客户联系统计数据实体
*/
private CustomerContactData createCustomerContactData(String userid, JSONObject statData) {
CustomerContactData contactData = new CustomerContactData();
// 设置统计时间戳
if (statData.containsKey("stat_time")) {
contactData.setStatTime(statData.getLong("stat_time"));
// 根据时间戳设置统计日期
Date statDate = new Date(statData.getLong("stat_time") * 1000);
contactData.setStatDate(statDate);
}
// 设置聊天次数
if (statData.containsKey("chat_cnt")) {
contactData.setChatCnt(statData.getInteger("chat_cnt"));
}
// 设置消息次数
if (statData.containsKey("message_cnt")) {
contactData.setMessageCnt(statData.getInteger("message_cnt"));
}
// 设置回复率
if (statData.containsKey("reply_percentage")) {
contactData.setReplyPercentage(statData.getDouble("reply_percentage"));
}
// 设置平均回复时间
if (statData.containsKey("avg_reply_time")) {
contactData.setAvgReplyTime(statData.getInteger("avg_reply_time"));
}
// 设置负面反馈次数
if (statData.containsKey("negative_feedback_cnt")) {
contactData.setNegativeFeedbackCnt(statData.getInteger("negative_feedback_cnt"));
}
// 设置新申请次数
if (statData.containsKey("new_apply_cnt")) {
contactData.setNewApplyCnt(statData.getInteger("new_apply_cnt"));
}
// 设置新联系次数
if (statData.containsKey("new_contact_cnt")) {
contactData.setNewContactCnt(statData.getInteger("new_contact_cnt"));
}
// 设置成员ID
contactData.setUserid(userid);
return contactData;
}
/**
* 根据日期获取客户联系统计数据
*
* @param statDate 统计日期
* @return 客户联系统计数据列表
*/
public List<CustomerContactData> getCustomerContactDataByDate(Date statDate) {
return customerContactDataMapper.selectByStatDate(statDate);
}
/**
* 根据成员ID和日期获取客户联系统计数据
*
* @param userid 成员ID
* @param statDate 统计日期
* @return 客户联系统计数据列表
*/
public List<CustomerContactData> getCustomerContactDataByUseridAndDate(String userid, Date statDate) {
return customerContactDataMapper.selectByUseridAndStatDate(userid, statDate);
}
}

View File

@ -0,0 +1,97 @@
package com.ruoyi.excel.wecom.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
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.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;
/**
* 企业微信标签服务类
*/
@Service
public class WecomTagService {
@Autowired
private WecomTagGroupMapper wecomTagGroupMapper;
@Autowired
private WecomTagMapper wecomTagMapper;
/**
* 同步企业标签库到数据库
*
* @param tagGroupList 标签库列表
* @return 同步结果
*/
public boolean syncCorpTagList(List<WecomTagGroup> tagGroupList) {
try {
// 遍历标签组列表
for (WecomTagGroup tagGroup : tagGroupList) {
//保存标签组
WecomTagGroupDomain groupDomain = new WecomTagGroupDomain();
groupDomain.setTagGroupId(tagGroup.getGroupId());
groupDomain.setName(tagGroup.getGroupName());
groupDomain.setCreateTime(new Date(tagGroup.getCreateTime() * 1000));
groupDomain.setOrderNo(tagGroup.getOrder());
wecomTagGroupMapper.insert(groupDomain);
// 保存标签
List<WecomTag> tagList = tagGroup.getTag();
if (tagList != null && !tagList.isEmpty()) {
for (WecomTag tag : tagList) {
WecomTagDomain tagDomain = new WecomTagDomain();
BeanUtils.copyProperties(tag, tagDomain);
tagDomain.setTagId(tag.getId());
tagDomain.setTagGroupId(tagGroup.getGroupId());
tagDomain.setOrderNo(tag.getOrder());
tagDomain.setCreateTime(new Date(tag.getCreateTime() * 1000));
tagDomain.setSyncTime(new Date());
// 插入标签
wecomTagMapper.insert(tagDomain);
}
}
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取所有标签组
*
* @return 标签组列表
*/
public List<WecomTagGroupDomain> getAllTagGroups() {
if (wecomTagGroupMapper == null) {
System.out.println("Mapper 接口未初始化,无法获取标签组列表");
return new ArrayList<>();
}
return wecomTagGroupMapper.selectList(null);
}
/**
* 获取所有标签
*
* @return 标签列表
*/
public List<WecomTagDomain> getAllTags() {
if (wecomTagMapper == null) {
System.out.println("Mapper 接口未初始化,无法获取标签列表");
return new ArrayList<>();
}
return wecomTagMapper.selectList(null);
}
}

View File

@ -0,0 +1,89 @@
package com.ruoyi.excel.wecom.service.impl;
import com.ruoyi.excel.wecom.domain.CustomerContactData;
import com.ruoyi.excel.wecom.mapper.CustomerContactDataMapper;
import com.ruoyi.excel.wecom.service.ICustomerContactDataService;
import com.ruoyi.excel.wecom.vo.CustomerContactDataVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* 客户联系统计数据Service业务层处理
*/
@Service
public class CustomerContactDataServiceImpl implements ICustomerContactDataService {
@Autowired
private CustomerContactDataMapper customerContactDataMapper;
/**
* 查询客户联系统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param userid 成员ID
* @return 客户联系统计数据列表
*/
@Override
public List<CustomerContactData> selectCustomerContactDataList(Date startDate, Date endDate, String userid) {
return customerContactDataMapper.selectCustomerContactDataList(startDate, endDate, userid);
}
/**
* 查询客户联系统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param userid 成员ID
* @return 客户联系统计数据VO列表
*/
@Override
public List<CustomerContactDataVO> selectCustomerContactDataVOList(Date startDate, Date endDate, String userid) {
return customerContactDataMapper.selectCustomerContactDataVOList(startDate, endDate, userid);
}
/**
* 根据ID查询客户联系统计数据
* @param id 主键ID
* @return 客户联系统计数据
*/
@Override
public CustomerContactData selectCustomerContactDataById(Long id) {
return customerContactDataMapper.selectById(id);
}
/**
* 新增客户联系统计数据
* @param customerContactData 客户联系统计数据
* @return 结果
*/
@Override
public int insertCustomerContactData(CustomerContactData customerContactData) {
return customerContactDataMapper.insert(customerContactData);
}
/**
* 修改客户联系统计数据
* @param customerContactData 客户联系统计数据
* @return 结果
*/
@Override
public int updateCustomerContactData(CustomerContactData customerContactData) {
return customerContactDataMapper.updateById(customerContactData);
}
/**
* 批量删除客户联系统计数据
* @param ids 需要删除的数据ID
* @return 结果
*/
@Override
public int deleteCustomerContactDataByIds(Long[] ids) {
int count = 0;
for (Long id : ids) {
count += customerContactDataMapper.deleteById(id);
}
return count;
}
}

View File

@ -0,0 +1,89 @@
package com.ruoyi.excel.wecom.service.impl;
import com.ruoyi.excel.wecom.domain.CustomerExportData;
import com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper;
import com.ruoyi.excel.wecom.service.ICustomerExportDataService;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* 客户导出数据Service业务层处理
*/
@Service
public class CustomerExportDataServiceImpl implements ICustomerExportDataService {
@Autowired
private CustomerExportDataMapper customerExportDataMapper;
/**
* 查询客户导出数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称
* @return 客户导出数据列表
*/
@Override
public List<CustomerExportData> selectCustomerExportDataList(Date startDate, Date endDate, String customerName) {
return customerExportDataMapper.selectCustomerExportDataList(startDate, endDate, customerName);
}
/**
* 查询客户导出数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param customerName 客户名称
* @return 客户导出数据VO列表
*/
@Override
public List<CustomerExportDataVO> selectCustomerExportDataVOList(Date startDate, Date endDate, String customerName) {
return customerExportDataMapper.selectCustomerExportDataVOList(startDate, endDate, customerName);
}
/**
* 根据ID查询客户导出数据
* @param id 主键ID
* @return 客户导出数据
*/
@Override
public CustomerExportData selectCustomerExportDataById(Long id) {
return customerExportDataMapper.selectById(id);
}
/**
* 新增客户导出数据
* @param customerExportData 客户导出数据
* @return 结果
*/
@Override
public int insertCustomerExportData(CustomerExportData customerExportData) {
return customerExportDataMapper.insert(customerExportData);
}
/**
* 修改客户导出数据
* @param customerExportData 客户导出数据
* @return 结果
*/
@Override
public int updateCustomerExportData(CustomerExportData customerExportData) {
return customerExportDataMapper.updateById(customerExportData);
}
/**
* 批量删除客户导出数据
* @param ids 需要删除的数据ID
* @return 结果
*/
@Override
public int deleteCustomerExportDataByIds(Long[] ids) {
int count = 0;
for (Long id : ids) {
count += customerExportDataMapper.deleteById(id);
}
return count;
}
}

View File

@ -0,0 +1,194 @@
package com.ruoyi.excel.wecom.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.excel.wecom.domain.CustomerStatisticsData;
import com.ruoyi.excel.wecom.mapper.CustomerStatisticsDataMapper;
import com.ruoyi.excel.wecom.service.ICustomerStatisticsDataService;
import com.ruoyi.excel.wecom.vo.CustomerStatisticsDataVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.List;
/**
* 客户统计数据Service业务层处理
*/
@Service
public class CustomerStatisticsDataServiceImpl implements ICustomerStatisticsDataService {
@Autowired
private CustomerStatisticsDataMapper customerStatisticsDataMapper;
/**
* 查询客户统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param indicatorName 指标名称
* @return 客户统计数据列表
*/
@Override
public List<CustomerStatisticsData> selectCustomerStatisticsDataList(Date startDate, Date endDate, String indicatorName) {
return customerStatisticsDataMapper.selectCustomerStatisticsDataList(startDate, endDate, indicatorName);
}
/**
* 查询客户统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param indicatorName 指标名称
* @return 客户统计数据VO列表
*/
@Override
public List<CustomerStatisticsDataVO> selectCustomerStatisticsDataVOList(Date startDate, Date endDate, String indicatorName) {
return customerStatisticsDataMapper.selectCustomerStatisticsDataVOList(startDate, endDate, indicatorName);
}
/**
* 根据ID查询客户统计数据
* @param id 主键ID
* @return 客户统计数据
*/
@Override
public CustomerStatisticsData selectCustomerStatisticsDataById(Long id) {
return customerStatisticsDataMapper.selectById(id);
}
/**
* 新增客户统计数据
* @param customerStatisticsData 客户统计数据
* @return 结果
*/
@Override
public int insertCustomerStatisticsData(CustomerStatisticsData customerStatisticsData) {
return customerStatisticsDataMapper.insert(customerStatisticsData);
}
/**
* 修改客户统计数据
* @param customerStatisticsData 客户统计数据
* @return 结果
*/
@Override
public int updateCustomerStatisticsData(CustomerStatisticsData customerStatisticsData) {
return customerStatisticsDataMapper.updateById(customerStatisticsData);
}
/**
* 批量删除客户统计数据
* @param ids 需要删除的数据ID
* @return 结果
*/
@Override
public int deleteCustomerStatisticsDataByIds(Long[] ids) {
int count = 0;
for (Long id : ids) {
count += customerStatisticsDataMapper.deleteById(id);
}
return count;
}
@Override
@Transactional
public int updateCost(Date curDate, BigDecimal totalCost, String titleAttr) {
try {
// 1. 查询该日期的所有记录
LambdaQueryWrapper<CustomerStatisticsData> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustomerStatisticsData::getCurDate, curDate);
List<CustomerStatisticsData> dataList = customerStatisticsDataMapper.selectList(queryWrapper);
if (dataList == null || dataList.isEmpty()) {
throw new RuntimeException("未找到日期为 " + curDate + " 的统计数据");
}
// 2. 查找关键指标记录
CustomerStatisticsData totalCostData = findByIndicatorName(dataList, "总成本(当日)");
CustomerStatisticsData jinFenShuData = findByIndicatorName(dataList, "进粉数(当日)");
CustomerStatisticsData chengDanShuData = findByIndicatorName(dataList, "成单数(当日)");
CustomerStatisticsData danTiaoCostData = findByIndicatorName(dataList, "单条成本(当日)");
CustomerStatisticsData chengDanCostData = findByIndicatorName(dataList, "成单成本(当日)");
// 3. 更新总成本
setFieldValue(totalCostData, titleAttr, totalCost.toString());
customerStatisticsDataMapper.updateById(totalCostData);
// 4. 获取进粉数和成单数
String jinFenShuStr = getFieldValue(jinFenShuData, titleAttr);
String chengDanShuStr = getFieldValue(chengDanShuData, titleAttr);
// 5. 计算并更新单条成本
if (jinFenShuStr != null && !jinFenShuStr.trim().isEmpty()) {
BigDecimal jinFenShu = new BigDecimal(jinFenShuStr);
if (jinFenShu.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal danTiaoCost = totalCost.divide(jinFenShu, 2, RoundingMode.HALF_UP);
setFieldValue(danTiaoCostData, titleAttr, danTiaoCost.toString());
customerStatisticsDataMapper.updateById(danTiaoCostData);
} else {
setFieldValue(danTiaoCostData, titleAttr, "0");
customerStatisticsDataMapper.updateById(danTiaoCostData);
}
}
// 6. 计算并更新成单成本
if (chengDanShuStr != null && !chengDanShuStr.trim().isEmpty()) {
BigDecimal chengDanShu = new BigDecimal(chengDanShuStr);
if (chengDanShu.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal chengDanCost = totalCost.divide(chengDanShu, 2, RoundingMode.HALF_UP);
setFieldValue(chengDanCostData, titleAttr, chengDanCost.toString());
customerStatisticsDataMapper.updateById(chengDanCostData);
} else {
setFieldValue(chengDanCostData, titleAttr, "0");
customerStatisticsDataMapper.updateById(chengDanCostData);
}
}
return 1;
} catch (Exception e) {
throw new RuntimeException("更新成本数据失败: " + e.getMessage(), e);
}
}
/**
* 根据指标名称查找记录
*/
private CustomerStatisticsData findByIndicatorName(List<CustomerStatisticsData> dataList, String indicatorName) {
for (CustomerStatisticsData data : dataList) {
if (indicatorName.equals(data.getIndicatorName())) {
return data;
}
}
throw new RuntimeException("未找到指标: " + indicatorName);
}
/**
* 动态获取字段值
*/
private String getFieldValue(CustomerStatisticsData data, String fieldName) {
try {
Field field = CustomerStatisticsData.class.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(data);
return value != null ? value.toString() : null;
} catch (Exception e) {
throw new RuntimeException("获取字段值失败: " + fieldName, e);
}
}
/**
* 动态设置字段值
*/
private void setFieldValue(CustomerStatisticsData data, String fieldName, String value) {
try {
Field field = CustomerStatisticsData.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(data, value);
} catch (Exception e) {
throw new RuntimeException("设置字段值失败: " + fieldName, e);
}
}
}

View File

@ -0,0 +1,96 @@
package com.ruoyi.excel.wecom.service.impl;
import com.ruoyi.excel.wecom.domain.DepartmentStatisticsData;
import com.ruoyi.excel.wecom.mapper.DepartmentStatisticsDataMapper;
import com.ruoyi.excel.wecom.service.IDepartmentStatisticsDataService;
import com.ruoyi.excel.wecom.vo.DepartmentStatisticsDataVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 部门统计数据Service业务层处理
*/
@Service
public class DepartmentStatisticsDataServiceImpl implements IDepartmentStatisticsDataService {
@Autowired
private DepartmentStatisticsDataMapper departmentStatisticsDataMapper;
/**
* 查询部门统计数据列表
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径
* @return 部门统计数据列表
*/
@Override
public List<DepartmentStatisticsData> selectDepartmentStatisticsDataList(Date startDate, Date endDate, String departmentPath) {
return departmentStatisticsDataMapper.selectDepartmentStatisticsDataList(startDate, endDate, departmentPath);
}
/**
* 查询部门统计数据VO列表(用于导出)
* @param startDate 开始日期
* @param endDate 结束日期
* @param departmentPath 部门路径
* @return 部门统计数据VO列表
*/
@Override
public List<DepartmentStatisticsDataVO> selectDepartmentStatisticsDataVOList(Date startDate, Date endDate, String departmentPath) {
return departmentStatisticsDataMapper.selectDepartmentStatisticsDataVOList(startDate, endDate, departmentPath);
}
/**
* 根据ID查询部门统计数据
* @param id 主键ID
* @return 部门统计数据
*/
@Override
public DepartmentStatisticsData selectDepartmentStatisticsDataById(Long id) {
return departmentStatisticsDataMapper.selectById(id);
}
/**
* 新增部门统计数据
* @param departmentStatisticsData 部门统计数据
* @return 结果
*/
@Override
public int insertDepartmentStatisticsData(DepartmentStatisticsData departmentStatisticsData) {
return departmentStatisticsDataMapper.insert(departmentStatisticsData);
}
/**
* 修改部门统计数据
* @param departmentStatisticsData 部门统计数据
* @return 结果
*/
@Override
public int updateDepartmentStatisticsData(DepartmentStatisticsData departmentStatisticsData) {
return departmentStatisticsDataMapper.updateById(departmentStatisticsData);
}
/**
* 批量删除部门统计数据
* @param ids 需要删除的数据ID
* @return 结果
*/
@Override
public int deleteDepartmentStatisticsDataByIds(Long[] ids) {
int count = 0;
for (Long id : ids) {
count += departmentStatisticsDataMapper.deleteById(id);
}
return count;
}
@Override
public Map<String, BigDecimal> getSummary(Date startDate, Date endDate, String departmentPath) {
return departmentStatisticsDataMapper.getSummary(startDate,endDate,departmentPath);
}
}

View File

@ -0,0 +1,55 @@
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 lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客户联系统计数据VO
* 用于EasyExcel导出客户联系统计数据
*/
@Data
public class CustomerContactDataVO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty("统计日期")
@DateTimeFormat("yyyy-MM-dd")
@ColumnWidth(20)
private Date statDate;
@ExcelProperty("成员ID")
@ColumnWidth(20)
private String userid;
@ExcelProperty("聊天次数")
@ColumnWidth(15)
private Integer chatCnt;
@ExcelProperty("消息次数")
@ColumnWidth(15)
private Integer messageCnt;
@ExcelProperty("回复率(%)")
@ColumnWidth(15)
private Double replyPercentage;
@ExcelProperty("平均回复时间(秒)")
@ColumnWidth(20)
private Integer avgReplyTime;
@ExcelProperty("负面反馈次数")
@ColumnWidth(20)
private Integer negativeFeedbackCnt;
@ExcelProperty("新申请次数")
@ColumnWidth(15)
private Integer newApplyCnt;
@ExcelProperty("新联系次数")
@ColumnWidth(15)
private Integer newContactCnt;
}

View File

@ -0,0 +1,178 @@
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.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客户导出数据VO
* 用于EasyExcel导出客户数据
*/
@Data
public class CustomerExportDataVO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty("客户名称")
@Excel(name = "客户名称")
@ColumnWidth(20)
private String customerName;
@ExcelProperty("描述")
@Excel(name = "描述")
@ColumnWidth(30)
private String description;
@ExcelProperty("添加人")
@Excel(name = "添加人")
@ColumnWidth(15)
private String addUserName;
@ExcelProperty("添加人账号")
@Excel(name = "添加人账号")
@ColumnWidth(20)
private String addUserAccount;
@ExcelProperty("添加人所属部门")
@Excel(name = "添加人所属部门")
@ColumnWidth(25)
private String addUserDepartment;
@ExcelProperty("添加时间")
@Excel(name = "添加时间",dateFormat = "yyyy-MM-dd")
@DateTimeFormat("yyyy-MM-dd")
@ColumnWidth(20)
private Date addTime;
@ExcelProperty("来源")
@Excel(name = "来源")
@ColumnWidth(15)
private String source;
@ExcelProperty("手机")
@Excel(name = "手机")
@ColumnWidth(15)
private String mobile;
@ExcelProperty("企业")
@Excel(name = "企业")
@ColumnWidth(25)
private String company;
@ExcelProperty("邮箱")
@Excel(name = "邮箱")
@ColumnWidth(25)
private String email;
@ExcelProperty("地址")
@Excel(name = "地址")
@ColumnWidth(30)
private String address;
@ExcelProperty("职务")
@Excel(name = "职务")
@ColumnWidth(15)
private String position;
@ExcelProperty("电话")
@Excel(name = "电话")
@ColumnWidth(15)
private String phone;
@ExcelProperty("标签组1(投放)")
@Excel(name = "标签组1(投放)")
@ColumnWidth(20)
private String tagGroup1;
@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;
@ExcelProperty("标签组5(年级组)")
@Excel(name = "标签组5(年级组)")
@ColumnWidth(20)
private String tagGroup5;
@ExcelProperty("标签组6(客户属性)")
@Excel(name = "标签组6(客户属性)")
@ColumnWidth(20)
private String tagGroup6;
@ExcelProperty("标签组7(成交)")
@Excel(name = "标签组7(成交)")
@ColumnWidth(20)
private String tagGroup7;
@ExcelProperty("标签组8(成交品牌)")
@Excel(name = "标签组8(成交品牌)")
@ColumnWidth(20)
private String tagGroup8;
@ExcelProperty("标签组9(线索通标签)")
@Excel(name = "标签组9(线索通标签)")
@ColumnWidth(20)
private String tagGroup9;
@ExcelProperty("标签组10(A1组)")
@Excel(name = "标签组10(A1组)")
@ColumnWidth(20)
private String tagGroup10;
@ExcelProperty("标签组11(B1组)")
@Excel(name = "标签组11(B1组)")
@ColumnWidth(20)
private String tagGroup11;
@ExcelProperty("标签组12(C1组)")
@Excel(name = "标签组12(C1组)")
@ColumnWidth(20)
private String tagGroup12;
@ExcelProperty("标签组13(D1组)")
@Excel(name = "标签组13(D1组)")
@ColumnWidth(20)
private String tagGroup13;
@ExcelProperty("标签组14(E1组)")
@Excel(name = "标签组14(E1组)")
@ColumnWidth(20)
private String tagGroup14;
@ExcelProperty("标签组15(意向度)")
@Excel(name = "标签组15(意向度)")
@ColumnWidth(20)
private String tagGroup15;
@ExcelProperty("标签组16(自然流)")
@Excel(name = "标签组16(自然流)")
@ColumnWidth(20)
private String tagGroup16;
@ExcelProperty("标签组17(F1组)")
@Excel(name = "标签组17(F1组)")
@ColumnWidth(20)
private String tagGroup17;
@ExcelProperty("标签组18(G1组)")
@Excel(name = "标签组18(G1组)")
@ColumnWidth(20)
private String tagGroup18;
}

View File

@ -0,0 +1,174 @@
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 java.io.Serializable;
import java.util.Date;
/**
* 客户统计数据VO
* 用于EasyExcel导出客户统计数据
*/
public class CustomerStatisticsDataVO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty("统计日期")
@DateTimeFormat("yyyy-MM-dd")
@ColumnWidth(20)
private Date curDate;
@ExcelProperty("重要指标")
@ColumnWidth(30)
private String indicatorName;
@ExcelProperty("N组")
@ColumnWidth(15)
private String nGroup;
@ExcelProperty("O组(公司孵化)")
@ColumnWidth(20)
private String oGroup;
@ExcelProperty("P组(商务)")
@ColumnWidth(15)
private String pGroup;
@ExcelProperty("W组(A1组)")
@ColumnWidth(15)
private String wGroup;
@ExcelProperty("X组(B1组)")
@ColumnWidth(15)
private String xGroup;
@ExcelProperty("Y组(C1组)")
@ColumnWidth(15)
private String yGroup;
@ExcelProperty("Z组(D1组)")
@ColumnWidth(15)
private String zGroup;
@ExcelProperty("AA组(E1组)")
@ColumnWidth(15)
private String aaGroup;
@ExcelProperty("AC组(自然流)")
@ColumnWidth(20)
private String acGroup;
@ExcelProperty("AD组(F1组)")
@ColumnWidth(15)
private String adGroup;
@ExcelProperty("AE组(G1组)")
@ColumnWidth(15)
private String aeGroup;
public Date getCurDate() {
return curDate;
}
public void setCurDate(Date curDate) {
this.curDate = curDate;
}
public String getIndicatorName() {
return indicatorName;
}
public void setIndicatorName(String indicatorName) {
this.indicatorName = indicatorName;
}
public String getnGroup() {
return nGroup;
}
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

@ -0,0 +1,64 @@
package com.ruoyi.excel.wecom.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;
import java.io.Serializable;
/**
* 客户统计数据VO
* 用于存储30个统计指标的结果
*/
@Data
public class CustomerStatisticsVO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty("重要指标")
@ColumnWidth(30)
private String indicatorName;
@ExcelProperty("N组")
@ColumnWidth(15)
private String ntfGroup;
@ExcelProperty("O组(公司孵化)")
@ColumnWidth(15)
private String ofhGroup;
@ExcelProperty("P组(商务)")
@ColumnWidth(15)
private String pswGroup;
@ExcelProperty("W组(A1组)")
@ColumnWidth(15)
private String wa1Group;
@ExcelProperty("X组(B1组)")
@ColumnWidth(15)
private String xb1Group;
@ExcelProperty("Y组(C1组)")
@ColumnWidth(15)
private String yc1Group;
@ExcelProperty("Z组(D1组)")
@ColumnWidth(15)
private String zd1Group;
@ExcelProperty("AA组(E1组)")
@ColumnWidth(15)
private String aaGroup;
@ExcelProperty("AC组(自然流)")
@ColumnWidth(15)
private String acGroup;
@ExcelProperty("AD组(F1组)")
@ColumnWidth(15)
private String adGroup;
@ExcelProperty("AE组(G1组)")
@ColumnWidth(15)
private String aeGroup;
}

View File

@ -0,0 +1,45 @@
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 lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 部门统计数据导出VO
* 用于EasyExcel导出部门统计数据
*/
@Data
public class DepartmentStatisticsDataVO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty("统计日期")
@DateTimeFormat("yyyy-MM-dd")
@ColumnWidth(15)
private Date statDate;
@ExcelProperty("部门路径")
@ColumnWidth(40)
private String departmentPath;
@ExcelProperty("当日总承接数")
@ColumnWidth(15)
private Integer dailyTotalAccepted;
@ExcelProperty("当日总成单数")
@ColumnWidth(15)
private Integer dailyTotalOrders;
@ExcelProperty("当日转化率")
@ColumnWidth(15)
private BigDecimal dailyConversionRate;
@ExcelProperty("排序号")
@ColumnWidth(10)
private Integer sortNo;
}

View File

@ -0,0 +1,16 @@
# 企业微信配置
wecom:
# 企业ID
corp-id: "ww4f2fd849224439be"
# 应用密钥
app-secret: "gsRCPzJuKsmxQVQlOjZWgYVCQMvNvliuUSJSbK8AWzk"
# 应用配置
spring:
application:
name: excel-handle
# 定时任务配置
task:
scheduling:
pool:
size: 10

View File

@ -0,0 +1,187 @@
# ========================================
# 企业微信表格数据处理系统 - 配置示例
# ========================================
#
# 使用说明:
# 1. 复制本文件为 application.yml
# 2. 替换所有 "your_xxx_here" 为实际的配置值
# 3. 根据实际情况调整其他配置项
#
# ========================================
# 企业微信配置(必填)
# ========================================
wecom:
# 企业 ID
# 获取方式:企业微信管理后台 -> 我的企业 -> 企业信息 -> 企业 ID
# 示例ww1234567890abcdef
corp-id: "your_corp_id_here"
# 应用密钥
# 获取方式:企业微信管理后台 -> 应用管理 -> 选择应用 -> Secret
# 注意需要给应用授予「企业微信文档」API 权限
# 示例abc123def456ghi789jkl012mno345pqr678stu901vwx234yz
app-secret: "your_app_secret_here"
# 文档 ID
# 获取方式:打开企业微信文档,查看浏览器地址栏 URL
# URL 格式https://doc.weixin.qq.com/sheet/xxxxx
# xxxxx 部分就是 doc-id
# 示例e3_AQMAAgAGACcAZQBnAGcA
doc-id: "your_doc_id_here"
# 工作表 ID
# 获取方式:
# 方法1浏览器开发者工具查看网络请求中的 sheet_id 参数
# 方法2如果是第一个工作表通常为 "1"
# 示例1
sheet-id: "your_sheet_id_here"
# 查询范围A1 表示法)
# 格式说明:
# - A1:Z100 表示查询 A 到 Z 列,前 100 行
# - A1:AE1000 表示查询 A 到 AE 列,前 1000 行
# - A:Z 表示查询 A 到 Z 列的所有行
# 建议:根据实际表格的列数和预计行数设置,不宜过大
# 示例A1:AE1000
range: "A1:AE1000"
# 抓取间隔(分钟)
# 说明:定时任务的执行间隔,默认 60 分钟
# 建议:根据数据更新频率调整,不建议设置过小(避免频繁调用 API
fetch-interval: 60
# 企业微信 API 地址(可选,使用默认值即可)
access-token-url: "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
spreadsheet-url: "https://qyapi.weixin.qq.com/cgi-bin/wedoc/spreadsheet/get_sheet_range_data"
# ========================================
# 应用配置
# ========================================
spring:
application:
name: excel-handle
# ========================================
# 数据库配置(必填)
# ========================================
datasource:
# 数据库连接 URL
# 格式jdbc:mysql://主机:端口/数据库名?参数
# 示例jdbc:mysql://localhost:3306/ruoyi?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
url: jdbc:mysql://localhost:3306/ruoyi?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
# 数据库用户名
username: root
# 数据库密码
password: password
# 数据库驱动可选Spring Boot 会自动识别)
driver-class-name: com.mysql.cj.jdbc.Driver
# 连接池配置(可选)
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 初始连接数
initial-size: 5
# 最小空闲连接数
min-idle: 5
# 最大活跃连接数
max-active: 20
# 获取连接等待超时时间(毫秒)
max-wait: 60000
# 配置间隔多久进行一次检测,检测需要关闭的空闲连接(毫秒)
time-between-eviction-runs-millis: 60000
# 配置连接在池中的最小生存时间(毫秒)
min-evictable-idle-time-millis: 300000
# 验证连接是否有效的 SQL
validation-query: SELECT 1 FROM DUAL
# 是否在获取连接时检测其有效性
test-while-idle: true
# 是否在获取连接时检测其有效性
test-on-borrow: false
# 是否在归还连接时检测其有效性
test-on-return: false
# ========================================
# 定时任务配置
# ========================================
task:
scheduling:
# 定时任务线程池大小
pool:
size: 10
# 线程名称前缀
thread-name-prefix: schedule-
# ========================================
# MyBatis 配置(如果使用 MyBatis
# ========================================
mybatis:
# Mapper XML 文件位置
mapper-locations: classpath*:mapper/**/*Mapper.xml
# 实体类包路径
type-aliases-package: com.ruoyi.excel.entity,com.ruoyi.excel.domain,com.ruoyi.excel.wecom.entity
# MyBatis 配置文件位置
config-location: classpath:mybatis/mybatis-config.xml
# ========================================
# 日志配置
# ========================================
logging:
level:
# 根日志级别
root: INFO
# 企业微信相关日志级别(可设置为 DEBUG 查看详细信息)
com.ruoyi.excel.wecom: INFO
# SQL 日志级别(可设置为 DEBUG 查看 SQL 语句)
com.ruoyi.excel.mapper: INFO
# 日志文件配置
file:
# 日志文件路径
path: logs
# 日志文件名
name: logs/excel-handle.log
# 日志格式
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
# ========================================
# 服务器配置
# ========================================
server:
# 服务端口
port: 8080
# 应用上下文路径
servlet:
context-path: /
# Tomcat 配置
tomcat:
# URI 编码
uri-encoding: UTF-8
# 最大线程数
max-threads: 200
# 最小空闲线程数
min-spare-threads: 10
# ========================================
# 其他配置
# ========================================
# 是否启用定时任务(可选,默认启用)
# 如果只想手动触发同步,可以设置为 false
# schedule.enabled: true
# 数据处理批次大小(可选,默认 1000
# 用于控制批量处理数据的大小
# batch.size: 1000
# 缓存配置(可选)
# 用于控制数据去重缓存的行为
# cache:
# # 是否启用缓存
# enabled: true
# # 缓存过期时间(小时)
# expire-hours: 24

View File

@ -0,0 +1,9 @@
<?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.CorpUserMapper">
<select id="selectAllUserIds" resultType="java.lang.String">
select distinct userid from corp_user
</select>
</mapper>

View File

@ -0,0 +1,70 @@
<?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.CustomerContactDataMapper">
<!-- 查询客户联系统计数据列表 -->
<select id="selectCustomerContactDataList" resultType="com.ruoyi.excel.wecom.domain.CustomerContactData">
SELECT
id,
stat_date,
userid,
chat_cnt,
message_cnt,
reply_percentage,
avg_reply_time,
negative_feedback_cnt,
new_apply_cnt,
new_contact_cnt
FROM customer_contact_data
<where>
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND stat_date &lt;= #{endDate}
</if>
<if test="userid != null and userid != ''">
AND userid = #{userid}
</if>
</where>
ORDER BY stat_date DESC
</select>
<!-- 查询客户联系统计数据VO列表(用于导出) -->
<select id="selectCustomerContactDataVOList" resultType="com.ruoyi.excel.wecom.vo.CustomerContactDataVO">
SELECT
stat_date as statDate,
userid,
chat_cnt as chatCnt,
message_cnt as messageCnt,
reply_percentage as replyPercentage,
avg_reply_time as avgReplyTime,
negative_feedback_cnt as negativeFeedbackCnt,
new_apply_cnt as newApplyCnt,
new_contact_cnt as newContactCnt
FROM customer_contact_data
<where>
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND stat_date &lt;= #{endDate}
</if>
<if test="userid != null and userid != ''">
AND userid = #{userid}
</if>
</where>
ORDER BY stat_date DESC
</select>
<!-- 根据日期查询客户联系统计数据 -->
<select id="selectByStatDate" resultType="com.ruoyi.excel.wecom.domain.CustomerContactData">
SELECT * FROM customer_contact_data WHERE stat_date = #{statDate}
</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>
</mapper>

View File

@ -0,0 +1,44 @@
<?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.CustomerDataChangeLogMapper">
<!-- 根据客户ID查询所有变更日志按时间倒序 -->
<select id="selectChangeLogByCustomerId" resultType="com.ruoyi.excel.wecom.domain.CustomerDataChangeLog">
SELECT *
FROM customer_data_change_log
WHERE customer_id = #{customerId}
ORDER BY change_time DESC
</select>
<!-- 根据历史记录ID查询变更日志 -->
<select id="selectChangeLogByHistoryId" resultType="com.ruoyi.excel.wecom.domain.CustomerDataChangeLog">
SELECT *
FROM customer_data_change_log
WHERE history_id = #{historyId}
ORDER BY log_id
</select>
<!-- 根据客户ID和版本号查询变更日志 -->
<select id="selectChangeLogByCustomerIdAndVersion" resultType="com.ruoyi.excel.wecom.domain.CustomerDataChangeLog">
SELECT *
FROM customer_data_change_log
WHERE customer_id = #{customerId} AND version = #{version}
ORDER BY log_id
</select>
<!-- 批量插入变更日志 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO customer_data_change_log (
history_id, customer_id, version, field_name, field_label,
old_value, new_value, change_time
) VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.historyId}, #{item.customerId}, #{item.version},
#{item.fieldName}, #{item.fieldLabel}, #{item.oldValue},
#{item.newValue}, #{item.changeTime}
)
</foreach>
</insert>
</mapper>

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.CustomerExportDataHistoryMapper">
<!-- 根据客户ID查询最大版本号 -->
<select id="selectMaxVersionByCustomerId" resultType="java.lang.Integer">
SELECT MAX(version)
FROM customer_export_data_history
WHERE customer_id = #{customerId}
</select>
<!-- 根据客户ID和版本号查询历史记录 -->
<select id="selectByCustomerIdAndVersion" resultType="com.ruoyi.excel.wecom.domain.CustomerExportDataHistory">
SELECT *
FROM customer_export_data_history
WHERE customer_id = #{customerId} AND version = #{version}
</select>
<!-- 根据客户ID查询所有历史记录按版本号倒序 -->
<select id="selectHistoryByCustomerId" resultType="com.ruoyi.excel.wecom.domain.CustomerExportDataHistory">
SELECT *
FROM customer_export_data_history
WHERE customer_id = #{customerId}
ORDER BY version DESC
</select>
</mapper>

View File

@ -0,0 +1,118 @@
<?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="getDistinctDate" resultType="java.util.Date">
select distinct add_date from customer_export_data order by add_date
</select>
<!-- 查询客户导出数据VO列表(用于导出) -->
<select id="selectCustomerExportDataVOList" 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>
<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
</select>
<select id="selectCustomerExportDataList" resultType="com.ruoyi.excel.wecom.domain.CustomerExportData">
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>
<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
</select>
<!-- 根据客户唯一标识查询客户数据(客户名称+添加人账号+添加时间) -->
<select id="selectByUniqueKey" resultType="com.ruoyi.excel.wecom.domain.CustomerExportData">
SELECT *
FROM customer_export_data
WHERE customer_name = #{customerName}
AND add_user_account = #{addUserAccount}
AND add_time = #{addTime}
LIMIT 1
</select>
</mapper>

View File

@ -0,0 +1,49 @@
<?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.CustomerStatisticsDataMapper">
<!-- 根据标签ID查询标签 -->
<select id="selectByDate" resultType="com.ruoyi.excel.wecom.vo.CustomerStatisticsVO">
select *
from customer_statistics_data
where tag_id = #{tagId}
</select>
<!-- 查询客户统计数据列表 -->
<select id="selectCustomerStatisticsDataList" resultType="com.ruoyi.excel.wecom.domain.CustomerStatisticsData">
SELECT *
FROM customer_statistics_data
<where>
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND cur_date &lt;= #{endDate}
</if>
<if test="indicatorName != null and indicatorName != ''">
AND indicator_name LIKE CONCAT('%', #{indicatorName}, '%')
</if>
</where>
ORDER BY cur_date DESC,sort_no
</select>
<!-- 查询客户统计数据VO列表(用于导出) -->
<select id="selectCustomerStatisticsDataVOList" resultType="com.ruoyi.excel.wecom.vo.CustomerStatisticsDataVO">
SELECT
*
FROM customer_statistics_data
<where>
<if test="startDate != null">
AND cur_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND cur_date &lt;= #{endDate}
</if>
<if test="indicatorName != null and indicatorName != ''">
AND indicator_name LIKE CONCAT('%', #{indicatorName}, '%')
</if>
</where>
ORDER BY cur_date DESC,sort_no
</select>
</mapper>

View File

@ -0,0 +1,112 @@
<?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.DepartmentStatisticsDataMapper">
<!-- 根据日期查询统计数据 -->
<select id="selectByDate" resultType="com.ruoyi.excel.wecom.domain.DepartmentStatisticsData">
SELECT *
FROM department_statistics_data
WHERE stat_date = #{date}
ORDER BY department_path, sort_no
</select>
<!-- 根据部门路径和日期范围查询统计数据 -->
<select id="selectByDepartmentAndDateRange" resultType="com.ruoyi.excel.wecom.domain.DepartmentStatisticsData">
SELECT *
FROM department_statistics_data
WHERE department_path = #{departmentPath}
<if test="startDate != null">
AND stat_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND stat_date &lt;= #{endDate}
</if>
ORDER BY stat_date, sort_no
</select>
<!-- 查询指定部门的历史累计数据(总承接数) -->
<select id="selectHistoricalAcceptSum" resultType="map">
SELECT
SUM(daily_total_accepted) as total_value,
COUNT(*) as record_count
FROM department_statistics_data
WHERE department_path = #{departmentPath}
</select>
<!-- 查询指定部门的历史累计数据(总成单数) -->
<select id="selectHistoricalOrderSum" resultType="map">
SELECT
SUM(daily_total_orders) as total_value,
COUNT(*) as record_count
FROM department_statistics_data
WHERE department_path = #{departmentPath}
</select>
<!-- 查询部门统计数据VO列表(用于导出) -->
<select id="selectDepartmentStatisticsDataVOList" resultType="com.ruoyi.excel.wecom.vo.DepartmentStatisticsDataVO">
SELECT
stat_date as statDate,
department_path as departmentPath,
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
FROM department_statistics_data
<where>
<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>
ORDER BY stat_date DESC, department_path, sort_no
</select>
<select id="selectDepartmentStatisticsDataList"
resultType="com.ruoyi.excel.wecom.domain.DepartmentStatisticsData">
SELECT
stat_date as statDate,
department_path as departmentPath,
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
FROM department_statistics_data
<where>
<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>
ORDER BY stat_date DESC, department_path, sort_no
</select>
<select id="getSummary" resultType="java.util.Map">
select sum(daily_total_orders) as totalOrders,
sum(ifnull(daily_total_accepted,0)+ifnull(manager_accepted,0)) as totalIns from department_statistics_data
<where>
<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 = #{departmentPath}
</if>
</where>
</select>
</mapper>

View File

@ -0,0 +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.WecomTagGroupMapper">
<resultMap id="BaseResultMap" type="com.ruoyi.excel.wecom.domain.WecomTagGroupDomain">
<id column="id" property="id" />
<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="sync_time" property="syncTime" />
</resultMap>
<sql id="Base_Column_List">
id, tag_group_id, name, create_time, order_no, sync_time
</sql>
<!-- 根据标签组ID查询标签组 -->
<select id="selectByTagGroupId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from wecom_tag_group
where tag_group_id = #{tagGroupId}
</select>
</mapper>

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.WecomTagMapper">
<resultMap id="BaseResultMap" type="com.ruoyi.excel.wecom.domain.WecomTagDomain">
<id column="id" property="id" />
<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="sync_time" property="syncTime" />
</resultMap>
<sql id="Base_Column_List">
id, tag_id, tag_group_id, name, create_time, order, sync_time
</sql>
<!-- 根据标签ID查询标签 -->
<select id="selectByTagId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from wecom_tag
where tag_id = #{tagId}
</select>
</mapper>

View File

@ -0,0 +1,38 @@
-- 企业部门表
DROP TABLE IF EXISTS `corp_department`;
CREATE TABLE `corp_department` (
`id` BIGINT(20) NOT NULL COMMENT '部门ID企业微信部门ID',
`parentid` BIGINT(20) DEFAULT NULL COMMENT '父部门ID',
`order_no` BIGINT(20) DEFAULT NULL COMMENT '部门排序',
`name` VARCHAR(200) NOT NULL COMMENT '部门名称',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
INDEX `idx_parentid` (`parentid`),
INDEX `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业部门表';
-- 企业员工表
DROP TABLE IF EXISTS `corp_user`;
CREATE TABLE `corp_user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT 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格式',
`department_name` VARCHAR(500) DEFAULT NULL COMMENT '部门名称',
`name` VARCHAR(100) NOT NULL COMMENT '员工姓名',
`mobile` VARCHAR(50) DEFAULT NULL COMMENT '手机号',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`alias` VARCHAR(100) DEFAULT NULL COMMENT '别名',
`open_userid` VARCHAR(100) DEFAULT NULL COMMENT '全局唯一ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_userid` (`userid`),
INDEX `idx_depart_id` (`depart_id`),
INDEX `idx_name` (`name`),
INDEX `idx_mobile` (`mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业员工表';

View File

@ -0,0 +1,23 @@
-- 客户联系统计数据表
DROP TABLE IF EXISTS `customer_contact_data`;
CREATE TABLE `customer_contact_data` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`stat_time` BIGINT(20) DEFAULT NULL COMMENT '统计时间戳(秒)',
`stat_date` DATE DEFAULT NULL COMMENT '统计日期',
`chat_cnt` INT(11) DEFAULT 0 COMMENT '聊天次数',
`message_cnt` INT(11) DEFAULT 0 COMMENT '消息次数',
`reply_percentage` DOUBLE DEFAULT 0 COMMENT '回复率(%',
`avg_reply_time` INT(11) DEFAULT 0 COMMENT '平均回复时间(秒)',
`negative_feedback_cnt` INT(11) DEFAULT 0 COMMENT '负面反馈次数',
`new_apply_cnt` INT(11) DEFAULT 0 COMMENT '新申请次数',
`new_contact_cnt` INT(11) DEFAULT 0 COMMENT '新联系次数',
`userid` VARCHAR(100) DEFAULT NULL COMMENT '成员ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_stat_date` (`stat_date`),
INDEX `idx_userid` (`userid`),
INDEX `idx_stat_time` (`stat_time`),
INDEX `idx_userid_stat_date` (`userid`, `stat_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户联系统计数据表';

View File

@ -0,0 +1,20 @@
-- 客户数据变更日志表
DROP TABLE IF EXISTS `customer_data_change_log`;
CREATE TABLE `customer_data_change_log` (
`log_id` BIGINT(20) NOT NULL AUTO_INCREMENT 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 '变更字段名称(英文)',
`field_label` VARCHAR(100) DEFAULT NULL COMMENT '变更字段中文名称',
`old_value` TEXT DEFAULT NULL COMMENT '变更前的值',
`new_value` TEXT DEFAULT NULL COMMENT '变更后的值',
`change_time` DATETIME NOT NULL COMMENT '变更时间',
`version` INT(11) NOT NULL COMMENT '数据版本号',
PRIMARY KEY (`log_id`),
INDEX `idx_history_id` (`history_id`),
INDEX `idx_customer_id` (`customer_id`),
INDEX `idx_field_name` (`field_name`),
INDEX `idx_change_time` (`change_time`),
INDEX `idx_version` (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户数据变更日志表';

View File

@ -0,0 +1,194 @@
-- =============================================
-- 客户数据变更追踪系统 - 数据库建表脚本
-- =============================================
-- 1. 客户数据历史记录表
-- 用于保存客户数据的每个版本快照
DROP TABLE IF EXISTS `customer_export_data_history`;
CREATE TABLE `customer_export_data_history` (
`history_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '历史记录ID',
`customer_id` BIGINT(20) NOT NULL COMMENT '客户ID关联customer_export_data表的id',
`version` INT(11) NOT NULL COMMENT '版本号从1开始递增',
`data_fingerprint` VARCHAR(64) NOT NULL COMMENT '数据指纹MD5哈希值用于快速判断数据是否变更',
`change_type` VARCHAR(20) NOT NULL COMMENT '变更类型INSERT-新增, UPDATE-更新, DELETE-删除',
`change_time` DATETIME NOT NULL COMMENT '变更时间',
-- 以下字段为客户数据快照与customer_export_data表结构一致
`customer_name` VARCHAR(255) DEFAULT NULL COMMENT '客户名称',
`description` TEXT DEFAULT NULL COMMENT '描述',
`gender` INT(11) DEFAULT NULL COMMENT '性别0-未知, 1-男性, 2-女性',
`add_user_name` VARCHAR(255) DEFAULT NULL COMMENT '添加人姓名',
`add_user_account` VARCHAR(255) DEFAULT NULL COMMENT '添加人账号',
`add_user_department` VARCHAR(255) DEFAULT NULL COMMENT '添加人所属部门',
`add_time` DATETIME DEFAULT NULL COMMENT '添加时间',
`add_date` DATE DEFAULT NULL COMMENT '添加日期',
`source` VARCHAR(255) DEFAULT NULL COMMENT '来源',
`mobile` VARCHAR(255) DEFAULT NULL COMMENT '手机号',
`company` VARCHAR(255) DEFAULT NULL COMMENT '公司名称',
`email` VARCHAR(255) DEFAULT NULL COMMENT '邮箱',
`address` VARCHAR(500) DEFAULT NULL COMMENT '地址',
`position` VARCHAR(255) DEFAULT NULL COMMENT '职位',
`phone` VARCHAR(255) DEFAULT NULL COMMENT '电话',
`tag_group_1` TEXT DEFAULT NULL COMMENT '标签组1(投放)',
`tag_group_2` TEXT DEFAULT NULL COMMENT '标签组2(公司孵化)',
`tag_group_3` TEXT DEFAULT NULL COMMENT '标签组3(商务)',
`tag_group_4` TEXT DEFAULT NULL COMMENT '标签组4(成交日期)',
`tag_group_5` TEXT DEFAULT NULL COMMENT '标签组5(年级组)',
`tag_group_6` TEXT DEFAULT NULL COMMENT '标签组6(客户属性)',
`tag_group_7` TEXT DEFAULT NULL COMMENT '标签组7(成交)',
`tag_group_8` TEXT DEFAULT NULL COMMENT '标签组8(成交品牌)',
`tag_group_9` TEXT DEFAULT NULL COMMENT '标签组9(线索通标签)',
`tag_group_10` TEXT DEFAULT NULL COMMENT '标签组10(A1组)',
`tag_group_11` TEXT DEFAULT NULL COMMENT '标签组11(B1组)',
`tag_group_12` TEXT DEFAULT NULL COMMENT '标签组12(C1组)',
`tag_group_13` TEXT DEFAULT NULL COMMENT '标签组13(D1组)',
`tag_group_14` TEXT DEFAULT NULL COMMENT '标签组14(E1组)',
`tag_group_15` TEXT DEFAULT NULL COMMENT '标签组15(意向度)',
`tag_group_16` TEXT DEFAULT NULL COMMENT '标签组16(自然流)',
`tag_group_17` TEXT DEFAULT NULL COMMENT '标签组17(F1组)',
`tag_group_18` TEXT DEFAULT NULL COMMENT '标签组18(G1组)',
PRIMARY KEY (`history_id`),
KEY `idx_customer_id` (`customer_id`),
KEY `idx_version` (`version`),
KEY `idx_change_time` (`change_time`),
KEY `idx_customer_version` (`customer_id`, `version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户数据历史记录表';
-- 2. 客户数据变更日志表
-- 用于记录每个字段的具体变更内容
DROP TABLE IF EXISTS `customer_data_change_log`;
CREATE TABLE `customer_data_change_log` (
`log_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
`history_id` BIGINT(20) NOT NULL COMMENT '历史记录ID关联customer_export_data_history表',
`customer_id` BIGINT(20) NOT NULL COMMENT '客户ID',
`version` INT(11) NOT NULL COMMENT '版本号',
`field_name` VARCHAR(100) NOT NULL COMMENT '字段名称(英文)',
`field_label` VARCHAR(100) NOT NULL COMMENT '字段标签(中文)',
`old_value` TEXT DEFAULT NULL COMMENT '旧值',
`new_value` TEXT DEFAULT NULL COMMENT '新值',
`change_time` DATETIME NOT NULL COMMENT '变更时间',
PRIMARY KEY (`log_id`),
KEY `idx_history_id` (`history_id`),
KEY `idx_customer_id` (`customer_id`),
KEY `idx_version` (`version`),
KEY `idx_field_name` (`field_name`),
KEY `idx_change_time` (`change_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户数据变更日志表';
-- 3. 为customer_export_data表添加唯一索引
-- 使用"客户名称+添加人账号+添加时间"作为唯一标识
ALTER TABLE `customer_export_data` ADD UNIQUE INDEX `uk_customer_unique` (`customer_name`, `add_user_account`, `add_time`);
-- =============================================
-- Mapper XML 配置参考
-- =============================================
-- CustomerExportDataHistoryMapper.xml 需要添加以下SQL
/*
<!-- 根据客户ID查询最大版本号 -->
<select id="selectMaxVersionByCustomerId" resultType="java.lang.Integer">
SELECT MAX(version)
FROM customer_export_data_history
WHERE customer_id = #{customerId}
</select>
<!-- 根据客户ID和版本号查询历史记录 -->
<select id="selectByCustomerIdAndVersion" resultType="com.ruoyi.excel.wecom.domain.CustomerExportDataHistory">
SELECT *
FROM customer_export_data_history
WHERE customer_id = #{customerId} AND version = #{version}
</select>
<!-- 根据客户ID查询所有历史记录按版本号倒序 -->
<select id="selectHistoryByCustomerId" resultType="com.ruoyi.excel.wecom.domain.CustomerExportDataHistory">
SELECT *
FROM customer_export_data_history
WHERE customer_id = #{customerId}
ORDER BY version DESC
</select>
*/
-- CustomerDataChangeLogMapper.xml 需要添加以下SQL
/*
<!-- 根据客户ID查询所有变更日志按时间倒序 -->
<select id="selectChangeLogByCustomerId" resultType="com.ruoyi.excel.wecom.domain.CustomerDataChangeLog">
SELECT *
FROM customer_data_change_log
WHERE customer_id = #{customerId}
ORDER BY change_time DESC
</select>
<!-- 根据历史记录ID查询变更日志 -->
<select id="selectChangeLogByHistoryId" resultType="com.ruoyi.excel.wecom.domain.CustomerDataChangeLog">
SELECT *
FROM customer_data_change_log
WHERE history_id = #{historyId}
ORDER BY log_id
</select>
<!-- 根据客户ID和版本号查询变更日志 -->
<select id="selectChangeLogByCustomerIdAndVersion" resultType="com.ruoyi.excel.wecom.domain.CustomerDataChangeLog">
SELECT *
FROM customer_data_change_log
WHERE customer_id = #{customerId} AND version = #{version}
ORDER BY log_id
</select>
<!-- 批量插入变更日志 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO customer_data_change_log (
history_id, customer_id, version, field_name, field_label,
old_value, new_value, change_time
) VALUES
<foreach collection="changeLogs" item="item" separator=",">
(
#{item.historyId}, #{item.customerId}, #{item.version},
#{item.fieldName}, #{item.fieldLabel}, #{item.oldValue},
#{item.newValue}, #{item.changeTime}
)
</foreach>
</insert>
*/
-- CustomerExportDataMapper.xml 需要添加以下SQL
/*
<!-- 根据客户唯一标识查询客户数据(客户名称+添加人账号+添加时间) -->
<select id="selectByUniqueKey" resultType="com.ruoyi.excel.wecom.domain.CustomerExportData">
SELECT *
FROM customer_export_data
WHERE customer_name = #{customerName}
AND add_user_account = #{addUserAccount}
AND add_time = #{addTime}
LIMIT 1
</select>
*/
-- =============================================
-- 测试查询示例
-- =============================================
-- 查询某个客户的所有历史版本
-- SELECT * FROM customer_export_data_history WHERE customer_id = 1 ORDER BY version DESC;
-- 查询某个客户的所有变更日志
-- SELECT * FROM customer_data_change_log WHERE customer_id = 1 ORDER BY change_time DESC;
-- 查询某个版本的具体变更内容
-- SELECT * FROM customer_data_change_log WHERE customer_id = 1 AND version = 2;
-- 查询最近的变更记录(所有客户)
-- SELECT * FROM customer_data_change_log ORDER BY change_time DESC LIMIT 100;
-- 统计每个客户的版本数量
-- SELECT customer_id, COUNT(*) as version_count FROM customer_export_data_history GROUP BY customer_id;

View File

@ -0,0 +1,42 @@
-- 客户导出数据表
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 '客户姓名',
`add_user_name` VARCHAR(100) DEFAULT NULL COMMENT '添加人姓名',
`add_user_account` VARCHAR(100) DEFAULT NULL COMMENT '添加人账号',
`add_user_department` VARCHAR(200) DEFAULT NULL COMMENT '添加人部门',
`add_time` DATETIME DEFAULT NULL COMMENT '添加时间',
`add_date` DATE DEFAULT NULL COMMENT '添加日期',
`source` VARCHAR(50) DEFAULT NULL COMMENT '来源',
`mobile` VARCHAR(200) DEFAULT NULL COMMENT '手机号(可能多个,逗号分隔)',
`company` VARCHAR(200) DEFAULT NULL COMMENT '公司',
`description` TEXT DEFAULT NULL COMMENT '描述/备注',
`tag_group1` VARCHAR(500) DEFAULT NULL COMMENT '标签组1-投放',
`tag_group2` VARCHAR(500) DEFAULT NULL COMMENT '标签组2-公司孵化',
`tag_group3` VARCHAR(500) DEFAULT NULL COMMENT '标签组3-商务',
`tag_group4` VARCHAR(500) DEFAULT NULL COMMENT '标签组4-成交日期',
`tag_group5` VARCHAR(500) DEFAULT NULL COMMENT '标签组5-年级组',
`tag_group6` VARCHAR(500) DEFAULT NULL COMMENT '标签组6-客户属性',
`tag_group7` VARCHAR(500) DEFAULT NULL COMMENT '标签组7-成交',
`tag_group8` VARCHAR(500) DEFAULT NULL COMMENT '标签组8-成交品牌',
`tag_group9` VARCHAR(500) DEFAULT NULL COMMENT '标签组9-线索通',
`tag_group10` VARCHAR(500) DEFAULT NULL COMMENT '标签组10-A1',
`tag_group11` VARCHAR(500) DEFAULT NULL COMMENT '标签组11-B1',
`tag_group12` VARCHAR(500) DEFAULT NULL COMMENT '标签组12-C1',
`tag_group13` VARCHAR(500) DEFAULT NULL COMMENT '标签组13-D1',
`tag_group14` VARCHAR(500) DEFAULT NULL COMMENT '标签组14-E1',
`tag_group15` VARCHAR(500) DEFAULT NULL COMMENT '标签组15-意向度',
`tag_group16` VARCHAR(500) DEFAULT NULL COMMENT '标签组16-自然流',
`tag_group17` VARCHAR(500) DEFAULT NULL COMMENT '标签组17-F1',
`tag_group18` VARCHAR(500) 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`),
UNIQUE KEY `uk_customer_unique` (`customer_name`, `add_user_account`, `add_time`),
INDEX `idx_customer_name` (`customer_name`),
INDEX `idx_add_user_account` (`add_user_account`),
INDEX `idx_add_time` (`add_time`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户导出数据表';

View File

@ -0,0 +1,50 @@
-- 客户导出数据历史记录表
DROP TABLE IF EXISTS `customer_export_data_history`;
CREATE TABLE `customer_export_data_history` (
`history_id` BIGINT(20) NOT NULL AUTO_INCREMENT 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)',
`change_type` VARCHAR(20) NOT NULL COMMENT '变更类型:INSERT/UPDATE/DELETE',
`change_time` DATETIME NOT NULL COMMENT '变更时间',
`customer_name` VARCHAR(100) DEFAULT NULL COMMENT '客户姓名(快照)',
`description` TEXT DEFAULT NULL COMMENT '描述(快照)',
`gender` INT(11) DEFAULT NULL COMMENT '性别(快照)',
`add_user_name` VARCHAR(100) DEFAULT NULL COMMENT '添加人姓名(快照)',
`add_user_account` VARCHAR(100) DEFAULT NULL COMMENT '添加人账号(快照)',
`add_user_department` VARCHAR(200) DEFAULT NULL COMMENT '添加人部门(快照)',
`add_time` DATETIME DEFAULT NULL COMMENT '添加时间(快照)',
`add_date` DATE DEFAULT NULL COMMENT '添加日期(快照)',
`source` VARCHAR(50) DEFAULT NULL COMMENT '来源(快照)',
`mobile` VARCHAR(200) DEFAULT NULL COMMENT '手机号(快照)',
`company` VARCHAR(200) DEFAULT NULL COMMENT '公司(快照)',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱(快照)',
`address` VARCHAR(500) DEFAULT NULL COMMENT '地址(快照)',
`position` VARCHAR(100) DEFAULT NULL COMMENT '职务(快照)',
`phone` VARCHAR(50) DEFAULT NULL COMMENT '电话(快照)',
`tag_group1` VARCHAR(500) DEFAULT NULL COMMENT '标签组1-投放(快照)',
`tag_group2` VARCHAR(500) DEFAULT NULL COMMENT '标签组2-公司孵化(快照)',
`tag_group3` VARCHAR(500) DEFAULT NULL COMMENT '标签组3-商务(快照)',
`tag_group4` VARCHAR(500) DEFAULT NULL COMMENT '标签组4-成交日期(快照)',
`tag_group5` VARCHAR(500) DEFAULT NULL COMMENT '标签组5-年级组(快照)',
`tag_group6` VARCHAR(500) DEFAULT NULL COMMENT '标签组6-客户属性(快照)',
`tag_group7` VARCHAR(500) DEFAULT NULL COMMENT '标签组7-成交(快照)',
`tag_group8` VARCHAR(500) DEFAULT NULL COMMENT '标签组8-成交品牌(快照)',
`tag_group9` VARCHAR(500) DEFAULT NULL COMMENT '标签组9-线索通(快照)',
`tag_group10` VARCHAR(500) DEFAULT NULL COMMENT '标签组10-A1(快照)',
`tag_group11` VARCHAR(500) DEFAULT NULL COMMENT '标签组11-B1(快照)',
`tag_group12` VARCHAR(500) DEFAULT NULL COMMENT '标签组12-C1(快照)',
`tag_group13` VARCHAR(500) DEFAULT NULL COMMENT '标签组13-D1(快照)',
`tag_group14` VARCHAR(500) DEFAULT NULL COMMENT '标签组14-E1(快照)',
`tag_group15` VARCHAR(500) DEFAULT NULL COMMENT '标签组15-意向度(快照)',
`tag_group16` VARCHAR(500) DEFAULT NULL COMMENT '标签组16-自然流(快照)',
`tag_group17` VARCHAR(500) DEFAULT NULL COMMENT '标签组17-F1(快照)',
`tag_group18` VARCHAR(500) DEFAULT NULL COMMENT '标签组18-G1(快照)',
PRIMARY KEY (`history_id`),
INDEX `idx_customer_id` (`customer_id`),
INDEX `idx_version` (`version`),
INDEX `idx_change_time` (`change_time`),
INDEX `idx_change_type` (`change_type`),
INDEX `idx_data_fingerprint` (`data_fingerprint`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户导出数据历史记录表';

View File

@ -0,0 +1,26 @@
-- 客户统计数据表
DROP TABLE IF EXISTS `customer_statistics_data`;
CREATE TABLE `customer_statistics_data` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`cur_date` DATE DEFAULT NULL COMMENT '统计日期',
`indicator_name` VARCHAR(100) DEFAULT NULL COMMENT '重要指标名称',
`ntf_group` VARCHAR(50) DEFAULT NULL COMMENT 'N组(投放)',
`ofh_group` VARCHAR(50) DEFAULT NULL COMMENT 'O组(公司孵化)',
`psw_group` VARCHAR(50) DEFAULT NULL COMMENT 'P组(商务)',
`wa1_group` VARCHAR(50) DEFAULT NULL COMMENT 'W组(A1组)',
`xb1_group` VARCHAR(50) DEFAULT NULL COMMENT 'X组(B1组)',
`yc1_group` VARCHAR(50) DEFAULT NULL COMMENT 'Y组(C1组)',
`zd1_group` VARCHAR(50) DEFAULT NULL COMMENT 'Z组(D1组)',
`aa_group` VARCHAR(50) DEFAULT NULL COMMENT 'AA组(E1组)',
`ac_group` VARCHAR(50) DEFAULT NULL COMMENT 'AC组(自然流)',
`ad_group` VARCHAR(50) DEFAULT NULL COMMENT 'AD组(F1组)',
`ae_group` VARCHAR(50) DEFAULT NULL COMMENT 'AE组(G1组)',
`sort_no` 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`),
INDEX `idx_cur_date` (`cur_date`),
INDEX `idx_indicator_name` (`indicator_name`),
INDEX `idx_sort_no` (`sort_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户统计数据表';

View File

@ -0,0 +1,22 @@
-- 部门统计数据表(宽表模型)
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(500) NOT NULL COMMENT '部门路径(完整层级路径)',
`daily_total_accepted` INT(11) DEFAULT 0 COMMENT '当日总承接数',
`daily_total_orders` INT(11) DEFAULT 0 COMMENT '当日总成单数',
`daily_conversion_rate` DECIMAL(10, 2) DEFAULT 0.00 COMMENT '当日转化率(%',
`daily_timely_order_ratio` DECIMAL(10, 2) DEFAULT 0.00 COMMENT '及时单占比(当日)',
`daily_non_timely_order_ratio` DECIMAL(10, 2) DEFAULT 0.00 COMMENT '非及时单占比(当日)',
`sort_no` 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_stat_date_department` (`stat_date`, `department_path`(255)),
INDEX `idx_stat_date` (`stat_date`),
INDEX `idx_department_path` (`department_path`(255)),
INDEX `idx_sort_no` (`sort_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门统计数据表';

297
pom.xml Normal file
View File

@ -0,0 +1,297 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.9.1</version>
<name>ruoyi</name>
<url>http://www.ruoyi.vip</url>
<description>超脑智子测试系统</description>
<properties>
<ruoyi.version>3.9.1</ruoyi.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<spring-boot.version>2.5.15</spring-boot.version>
<druid.version>1.2.27</druid.version>
<yauaa.version>7.32.0</yauaa.version>
<swagger.version>3.0.0</swagger.version>
<kaptcha.version>2.3.3</kaptcha.version>
<pagehelper.boot.version>1.4.7</pagehelper.boot.version>
<fastjson.version>2.0.60</fastjson.version>
<oshi.version>6.9.1</oshi.version>
<commons.io.version>2.21.0</commons.io.version>
<poi.version>4.1.2</poi.version>
<velocity.version>2.3</velocity.version>
<jwt.version>0.9.1</jwt.version>
<mybatis-plus.version>3.5.3</mybatis-plus.version>
<!-- override dependency version -->
<tomcat.version>9.0.112</tomcat.version>
<logback.version>1.2.13</logback.version>
<spring-security.version>5.7.14</spring-security.version>
<easyexcel.version>3.3.2</easyexcel.version>
<spring-framework.version>5.3.39</spring-framework.version>
</properties>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- 覆盖SpringFramework的依赖配置-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring-framework.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 覆盖SpringSecurity的依赖配置-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>${spring-security.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 覆盖logback的依赖配置-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- 覆盖tomcat的依赖配置-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
<artifactId>yauaa</artifactId>
<version>${yauaa.version}</version>
</dependency>
<!-- pagehelper 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.boot.version}</version>
</dependency>
<!-- 获取系统信息 -->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>${oshi.version}</version>
</dependency>
<!-- Swagger3依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${swagger.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- io常用工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons.io.version}</version>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<!-- velocity代码生成使用模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- Fastjson2 Spring Boot Starter -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- 验证码 -->
<dependency>
<groupId>pro.fessional</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
<!-- 定时任务-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-quartz</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-generator</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 核心模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 系统模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 通用工具-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${ruoyi.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>ruoyi-admin</module>
<module>ruoyi-framework</module>
<module>ruoyi-system</module>
<module>ruoyi-quartz</module>
<module>ruoyi-generator</module>
<module>ruoyi-common</module>
<module>excel-handle</module>
</modules>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>

102
ruoyi-admin/pom.xml Normal file
View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-admin</artifactId>
<description>
web服务入口
</description>
<dependencies>
<!-- spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- 表示依赖不会传递 -->
</dependency>
<!-- swagger3-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</dependency>
<!-- 防止进入swagger页面报类型转换错误排除3.0.0中的引用手动增加1.6.2版本 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.6.2</version>
</dependency>
<!-- Mysql驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 核心模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
</dependency>
<!-- 定时任务-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-quartz</artifactId>
</dependency>
<!-- 代码生成-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-generator</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>excel-handle</artifactId>
<version>3.9.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
<configuration>
<fork>true</fork> <!-- 如果没有该配置devtools不会生效 -->
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<warName>${project.artifactId}</warName>
</configuration>
</plugin>
</plugins>
<finalName>${project.artifactId}</finalName>
</build>
</project>

View File

@ -0,0 +1,32 @@
package com.ruoyi;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
/**
* 启动程序
*
* @author ruoyi
*/
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }, scanBasePackages = { "com.ruoyi" })
@MapperScan("com.ruoyi.**.mapper")
public class RuoYiApplication
{
public static void main(String[] args)
{
// System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(RuoYiApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 超脑智子启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
}
}

View File

@ -0,0 +1,18 @@
package com.ruoyi;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* web容器中进行部署
*
* @author ruoyi
*/
public class RuoYiServletInitializer extends SpringBootServletInitializer
{
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
{
return application.sources(RuoYiApplication.class);
}
}

View File

@ -0,0 +1,94 @@
package com.ruoyi.web.controller.common;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.code.kaptcha.Producer;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.system.service.ISysConfigService;
/**
* 验证码操作处理
*
* @author ruoyi
*/
@RestController
public class CaptchaController
{
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
@Autowired
private RedisCache redisCache;
@Autowired
private ISysConfigService configService;
/**
* 生成验证码
*/
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
AjaxResult ajax = AjaxResult.success();
boolean captchaEnabled = configService.selectCaptchaEnabled();
ajax.put("captchaEnabled", captchaEnabled);
if (!captchaEnabled)
{
return ajax;
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType();
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
}

View File

@ -0,0 +1,162 @@
package com.ruoyi.web.controller.common;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.framework.config.ServerConfig;
/**
* 通用请求处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/common")
public class CommonController
{
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
@Autowired
private ServerConfig serverConfig;
private static final String FILE_DELIMITER = ",";
/**
* 通用下载请求
*
* @param fileName 文件名称
* @param delete 是否删除
*/
@GetMapping("/download")
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
{
try
{
if (!FileUtils.checkAllowDownload(fileName))
{
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
}
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
String filePath = RuoYiConfig.getDownloadPath() + fileName;
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, realFileName);
FileUtils.writeBytes(filePath, response.getOutputStream());
if (delete)
{
FileUtils.deleteFile(filePath);
}
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}
/**
* 通用上传请求单个
*/
@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception
{
try
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
/**
* 通用上传请求多个
*/
@PostMapping("/uploads")
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
{
try
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
List<String> urls = new ArrayList<String>();
List<String> fileNames = new ArrayList<String>();
List<String> newFileNames = new ArrayList<String>();
List<String> originalFilenames = new ArrayList<String>();
for (MultipartFile file : files)
{
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
urls.add(url);
fileNames.add(fileName);
newFileNames.add(FileUtils.getName(fileName));
originalFilenames.add(file.getOriginalFilename());
}
AjaxResult ajax = AjaxResult.success();
ajax.put("urls", StringUtils.join(urls, FILE_DELIMITER));
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMITER));
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMITER));
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMITER));
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
/**
* 本地资源通用下载
*/
@GetMapping("/download/resource")
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
throws Exception
{
try
{
if (!FileUtils.checkAllowDownload(resource))
{
throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
}
// 本地资源路径
String localPath = RuoYiConfig.getProfile();
// 数据库资源地址
String downloadPath = localPath + FileUtils.stripPrefix(resource);
// 下载名称
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, downloadName);
FileUtils.writeBytes(downloadPath, response.getOutputStream());
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}
}

View File

@ -0,0 +1,121 @@
package com.ruoyi.web.controller.monitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysCache;
/**
* 缓存监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/cache")
public class CacheController
{
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final static List<SysCache> caches = new ArrayList<SysCache>();
{
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception
{
Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
Map<String, Object> result = new HashMap<>(3);
result.put("info", info);
result.put("dbSize", dbSize);
List<Map<String, String>> pieList = new ArrayList<>();
commandStats.stringPropertyNames().forEach(key -> {
Map<String, String> data = new HashMap<>(2);
String property = commandStats.getProperty(key);
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
pieList.add(data);
});
result.put("commandStats", pieList);
return AjaxResult.success(result);
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping("/getNames")
public AjaxResult cache()
{
return AjaxResult.success(caches);
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping("/getKeys/{cacheName}")
public AjaxResult getCacheKeys(@PathVariable String cacheName)
{
Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
return AjaxResult.success(new TreeSet<>(cacheKeys));
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping("/getValue/{cacheName}/{cacheKey}")
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
{
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
return AjaxResult.success(sysCache);
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@DeleteMapping("/clearCacheName/{cacheName}")
public AjaxResult clearCacheName(@PathVariable String cacheName)
{
Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
redisTemplate.delete(cacheKeys);
return AjaxResult.success();
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@DeleteMapping("/clearCacheKey/{cacheKey}")
public AjaxResult clearCacheKey(@PathVariable String cacheKey)
{
redisTemplate.delete(cacheKey);
return AjaxResult.success();
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@DeleteMapping("/clearCacheAll")
public AjaxResult clearCacheAll()
{
Collection<String> cacheKeys = redisTemplate.keys("*");
redisTemplate.delete(cacheKeys);
return AjaxResult.success();
}
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.web.controller.monitor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.framework.web.domain.Server;
/**
* 服务器监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/server")
public class ServerController
{
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception
{
Server server = new Server();
server.copyTo();
return AjaxResult.success(server);
}
}

View File

@ -0,0 +1,82 @@
package com.ruoyi.web.controller.monitor;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.poi.ExcelUtil;
import com.ruoyi.framework.web.service.SysPasswordService;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.service.ISysLogininforService;
/**
* 系统访问记录
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/logininfor")
public class SysLogininforController extends BaseController
{
@Autowired
private ISysLogininforService logininforService;
@Autowired
private SysPasswordService passwordService;
@PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
@GetMapping("/list")
public TableDataInfo list(SysLogininfor logininfor)
{
startPage();
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
return getDataTable(list);
}
@Log(title = "登录日志", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysLogininfor logininfor)
{
List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
util.exportExcel(response, list, "登录日志");
}
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
@Log(title = "登录日志", businessType = BusinessType.DELETE)
@DeleteMapping("/{infoIds}")
public AjaxResult remove(@PathVariable Long[] infoIds)
{
return toAjax(logininforService.deleteLogininforByIds(infoIds));
}
@PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
@Log(title = "登录日志", businessType = BusinessType.CLEAN)
@DeleteMapping("/clean")
public AjaxResult clean()
{
logininforService.cleanLogininfor();
return success();
}
@PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
@Log(title = "账户解锁", businessType = BusinessType.OTHER)
@GetMapping("/unlock/{userName}")
public AjaxResult unlock(@PathVariable("userName") String userName)
{
passwordService.clearLoginRecordCache(userName);
return success();
}
}

View File

@ -0,0 +1,69 @@
package com.ruoyi.web.controller.monitor;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.poi.ExcelUtil;
import com.ruoyi.system.domain.SysOperLog;
import com.ruoyi.system.service.ISysOperLogService;
/**
* 操作日志记录
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/operlog")
public class SysOperlogController extends BaseController
{
@Autowired
private ISysOperLogService operLogService;
@PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
@GetMapping("/list")
public TableDataInfo list(SysOperLog operLog)
{
startPage();
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
return getDataTable(list);
}
@Log(title = "操作日志", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysOperLog operLog)
{
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
util.exportExcel(response, list, "操作日志");
}
@Log(title = "操作日志", businessType = BusinessType.DELETE)
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
@DeleteMapping("/{operIds}")
public AjaxResult remove(@PathVariable Long[] operIds)
{
return toAjax(operLogService.deleteOperLogByIds(operIds));
}
@Log(title = "操作日志", businessType = BusinessType.CLEAN)
@PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
@DeleteMapping("/clean")
public AjaxResult clean()
{
operLogService.cleanOperLog();
return success();
}
}

View File

@ -0,0 +1,83 @@
package com.ruoyi.web.controller.monitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysUserOnline;
import com.ruoyi.system.service.ISysUserOnlineService;
/**
* 在线用户监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/online")
public class SysUserOnlineController extends BaseController
{
@Autowired
private ISysUserOnlineService userOnlineService;
@Autowired
private RedisCache redisCache;
@PreAuthorize("@ss.hasPermi('monitor:online:list')")
@GetMapping("/list")
public TableDataInfo list(String ipaddr, String userName)
{
Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
for (String key : keys)
{
LoginUser user = redisCache.getCacheObject(key);
if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
{
userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
}
else if (StringUtils.isNotEmpty(ipaddr))
{
userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
}
else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
{
userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
}
else
{
userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
}
}
Collections.reverse(userOnlineList);
userOnlineList.removeAll(Collections.singleton(null));
return getDataTable(userOnlineList);
}
/**
* 强退用户
*/
@PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
@Log(title = "在线用户", businessType = BusinessType.FORCE)
@DeleteMapping("/{tokenId}")
public AjaxResult forceLogout(@PathVariable String tokenId)
{
redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
return success();
}
}

View File

@ -0,0 +1,133 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.poi.ExcelUtil;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.service.ISysConfigService;
/**
* 参数配置 信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/config")
public class SysConfigController extends BaseController
{
@Autowired
private ISysConfigService configService;
/**
* 获取参数配置列表
*/
@PreAuthorize("@ss.hasPermi('system:config:list')")
@GetMapping("/list")
public TableDataInfo list(SysConfig config)
{
startPage();
List<SysConfig> list = configService.selectConfigList(config);
return getDataTable(list);
}
@Log(title = "参数管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:config:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysConfig config)
{
List<SysConfig> list = configService.selectConfigList(config);
ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
util.exportExcel(response, list, "参数数据");
}
/**
* 根据参数编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:config:query')")
@GetMapping(value = "/{configId}")
public AjaxResult getInfo(@PathVariable Long configId)
{
return success(configService.selectConfigById(configId));
}
/**
* 根据参数键名查询参数值
*/
@GetMapping(value = "/configKey/{configKey}")
public AjaxResult getConfigKey(@PathVariable String configKey)
{
return success(configService.selectConfigByKey(configKey));
}
/**
* 新增参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:add')")
@Log(title = "参数管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysConfig config)
{
if (!configService.checkConfigKeyUnique(config))
{
return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setCreateBy(getUsername());
return toAjax(configService.insertConfig(config));
}
/**
* 修改参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:edit')")
@Log(title = "参数管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysConfig config)
{
if (!configService.checkConfigKeyUnique(config))
{
return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setUpdateBy(getUsername());
return toAjax(configService.updateConfig(config));
}
/**
* 删除参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:remove')")
@Log(title = "参数管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{configIds}")
public AjaxResult remove(@PathVariable Long[] configIds)
{
configService.deleteConfigByIds(configIds);
return success();
}
/**
* 刷新参数缓存
*/
@PreAuthorize("@ss.hasPermi('system:config:remove')")
@Log(title = "参数管理", businessType = BusinessType.CLEAN)
@DeleteMapping("/refreshCache")
public AjaxResult refreshCache()
{
configService.resetConfigCache();
return success();
}
}

View File

@ -0,0 +1,132 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDeptService;
/**
* 部门信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dept")
public class SysDeptController extends BaseController
{
@Autowired
private ISysDeptService deptService;
/**
* 获取部门列表
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list")
public AjaxResult list(SysDept dept)
{
List<SysDept> depts = deptService.selectDeptList(dept);
return success(depts);
}
/**
* 查询部门列表排除节点
*/
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list/exclude/{deptId}")
public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
{
List<SysDept> depts = deptService.selectDeptList(new SysDept());
depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
return success(depts);
}
/**
* 根据部门编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:dept:query')")
@GetMapping(value = "/{deptId}")
public AjaxResult getInfo(@PathVariable Long deptId)
{
deptService.checkDeptDataScope(deptId);
return success(deptService.selectDeptById(deptId));
}
/**
* 新增部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:add')")
@Log(title = "部门管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDept dept)
{
if (!deptService.checkDeptNameUnique(dept))
{
return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
dept.setCreateBy(getUsername());
return toAjax(deptService.insertDept(dept));
}
/**
* 修改部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:edit')")
@Log(title = "部门管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDept dept)
{
Long deptId = dept.getDeptId();
deptService.checkDeptDataScope(deptId);
if (!deptService.checkDeptNameUnique(dept))
{
return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
else if (dept.getParentId().equals(deptId))
{
return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
}
else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
{
return error("该部门包含未停用的子部门!");
}
dept.setUpdateBy(getUsername());
return toAjax(deptService.updateDept(dept));
}
/**
* 删除部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:remove')")
@Log(title = "部门管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{deptId}")
public AjaxResult remove(@PathVariable Long deptId)
{
if (deptService.hasChildByDeptId(deptId))
{
return warn("存在下级部门,不允许删除");
}
if (deptService.checkDeptExistUser(deptId))
{
return warn("部门存在用户,不允许删除");
}
deptService.checkDeptDataScope(deptId);
return toAjax(deptService.deleteDeptById(deptId));
}
}

View File

@ -0,0 +1,121 @@
package com.ruoyi.web.controller.system;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.domain.entity.SysDictData;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDictDataService;
import com.ruoyi.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/data")
public class SysDictDataController extends BaseController
{
@Autowired
private ISysDictDataService dictDataService;
@Autowired
private ISysDictTypeService dictTypeService;
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictData dictData)
{
startPage();
List<SysDictData> list = dictDataService.selectDictDataList(dictData);
return getDataTable(list);
}
@Log(title = "字典数据", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:dict:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDictData dictData)
{
List<SysDictData> list = dictDataService.selectDictDataList(dictData);
ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
util.exportExcel(response, list, "字典数据");
}
/**
* 查询字典数据详细
*/
@PreAuthorize("@ss.hasPermi('system:dict:query')")
@GetMapping(value = "/{dictCode}")
public AjaxResult getInfo(@PathVariable Long dictCode)
{
return success(dictDataService.selectDictDataById(dictCode));
}
/**
* 根据字典类型查询字典数据信息
*/
@GetMapping(value = "/type/{dictType}")
public AjaxResult dictType(@PathVariable String dictType)
{
List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
if (StringUtils.isNull(data))
{
data = new ArrayList<SysDictData>();
}
return success(data);
}
/**
* 新增字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:add')")
@Log(title = "字典数据", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDictData dict)
{
dict.setCreateBy(getUsername());
return toAjax(dictDataService.insertDictData(dict));
}
/**
* 修改保存字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:edit')")
@Log(title = "字典数据", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictData dict)
{
dict.setUpdateBy(getUsername());
return toAjax(dictDataService.updateDictData(dict));
}
/**
* 删除字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictCodes}")
public AjaxResult remove(@PathVariable Long[] dictCodes)
{
dictDataService.deleteDictDataByIds(dictCodes);
return success();
}
}

View File

@ -0,0 +1,131 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.domain.entity.SysDictType;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/type")
public class SysDictTypeController extends BaseController
{
@Autowired
private ISysDictTypeService dictTypeService;
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
public TableDataInfo list(SysDictType dictType)
{
startPage();
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
return getDataTable(list);
}
@Log(title = "字典类型", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:dict:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDictType dictType)
{
List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
util.exportExcel(response, list, "字典类型");
}
/**
* 查询字典类型详细
*/
@PreAuthorize("@ss.hasPermi('system:dict:query')")
@GetMapping(value = "/{dictId}")
public AjaxResult getInfo(@PathVariable Long dictId)
{
return success(dictTypeService.selectDictTypeById(dictId));
}
/**
* 新增字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:add')")
@Log(title = "字典类型", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDictType dict)
{
if (!dictTypeService.checkDictTypeUnique(dict))
{
return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
}
dict.setCreateBy(getUsername());
return toAjax(dictTypeService.insertDictType(dict));
}
/**
* 修改字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:edit')")
@Log(title = "字典类型", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictType dict)
{
if (!dictTypeService.checkDictTypeUnique(dict))
{
return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
}
dict.setUpdateBy(getUsername());
return toAjax(dictTypeService.updateDictType(dict));
}
/**
* 删除字典类型
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictIds}")
public AjaxResult remove(@PathVariable Long[] dictIds)
{
dictTypeService.deleteDictTypeByIds(dictIds);
return success();
}
/**
* 刷新字典缓存
*/
@PreAuthorize("@ss.hasPermi('system:dict:remove')")
@Log(title = "字典类型", businessType = BusinessType.CLEAN)
@DeleteMapping("/refreshCache")
public AjaxResult refreshCache()
{
dictTypeService.resetDictCache();
return success();
}
/**
* 获取字典选择框列表
*/
@GetMapping("/optionselect")
public AjaxResult optionselect()
{
List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
return success(dictTypes);
}
}

View File

@ -0,0 +1,29 @@
package com.ruoyi.web.controller.system;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.StringUtils;
/**
* 首页
*
* @author ruoyi
*/
@RestController
public class SysIndexController
{
/** 系统基础配置 */
@Autowired
private RuoYiConfig ruoyiConfig;
/**
* 访问首页提示语
*/
@RequestMapping("/")
public String index()
{
return StringUtils.format("欢迎使用{}后台管理框架当前版本v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
}
}

View File

@ -0,0 +1,131 @@
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.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysLoginService;
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;
/**
* 登录验证
*
* @author ruoyi
*/
@RestController
public class SysLoginController
{
@Autowired
private SysLoginService loginService;
@Autowired
private ISysMenuService menuService;
@Autowired
private SysPermissionService permissionService;
@Autowired
private TokenService tokenService;
@Autowired
private ISysConfigService configService;
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
LoginUser loginUser = SecurityUtils.getLoginUser();
SysUser user = loginUser.getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
if (!loginUser.getPermissions().equals(permissions))
{
loginUser.setPermissions(permissions);
tokenService.refreshToken(loginUser);
}
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
return ajax;
}
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
// 检查初始密码是否提醒修改
public boolean initPasswordIsModify(Date pwdUpdateDate)
{
Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify"));
return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null;
}
// 检查密码是否过期
public boolean passwordIsExpiration(Date pwdUpdateDate)
{
Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays"));
if (passwordValidateDays != null && passwordValidateDays > 0)
{
if (StringUtils.isNull(pwdUpdateDate))
{
// 如果从未修改过初始密码直接提醒过期
return true;
}
Date nowDate = DateUtils.getNowDate();
return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays;
}
return false;
}
}

View File

@ -0,0 +1,150 @@
package com.ruoyi.web.controller.system;
import java.util.List;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysMenuService;
/**
* 菜单信息
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/menu")
public class SysMenuController extends BaseController
{
@Autowired
private ISysMenuService menuService;
/**
* 获取菜单列表
*/
@PreAuthorize("@ss.hasPermi('system:menu:list')")
@GetMapping("/list")
public AjaxResult list(SysMenu menu)
{
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
return success(menus);
}
/**
* 根据菜单编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:menu:query')")
@GetMapping(value = "/{menuId}")
public AjaxResult getInfo(@PathVariable Long menuId)
{
return success(menuService.selectMenuById(menuId));
}
/**
* 获取菜单下拉树列表
*/
@GetMapping("/treeselect")
public AjaxResult treeselect(SysMenu menu)
{
List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
return success(menuService.buildMenuTreeSelect(menus));
}
/**
* 加载对应角色菜单列表树
*/
@GetMapping(value = "/roleMenuTreeselect/{roleId}")
public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId)
{
List<SysMenu> menus = menuService.selectMenuList(getUserId());
AjaxResult ajax = AjaxResult.success();
ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
ajax.put("menus", menuService.buildMenuTreeSelect(menus));
return ajax;
}
/**
* 新增菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:add')")
@Log(title = "菜单管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysMenu menu)
{
if (!menuService.checkMenuNameUnique(menu))
{
return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
}
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
{
return error("新增菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
}
else if (!menuService.checkRouteConfigUnique(menu))
{
return error("新增菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
}
menu.setCreateBy(getUsername());
return toAjax(menuService.insertMenu(menu));
}
/**
* 修改菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:edit')")
@Log(title = "菜单管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysMenu menu)
{
if (!menuService.checkMenuNameUnique(menu))
{
return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
}
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
{
return error("修改菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
}
else if (menu.getMenuId().equals(menu.getParentId()))
{
return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
}
else if (!menuService.checkRouteConfigUnique(menu))
{
return error("修改菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
}
menu.setUpdateBy(getUsername());
return toAjax(menuService.updateMenu(menu));
}
/**
* 删除菜单
*/
@PreAuthorize("@ss.hasPermi('system:menu:remove')")
@Log(title = "菜单管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{menuId}")
public AjaxResult remove(@PathVariable("menuId") Long menuId)
{
if (menuService.hasChildByMenuId(menuId))
{
return warn("存在子菜单,不允许删除");
}
if (menuService.checkMenuExistRole(menuId))
{
return warn("菜单已分配,不允许删除");
}
return toAjax(menuService.deleteMenuById(menuId));
}
}

View File

@ -0,0 +1,91 @@
package com.ruoyi.web.controller.system;
import java.util.List;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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.system.domain.SysNotice;
import com.ruoyi.system.service.ISysNoticeService;
/**
* 公告 信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/system/notice")
public class SysNoticeController extends BaseController
{
@Autowired
private ISysNoticeService noticeService;
/**
* 获取通知公告列表
*/
@PreAuthorize("@ss.hasPermi('system:notice:list')")
@GetMapping("/list")
public TableDataInfo list(SysNotice notice)
{
startPage();
List<SysNotice> list = noticeService.selectNoticeList(notice);
return getDataTable(list);
}
/**
* 根据通知公告编号获取详细信息
*/
@PreAuthorize("@ss.hasPermi('system:notice:query')")
@GetMapping(value = "/{noticeId}")
public AjaxResult getInfo(@PathVariable Long noticeId)
{
return success(noticeService.selectNoticeById(noticeId));
}
/**
* 新增通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:add')")
@Log(title = "通知公告", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysNotice notice)
{
notice.setCreateBy(getUsername());
return toAjax(noticeService.insertNotice(notice));
}
/**
* 修改通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:edit')")
@Log(title = "通知公告", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysNotice notice)
{
notice.setUpdateBy(getUsername());
return toAjax(noticeService.updateNotice(notice));
}
/**
* 删除通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:remove')")
@Log(title = "通知公告", businessType = BusinessType.DELETE)
@DeleteMapping("/{noticeIds}")
public AjaxResult remove(@PathVariable Long[] noticeIds)
{
return toAjax(noticeService.deleteNoticeByIds(noticeIds));
}
}

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