增加企业信息 提交企业信息修改 增加多公司支持 运行无错初版

This commit is contained in:
MerCry 2026-02-08 23:38:56 +08:00
parent a4a8b468cd
commit c619c28895
38 changed files with 448 additions and 2713 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,79 +0,0 @@
version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:5.7
container_name: ruoyi-mysql
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: ruoyi
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./sql:/docker-entrypoint-initdb.d
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
networks:
- ruoyi-network
restart: unless-stopped
# Redis 缓存
redis:
image: redis:6-alpine
container_name: ruoyi-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- ruoyi-network
restart: unless-stopped
# 后端服务
backend:
build:
context: .
dockerfile: Dockerfile.backend
container_name: ruoyi-backend
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ruoyi?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: password
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
SERVER_SERVLET_CONTEXT_PATH: /
ports:
- "8080:8080"
volumes:
- upload-data:/home/ruoyi/uploadPath
depends_on:
- mysql
- redis
networks:
- ruoyi-network
restart: unless-stopped
# 前端 Nginx
frontend:
build:
context: ./ruoyi-ui
dockerfile: Dockerfile
container_name: ruoyi-frontend
ports:
- "8081:80"
depends_on:
- backend
networks:
- ruoyi-network
restart: unless-stopped
networks:
ruoyi-network:
driver: bridge
volumes:
mysql-data:
redis-data:
upload-data:

View File

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

View File

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

View File

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

View File

@ -116,8 +116,6 @@ import java.util.concurrent.atomic.AtomicInteger;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} finally { } finally {
// 清理主线程的上下文
CorpContextHolder.clear();
} }
}); });

View File

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

View File

@ -5,7 +5,6 @@ import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.excel.wecom.domain.CorpInfo; import com.ruoyi.excel.wecom.domain.CorpInfo;
import com.ruoyi.excel.wecom.model.WecomConfig;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
@ -22,17 +21,11 @@ import java.util.concurrent.TimeUnit;
* 企业微信基础服务类 * 企业微信基础服务类
*/ */
public class WecomBaseService { public class WecomBaseService {
private WecomConfig wecomConfig;
/** /**
* Redis key 前缀wecom_access_token:{corpId} * Redis key 前缀wecom_access_token:{corpId}
*/ */
private static final String ACCESS_TOKEN_KEY_PREFIX = "wecom_access_token:"; private static final String ACCESS_TOKEN_KEY_PREFIX = "wecom_access_token:";
public WecomBaseService(WecomConfig wecomConfig) {
this.wecomConfig = wecomConfig;
}
/** /**
* 获取accessToken根据当前企业上下文动态获取 * 获取accessToken根据当前企业上下文动态获取
* *
@ -116,11 +109,5 @@ public class WecomBaseService {
} }
} }
public WecomConfig getWecomConfig() {
return wecomConfig;
}
public void setWecomConfig(WecomConfig wecomConfig) {
this.wecomConfig = wecomConfig;
}
} }

View File

@ -5,7 +5,6 @@ import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.excel.wecom.domain.CorpDepartment; import com.ruoyi.excel.wecom.domain.CorpDepartment;
import com.ruoyi.excel.wecom.domain.CorpUser; 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.WecomCustomer;
import com.ruoyi.excel.wecom.model.WecomTagGroup; import com.ruoyi.excel.wecom.model.WecomTagGroup;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -20,11 +19,6 @@ import java.util.List;
*/ */
@Component @Component
public class WecomContactService extends WecomBaseService { public class WecomContactService extends WecomBaseService {
public WecomContactService(WecomConfig wecomConfig) {
super(wecomConfig);
}
/** /**
* 获取配置了客户联系功能的成员列表 * 获取配置了客户联系功能的成员列表
* *

View File

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

View File

@ -6,23 +6,20 @@ import com.alibaba.excel.write.metadata.WriteSheet;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.CorpContextHolder; import com.ruoyi.common.utils.CorpContextHolder;
import com.ruoyi.excel.wecom.domain.CorpDepartment;
import com.ruoyi.excel.wecom.domain.CorpUser;
import com.ruoyi.excel.wecom.helper.HandleAllData; import com.ruoyi.excel.wecom.helper.HandleAllData;
import com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper; import com.ruoyi.excel.wecom.mapper.CustomerExportDataMapper;
import com.ruoyi.excel.wecom.mapper.CustomerStatisticsDataMapper; import com.ruoyi.excel.wecom.mapper.CustomerStatisticsDataMapper;
import com.ruoyi.excel.wecom.mapper.DepartmentStatisticsDataMapper; import com.ruoyi.excel.wecom.mapper.DepartmentStatisticsDataMapper;
import com.ruoyi.excel.wecom.model.WecomConfig;
import com.ruoyi.excel.wecom.model.WecomCustomer;
import com.ruoyi.excel.wecom.model.WecomTagGroup;
import com.ruoyi.excel.wecom.service.WecomContactService;
import com.ruoyi.excel.wecom.service.WecomTagService; import com.ruoyi.excel.wecom.service.WecomTagService;
import com.ruoyi.excel.wecom.vo.CustomerExportDataVO; import com.ruoyi.excel.wecom.vo.CustomerExportDataVO;
import com.ruoyi.excel.wecom.vo.CustomerStatisticsDataVO; import com.ruoyi.excel.wecom.vo.CustomerStatisticsDataVO;
import com.ruoyi.excel.wecom.vo.DepartmentStatisticsDataVO; import com.ruoyi.excel.wecom.vo.DepartmentStatisticsDataVO;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -55,244 +52,10 @@ public class WecomContactController {
@Autowired @Autowired
private RedisCache redisCache; private RedisCache redisCache;
/**
* 获取配置了客户联系功能的成员列表
*
* @param request HttpServletRequest
* @return AjaxResult
*/
@GetMapping("/followUserList")
public AjaxResult getFollowUserList(HttpServletRequest request) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<String> userList = contactService.getFollowUserList(currentCorpId);
return AjaxResult.success("获取成功", userList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取指定成员的客户列表
*
* @param request HttpServletRequest
* @param userid 成员ID
* @return AjaxResult
*/
@GetMapping("/externalContactList")
public AjaxResult getExternalContactList(HttpServletRequest request, @RequestParam String userid) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<String> customerList = contactService.getExternalContactList(currentCorpId,userid);
return AjaxResult.success("获取成功", customerList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取所有成员的客户列表
*
* @param request HttpServletRequest
* @return AjaxResult
*/
@GetMapping("/allMembersCustomers")
public AjaxResult getAllMembersCustomers(HttpServletRequest request) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<WecomContactService.MemberCustomer> memberCustomerList = contactService.getAllMembersCustomers(currentCorpId);
return AjaxResult.success("获取成功", memberCustomerList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 批量获取客户详情
*
* @param request HttpServletRequest
* @return AjaxResult
*/
@PostMapping("/batchCustomerDetails")
public AjaxResult batchGetCustomerDetails(HttpServletRequest request, @RequestBody List<String> useridList) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
// 解析成员ID列表
List<WecomCustomer> customerList = contactService.batchGetAllCustomerDetails(currentCorpId,useridList);
return AjaxResult.success("获取成功", customerList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取单个客户详情
*
* @param request HttpServletRequest
* @param externalUserid 外部联系人ID
* @return AjaxResult
*/
@GetMapping("/customerDetail")
public AjaxResult getCustomerDetail(HttpServletRequest request, @RequestParam String externalUserid) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
WecomCustomer customer = contactService.getCustomerDetail(currentCorpId, externalUserid);
return AjaxResult.success("获取成功", customer);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取企业标签库
*
* @param request HttpServletRequest
* @return AjaxResult
*/
@GetMapping("/corpTagList")
public AjaxResult getCorpTagList(HttpServletRequest request) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<WecomTagGroup> tagGroupList = contactService.getCorpTagList(currentCorpId);
// 同步标签库到数据库
boolean syncResult = wecomTagService.syncCorpTagList(tagGroupList);
if (syncResult) {
return AjaxResult.success("获取成功并同步到数据库", tagGroupList);
} else {
return AjaxResult.success("获取成功但同步到数据库失败", tagGroupList);
}
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取企业微信配置
*
* @param request HttpServletRequest
* @return WecomConfig
*/
private WecomConfig getWecomConfig(HttpServletRequest request) {
// 这里可以从请求参数配置文件或数据库中获取配置
// 暂时使用硬编码的方式实际使用时应该从配置中获取
WecomConfig config = new WecomConfig();
return config;
}
/**
* 获取子部门id列表
*
* @param request HttpServletRequest
* @param departmentId 部门ID根部门传1
* @return AjaxResult
*/
@GetMapping("/departmentList")
public AjaxResult getDepartmentList(HttpServletRequest request, @RequestParam(required = false) Long departmentId) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<CorpDepartment> deptIdList = contactService.getDepartmentList(currentCorpId,departmentId);
return AjaxResult.success("获取成功", deptIdList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取单个部门详情
*
* @param request HttpServletRequest
* @param departmentId 部门ID
* @return AjaxResult
*/
@GetMapping("/departmentDetail")
public AjaxResult getDepartmentDetail(HttpServletRequest request, @RequestParam Long departmentId) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
Object deptDetail = contactService.getDepartmentDetail(currentCorpId,departmentId);
return AjaxResult.success("获取成功", deptDetail);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取部门成员详情
*
* @param request HttpServletRequest
* @param departmentId 部门ID
* @param fetchChild 是否递归获取子部门成员
* @param status 成员状态0-全部1-已激活2-已禁用4-未激活5-退出企业
* @return AjaxResult
*/
@GetMapping("/departmentMemberList")
public AjaxResult getDepartmentMemberList(HttpServletRequest request, @RequestParam Long departmentId,
@RequestParam(required = false) Integer fetchChild,
@RequestParam(required = false) Integer status) {
try {
String currentCorpId = CorpContextHolder.getCurrentCorpId();
WecomConfig config = getWecomConfig(request);
WecomContactService contactService = new WecomContactService(config);
List<CorpUser> memberList = contactService.getDepartmentMemberList(currentCorpId,departmentId, fetchChild, status);
return AjaxResult.success("获取成功", memberList);
} catch (IOException e) {
return AjaxResult.error("获取失败: " + e.getMessage());
} catch (Exception e) {
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
@GetMapping("/init") @GetMapping("/init")
public AjaxResult init(HttpServletRequest request) throws IOException { public AjaxResult init(HttpServletRequest request) throws IOException {
// Redis 获取当前企业ID并设置到上下文
String corpId = redisCache.getCacheObject("current_corp_id");
if (corpId == null || corpId.trim().isEmpty()) {
return AjaxResult.error("未找到当前企业ID请先切换企业");
}
CorpContextHolder.setCurrentCorpId(corpId);
try { try {
handleAllData.initData(); handleAllData.initData();
return AjaxResult.success(); return AjaxResult.success();
@ -303,13 +66,6 @@ public class WecomContactController {
@GetMapping("/test") @GetMapping("/test")
public AjaxResult test(HttpServletRequest request) throws IOException { public AjaxResult test(HttpServletRequest request) throws IOException {
// Redis 获取当前企业ID并设置到上下文
String corpId = redisCache.getCacheObject("current_corp_id");
if (corpId == null || corpId.trim().isEmpty()) {
return AjaxResult.error("未找到当前企业ID请先切换企业");
}
CorpContextHolder.setCurrentCorpId(corpId);
try { try {
handleAllData.handleAllData(); handleAllData.handleAllData();
return AjaxResult.success(); return AjaxResult.success();
@ -320,13 +76,6 @@ public class WecomContactController {
@GetMapping("/createAllReportData") @GetMapping("/createAllReportData")
public AjaxResult createAllReportData(HttpServletRequest request) throws IOException { public AjaxResult createAllReportData(HttpServletRequest request) throws IOException {
// Redis 获取当前企业ID并设置到上下文
String corpId = redisCache.getCacheObject("current_corp_id");
if (corpId == null || corpId.trim().isEmpty()) {
return AjaxResult.error("未找到当前企业ID请先切换企业");
}
CorpContextHolder.setCurrentCorpId(corpId);
try { try {
handleAllData.createAllReportData(); handleAllData.createAllReportData();
return AjaxResult.success(); return AjaxResult.success();
@ -337,13 +86,6 @@ public class WecomContactController {
@GetMapping("/createAllDepartmentReportData") @GetMapping("/createAllDepartmentReportData")
public AjaxResult createAllDepartmentReportData(HttpServletRequest request) throws IOException { public AjaxResult createAllDepartmentReportData(HttpServletRequest request) throws IOException {
// Redis 获取当前企业ID并设置到上下文
String corpId = redisCache.getCacheObject("current_corp_id");
if (corpId == null || corpId.trim().isEmpty()) {
return AjaxResult.error("未找到当前企业ID请先切换企业");
}
CorpContextHolder.setCurrentCorpId(corpId);
try { try {
handleAllData.createAllDepartmentReportData(); handleAllData.createAllDepartmentReportData();
return AjaxResult.success(); return AjaxResult.success();
@ -354,14 +96,8 @@ public class WecomContactController {
@GetMapping("/createAllDepartmentReportByDate") @GetMapping("/createAllDepartmentReportByDate")
public AjaxResult createAllDepartmentReportByDate(@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date date) throws IOException { public AjaxResult createAllDepartmentReportByDate(@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date date) throws IOException {
// Redis 获取当前企业ID并设置到上下文
String corpId = redisCache.getCacheObject("current_corp_id");
if (corpId == null || corpId.trim().isEmpty()) {
return AjaxResult.error("未找到当前企业ID请先切换企业");
}
CorpContextHolder.setCurrentCorpId(corpId);
try { try {
String corpId = CorpContextHolder.getCurrentCorpId();
handleAllData.createDepartmentReportData(corpId,date); handleAllData.createDepartmentReportData(corpId,date);
return AjaxResult.success(); return AjaxResult.success();
} finally { } finally {
@ -382,12 +118,7 @@ public class WecomContactController {
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate, @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) { @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
try { try {
// Redis 获取当前企业ID并设置到上下文 String corpId = CorpContextHolder.getCurrentCorpId();
String corpId = redisCache.getCacheObject("current_corp_id");
if (corpId == null || corpId.trim().isEmpty()) {
throw new RuntimeException("未找到当前企业ID请先切换企业");
}
CorpContextHolder.setCurrentCorpId(corpId);
// 设置响应头 // 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@ -433,12 +164,7 @@ public class WecomContactController {
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate, @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(required = false) String indicatorName) { @RequestParam(required = false) String indicatorName) {
try { try {
// Redis 获取当前企业ID并设置到上下文 String corpId = CorpContextHolder.getCurrentCorpId();
String corpId = redisCache.getCacheObject("current_corp_id");
if (corpId == null || corpId.trim().isEmpty()) {
throw new RuntimeException("未找到当前企业ID请先切换企业");
}
CorpContextHolder.setCurrentCorpId(corpId);
// 设置响应头 // 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@ -485,12 +211,7 @@ public class WecomContactController {
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate, @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(required = false) String departmentPath) { @RequestParam(required = false) String departmentPath) {
try { try {
// Redis 获取当前企业ID并设置到上下文 String corpId = CorpContextHolder.getCurrentCorpId();
String corpId = redisCache.getCacheObject("current_corp_id");
if (corpId == null || corpId.trim().isEmpty()) {
throw new RuntimeException("未找到当前企业ID请先切换企业");
}
CorpContextHolder.setCurrentCorpId(corpId);
// 设置响应头 // 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

View File

@ -6,7 +6,7 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://host.docker.internal:3316/excel-handle?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://localhost:3306/excel-handle?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root username: root
password: jiong1114 password: jiong1114
# 从库数据源 # 从库数据源

View File

@ -69,7 +69,7 @@ spring:
# redis 配置 # redis 配置
redis: redis:
# 地址 # 地址
host: host.docker.internal host: localhost
# 端口默认为6379 # 端口默认为6379
port: 6379 port: 6379
# 数据库索引 # 数据库索引

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,6 @@ package com.ruoyi.framework.config;
import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.Constants;
import com.ruoyi.framework.interceptor.CorpContextInterceptor;
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -28,8 +27,6 @@ public class ResourcesConfig implements WebMvcConfigurer
@Autowired @Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor; private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Autowired
private CorpContextInterceptor corpContextInterceptor;
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) public void addResourceHandlers(ResourceHandlerRegistry registry)
@ -50,8 +47,6 @@ public class ResourcesConfig implements WebMvcConfigurer
@Override @Override
public void addInterceptors(InterceptorRegistry registry) public void addInterceptors(InterceptorRegistry registry)
{ {
// 企业上下文拦截器用于设置当前企业ID到ThreadLocal
registry.addInterceptor(corpContextInterceptor).addPathPatterns("/**");
// 防重复提交拦截器 // 防重复提交拦截器
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
} }

View File

@ -1,41 +0,0 @@
package com.ruoyi.framework.interceptor;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.CorpContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 企业上下文拦截器
* 在请求处理前从 Redis 获取当前企业ID并设置到 ThreadLocal
* 在请求处理后清理 ThreadLocal
*/
@Component
public class CorpContextInterceptor implements HandlerInterceptor {
@Autowired
private RedisCache redisCache;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Redis 获取当前企业ID
String corpId = redisCache.getCacheObject("current_corp_id");
// 如果存在企业ID设置到 ThreadLocal
if (corpId != null && !corpId.trim().isEmpty()) {
CorpContextHolder.setCurrentCorpId(corpId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成后清理 ThreadLocal避免内存泄漏
CorpContextHolder.clear();
}
}

View File

@ -20,7 +20,7 @@ RUN npm run build:prod
FROM nginx:alpine FROM nginx:alpine
# 复制构建好的文件到 Nginx # 复制构建好的文件到 Nginx
COPY --from=build /app/dist /usr/share/nginx/html/ashai-wecom-test COPY --from=build /app/dist /usr/share/nginx/html
# 复制 Nginx 配置文件 # 复制 Nginx 配置文件
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf

View File

@ -3,14 +3,14 @@ server {
server_name localhost; server_name localhost;
# 前端静态文件路径 # 前端静态文件路径
location /ashai-wecom-test { location {
alias /usr/share/nginx/html/ashai-wecom-test; alias /usr/share/nginx/html;
try_files $uri $uri/ /ashai-wecom-test/index.html; try_files $uri $uri/ /index.html;
index index.html; index index.html;
} }
# API 代理到后端 # API 代理到后端
location /ashai-wecom-test/prod-api/ { location /prod-api/ {
proxy_pass http://backend:8080/; proxy_pass http://backend:8080/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;

View File

@ -0,0 +1,68 @@
import request from '@/utils/request'
// 查询企业信息列表
export function listCorp(query) {
return request({
url: '/wecom/corp/list',
method: 'get',
params: query
})
}
// 查询企业信息详细
export function getCorp(id) {
return request({
url: '/wecom/corp/' + id,
method: 'get'
})
}
// 新增企业信息
export function addCorp(data) {
return request({
url: '/wecom/corp',
method: 'post',
data: data
})
}
// 修改企业信息
export function updateCorp(data) {
return request({
url: '/wecom/corp',
method: 'put',
data: data
})
}
// 删除企业信息
export function delCorp(ids) {
return request({
url: '/wecom/corp/' + ids,
method: 'delete'
})
}
// 切换当前使用的企业
export function switchCorp(corpId) {
return request({
url: '/wecom/corp/switch/' + corpId,
method: 'post'
})
}
// 获取当前使用的企业信息
export function getCurrentCorp() {
return request({
url: '/wecom/corp/current',
method: 'get'
})
}
// 清除当前企业上下文
export function clearCurrentCorp() {
return request({
url: '/wecom/corp/clear',
method: 'post'
})
}

View File

@ -105,7 +105,7 @@ export default {
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$store.dispatch('LogOut').then(() => { this.$store.dispatch('LogOut').then(() => {
const baseUrl = process.env.NODE_ENV === 'production' ? '/ashai-wecom-test' : '' const baseUrl = process.env.NODE_ENV === 'production' ? '' : ''
location.href = baseUrl + '/index' location.href = baseUrl + '/index'
}) })
}).catch(() => {}) }).catch(() => {})

View File

@ -178,7 +178,7 @@ Router.prototype.replace = function push(location) {
export default new Router({ export default new Router({
mode: 'history', // 去掉url中的# mode: 'history', // 去掉url中的#
base: process.env.NODE_ENV === 'production' ? '/ashai-wecom-test/' : '/', base: process.env.NODE_ENV === 'production' ? '/' : '/',
scrollBehavior: () => ({ y: 0 }), scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes routes: constantRoutes
}) })

View File

@ -88,7 +88,7 @@ service.interceptors.response.use(res => {
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false isRelogin.show = false
store.dispatch('LogOut').then(() => { store.dispatch('LogOut').then(() => {
const baseUrl = process.env.NODE_ENV === 'production' ? '/ashai-wecom-test' : '' const baseUrl = process.env.NODE_ENV === 'production' ? '' : ''
location.href = baseUrl + '/index' location.href = baseUrl + '/index'
}) })
}).catch(() => { }).catch(() => {

View File

@ -0,0 +1,275 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="企业ID" prop="corpId">
<el-input
v-model="queryParams.corpId"
placeholder="请输入企业ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="企业名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入企业名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['wecom:corp:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['wecom:corp:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['wecom:corp:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="corpList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="ID" align="center" prop="id" width="80" />
<el-table-column label="企业ID" align="center" prop="corpId" width="200" :show-overflow-tooltip="true" />
<el-table-column label="企业名称" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="Secret" align="center" prop="secret" width="300" :show-overflow-tooltip="true" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleSwitch(scope.row)"
v-hasPermi="['wecom:corp:edit']"
>切换</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['wecom:corp:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['wecom:corp:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改企业信息对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="企业ID" prop="corpId">
<el-input v-model="form.corpId" placeholder="请输入企业ID" />
</el-form-item>
<el-form-item label="企业名称" prop="name">
<el-input v-model="form.name" placeholder="请输入企业名称" />
</el-form-item>
<el-form-item label="Secret" prop="secret">
<el-input v-model="form.secret" type="textarea" placeholder="请输入Secret" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {listCorp, getCorp, addCorp, updateCorp, delCorp, switchCorp} from "@/api/wecom/corpInfo"
export default {
name: "CorpInfo",
data() {
return {
//
loading: true,
//
ids: [],
//
single: true,
//
multiple: true,
//
showSearch: true,
//
total: 0,
//
corpList: [],
//
title: "",
//
open: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
corpId: undefined,
name: undefined
},
//
form: {},
//
rules: {
corpId: [
{ required: true, message: "企业ID不能为空", trigger: "blur" }
],
name: [
{ required: true, message: "企业名称不能为空", trigger: "blur" }
],
secret: [
{ required: true, message: "Secret不能为空", trigger: "blur" }
]
}
}
},
created() {
this.getList()
},
methods: {
/** 查询企业信息列表 */
getList() {
this.loading = true
listCorp(this.queryParams).then(response => {
this.corpList = response.rows
this.total = response.total
this.loading = false
})
},
//
cancel() {
this.open = false
this.reset()
},
//
reset() {
this.form = {
id: undefined,
corpId: undefined,
secret: undefined,
name: undefined
}
this.resetForm("form")
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = "添加企业信息"
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id = row.id || this.ids
getCorp(id).then(response => {
this.form = response.data
this.open = true
this.title = "修改企业信息"
})
},
handleSwitch(row) {
const ids = row.id || this.ids
this.$modal.confirm('是否确认切换当前企业为"' + row.name + '"').then(function() {
return switchCorp(row.corpId)
}).then(() => {
this.getList()
this.$modal.msgSuccess("切换成功")
}).catch(() => {})
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != undefined) {
updateCorp(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addCorp(this.form).then(response => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids
this.$modal.confirm('是否确认删除企业信息编号为"' + ids + '"的数据项?').then(function() {
return delCorp(ids)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
}
}
</script>

View File

@ -9,7 +9,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '超脑智子测试系统' // 网页标题 const name = process.env.VUE_APP_TITLE || '超脑智子测试系统' // 网页标题
const baseUrl = 'http://localhost:8080' // 后端接口 const baseUrl = 'http://localhost:8888' // 后端接口
const port = process.env.port || process.env.npm_config_port || 80 // 端口 const port = process.env.port || process.env.npm_config_port || 80 // 端口
@ -20,7 +20,7 @@ module.exports = {
// 部署生产环境和开发环境下的URL。 // 部署生产环境和开发环境下的URL。
// 默认情况下Vue CLI 会假设你的应用是被部署在一个域名的根路径上 // 默认情况下Vue CLI 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。 // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
publicPath: process.env.NODE_ENV === "production" ? "/ashai-wecom-test" : "/", publicPath: process.env.NODE_ENV === "production" ? "" : "/",
// 在npm run build 或 yarn build 时 生成文件的目录名称要和baseUrl的生产环境路径一致默认dist // 在npm run build 或 yarn build 时 生成文件的目录名称要和baseUrl的生产环境路径一致默认dist
outputDir: 'dist', outputDir: 'dist',
// 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下) // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)