Compare commits

..

1 Commits

Author SHA1 Message Date
fedf789511 0302-海宁管户报表优化+客群代码初稿 2026-03-02 17:00:27 +08:00
44 changed files with 2109 additions and 1016 deletions

216
CLAUDE.md
View File

@@ -1,216 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
**数字支行辅助管理系统(IBS)** - 基于 若依框架 v3.8.8 的前后端分离全栈项目,专注于银行支行的网格化营销、客户管理和走访业务。
## 常用命令
### 后端开发
```bash
# Maven 打包(跳过测试)
mvn clean package -Dmaven.test.skip=true
# 运行已打包的 JAR
cd ruoyi-admin/target
java -jar -Xms256m -Xmx1024m ruoyi-admin.jar
# 后端服务地址
http://localhost:8080
# Swagger API 文档
http://localhost:8080/swagger-ui/index.html
# 测试登录接口获取 token
POST /login/test?username=admin&password=admin123
```
### 前端开发
```bash
cd ruoyi-ui
# 安装依赖
npm install
# 开发环境运行(端口 80,代理到 localhost:8080)
npm run dev
# 生产环境构建
npm run build:prod
# 代码检查
npm run lint
```
### 数据库连接
```bash
# 通过 MCP MySQL 工具连接
# 地址: 116.62.17.81:3306
# 数据库: ibs
# 用户名: root
```
## 核心架构
### 后端模块结构
```
ruoyi-admin/ # 主入口模块,包含启动类和配置文件
ruoyi-framework/ # 框架核心:安全配置、缓存、数据源等
ruoyi-system/ # 系统管理:用户、角色、菜单、字典等
ruoyi-common/ # 通用工具:工具类、注解、常量等
ruoyi-quartz/ # 定时任务模块
ruoyi-generator/ # 代码生成器
ibs/ # ★ 业务模块:数字支行核心业务 ★
```
### IBS 业务模块 (核心业务)
位置: `ibs/src/main/java/com/ruoyi/ibs/`
**主要业务包:**
| 包名 | 功能 | 说明 |
|------|------|------|
| `grid` | 网格管理 | 支行网格划分、分配、统计 |
| `cmpm` | 客户经理管理 | 客户经理信息维护 |
| `list` | 客户列表管理 | 零售/商户/企业客户管理 |
| `visit` | 走访管理 | 走访任务、记录、轨迹 |
| `task` | 任务管理 | 营销任务分配和跟踪 |
| `draw` | 绘图/网格绘制 | 基于百度地图的网格绘制 |
| `custmap` | 客户地图 | 客户地理分布可视化 |
| `dashboard` | 仪表盘 | 数据统计和展示 |
| `datavisual` | 数据可视化 | 报表和图表 |
| `rules` | 规则配置 | 业务规则配置 |
| `qxhy` | 青县惠银接口 | 外部系统对接 |
| `websocket` | WebSocket通信 | 实时通信支持 |
**业务模块命名规范:**
- 新建模块命名: `ibs` + 主要功能(如 `ibs-grid`, `ibs-customer`)
- Controller 放在新建模块中,不与若依框架混合
- Entity 使用 `@Data` 注解
- Service 使用 `@Resource` 注解,不继承 `ServiceImpl`
- DAO 使用 MyBatis Plus,复杂操作在 XML 中编写 SQL
### 前端结构
```
ruoyi-ui/src/
├── api/ # API 接口定义
├── views/ # 页面视图
│ ├── grid/ # 网格管理相关页面
│ ├── customer/ # 客户管理
│ ├── taskManage/ # 任务管理
│ ├── dashboard/ # 仪表盘
│ └── ...
├── components/ # 公共组件
├── map/ # 地图相关(百度地图集成)
├── store/ # Vuex 状态管理
└── router/ # 路由配置
```
### 数据库表命名规范
- 新建表需加项目前缀: `ibs_` + 表名
- 示例: `ibs_grid`, `ibs_customer`, `ibs_visit_record`
## 关键技术点
### 1. 地图集成
项目深度集成百度地图 API,用于:
- 网格绘制和编辑
- 客户地理位置标注
- 走访轨迹记录
- 客户分布热力图
**相关配置:**
- 百度地图 AK 在前端配置
- 使用 JTS 库进行地理空间计算
### 2. 批量导入优化
设计批量导入功能时:
- 使用批量操作提高响应速度
- 导入结果只展示失败数据,不展示成功数据
- 使用 EasyExcel 处理 Excel
### 3. 多端支持
- PC 端: 主要管理和配置功能
- PAD 端: 走访记录功能(移动端)
### 4. 外部系统对接
| 系统 | 地址 | 用途 |
|------|------|------|
| 青县惠银 | http://158.234.96.76:5002 | 业务数据对接 |
| BI 系统 | http://158.220.52.42:9388/bi | 数据分析和报表 |
| 阿里云 OSS | - | 文件存储 |
## 开发规范
### 代码分层
- **Entity**: 实体类,不继承 BaseEntity,单独添加审计字段
- **DTO**: 接口传参专用类
- **VO**: 返回数据专用类
- **Service**: 业务逻辑,使用 `@Resource` 注入
- **Mapper**: MyBatis Plus + XML 混合使用
### 审计字段
通过注解实现自动填充:
```java
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
```
### 分页
使用 MyBatis Plus 分页插件自动处理,传入 `Page` 对象即可。
### 前端菜单配置
添加新页面后,需要在数据库 `sys_menu` 表中配置菜单权限。
## 重要配置文件
| 文件 | 位置 | 说明 |
|------|------|------|
| 后端配置 | `ruoyi-admin/src/main/resources/application-dev.yml` | 数据库、Redis、第三方服务配置 |
| 前端配置 | `ruoyi-ui/vue.config.js` | 开发服务器、代理配置 |
| 菜单数据 | 数据库 `sys_menu` 表 | 菜单权限配置 |
## 测试账号
- 用户名: `admin`
- 密码: `admin123`
## Swagger 接口文档
启动后端后访问: `http://localhost:8080/swagger-ui/index.html`
## 开发流程建议
1. **数据库设计**: 新建表需加 `ibs_` 前缀
2. **后端实体类**: 使用 `@Data` 注解,添加审计字段注解
3. **后端业务层**: 在 `ibs` 模块下开发,简单 CRUD 用 MyBatis Plus,复杂操作用 XML
4. **后端测试**: 使用 `/login/test` 获取 token 后测试接口
5. **前端开发**: 在 `ruoyi-ui/views/` 下对应业务模块开发
6. **菜单配置**: 在 `sys_menu` 表添加菜单项
7. **API 文档**: 生成后保存在 `doc/api/` 目录

View File

@@ -1,308 +0,0 @@
# 特色区域详情窗口添加查看客户功能设计文档
## 文档信息
- **日期**: 2026-02-28
- **作者**: Claude Code
- **模块**: 区域绘制 - 特色区域
- **需求来源**: 用户需求
## 1. 概述
### 1.1 背景
数字支行辅助管理系统的区域绘制功能包含两种区域类型:
1. **行政区域**btnType=1使用 index.vue 页面,区域详情窗口已有"查看客户"按钮
2. **特色区域**btnType=2使用 BMapPolygonEditor.vue 组件,区域详情窗口缺少"查看客户"功能
### 1.2 目标
在特色区域的区域详情窗口中,添加"查看客户"按钮,使功能与行政区域保持一致,方便用户快速查看该区域内的客户信息。
### 1.3 范围
**涉及模块:**
- 前端:`ruoyi-ui/src/map/BMapPolygonEditor.vue`
- 复用组件:`ruoyi-ui/src/views/grid/map/draw-area/customer-modal.vue`
**不涉及:**
- 后端 API 修改
- 数据库修改
- 新增文件
## 2. 需求分析
### 2.1 用户需求
在区域绘制的特色区域详情窗口内添加一个查看客户的按钮功能与特色区域列表menulist-modal中的查看客户功能保持一致。
### 2.2 功能需求
#### 必须实现MVP
- ✅ 在区域详情窗口的按钮区域添加"查看客户"图标按钮
- ✅ 点击按钮后打开客户查看模态框
- ✅ 模态框中显示该特色区域的企业、个人、商户客户列表
- ✅ 支持客户类型切换(企业/个人/商户)
- ✅ 支持分页查询
- ✅ 支持点击客户名称跳转到客户详情页
#### 可选功能
-
### 2.3 非功能需求
- **性能**: 不影响页面加载速度
- **易用性**: 图标按钮风格与现有按钮保持一致
- **兼容性**: 不影响现有功能
- **可维护性**: 代码结构清晰,复用现有组件
## 3. 技术设计
### 3.1 整体架构
采用组件复用架构:
- **展示层**: BMapPolygonEditor.vue特色区域地图编辑器
- **组件层**: CustomerModal.vue客户查看模态框已存在
- **数据层**: API 接口shapeCustList已存在
### 3.2 方案选择
经过方案对比分析,选择**方案1添加图标按钮**
**方案对比:**
| 方案 | 优点 | 缺点 | 评分 |
|------|------|------|------|
| 方案1图标按钮 | UI风格一致、改动最小、实现简单 | 图标不如文字明显 | ★★★★★ |
| 方案2文字按钮 | 按钮明显、文字清晰 | 可能破坏布局、需要调整样式 | ★★★☆☆ |
| 方案3替换布局 | 与行政区域一致 | 改动大、影响用户体验 | ★★☆☆☆ |
### 3.3 数据流程
```
用户操作流程:
用户点击特色区域
显示区域详情窗口area-info-modal
用户点击"查看客户"图标按钮
触发 previewCustomer() 方法
调用 this.$refs.customerModal.onOpen()
CustomerModal 组件接收参数:
- cardType="featured" (标识特色区域)
- :detailInfo="areaForm" (包含 shapeId)
- :btnType="'2'" (特色区域类型)
CustomerModal 调用 shapeCustList API
获取并展示客户列表数据
用户可切换客户类型(企业/个人/商户)
用户可点击客户名称查看详情
```
### 3.4 接口设计
**使用现有接口:**
#### 获取特色区域客户列表
- **接口**: `shapeCustList`
- **文件**: `@/api/grid/draw-area.js`
- **参数**:
- shapeId: 区域ID
- pageNum: 页码
- pageSize: 每页条数
- custType: 客户类型0=个人1=商户2=企业)
- **返回**: 客户列表数据
**无需新增接口。**
### 3.5 组件设计
#### BMapPolygonEditor.vue 修改
**1. 引入组件**
```javascript
import CustomerModal from "@/views/grid/map/draw-area/customer-modal.vue"
export default {
components: {
// ... 现有组件
CustomerModal,
},
}
```
**2. 添加组件引用**
在 template 末尾约第273行之后添加
```vue
<customer-modal
ref="customerModal"
cardType="featured"
:detailInfo="areaForm"
:btnType="'2'"
/>
```
**3. 添加图标按钮**
在区域详情窗口的按钮区域第221-270行建议位置在"修改信息"按钮之后:
```vue
<el-tooltip placement="top" effect="light" content="查看客户">
<i
class="el-icon-user icon-area"
@click.stop="previewCustomer"
/>
</el-tooltip>
```
**4. 添加方法**
```javascript
methods: {
// ... 现有方法
/**
* 查看客户
*/
previewCustomer() {
this.$refs.customerModal.onOpen()
}
}
```
#### CustomerModal.vue无需修改
该组件已实现完整的客户查看功能:
- 支持行政区域(通过 code 查询)
- 支持特色区域(通过 shapeId 查询)
- 支持三种客户类型切换
- 支持分页
- 支持跳转客户详情
## 4. 实施计划
### 4.1 开发任务
| 任务 | 文件 | 预估时间 | 负责人 |
|------|------|---------|--------|
| 添加"查看客户"功能 | BMapPolygonEditor.vue | 0.5h | 前端开发 |
| 功能测试 | - | 0.5h | 测试人员 |
**总计**: 1小时
### 4.2 测试计划
#### 单元测试
- 测试 previewCustomer() 方法是否正确调用
- 测试组件引用是否正确传递参数
#### 集成测试
- 测试点击按钮后模态框是否正常打开
- 测试客户列表是否正确加载
- 测试客户类型切换功能
- 测试分页功能
#### UI测试
- 测试图标样式是否一致
- 测试 tooltip 是否正确显示
- 测试响应式布局
#### 回归测试
- 验证现有4个图标按钮功能正常
- 验证行政区域的查看客户功能正常
- 验证页面无报错
### 4.3 部署计划
- **开发环境**: 开发完成后立即部署测试
- **测试环境**: 通过代码审查后部署
- **生产环境**: 测试通过后部署
## 5. 风险评估
### 5.1 技术风险
| 风险 | 等级 | 影响 | 缓解措施 |
|------|------|------|----------|
| 组件引入导致页面加载变慢 | 低 | 轻微 | 组件已在使用,无额外性能影响 |
| 图标样式不一致 | 低 | 轻微 | 使用现有 icon-area 样式类 |
| 数据传递错误 | 中 | 中等 | 充分测试,确保 shapeId 正确传递 |
### 5.2 业务风险
| 风险 | 等级 | 影响 | 缓解措施 |
|------|------|------|----------|
| 用户体验变化 | 低 | 轻微 | 仅新增功能,不影响现有操作 |
| 功能误解 | 低 | 轻微 | tooltip 提示清晰 |
## 6. 验收标准
### 6.1 功能验收
- [ ] 区域详情窗口中显示"查看客户"图标按钮
- [ ] 点击按钮后成功打开客户查看模态框
- [ ] 模态框顶部显示正确的区域名称和规模
- [ ] 客户列表正确加载(企业/个人/商户三种类型)
- [ ] 客户类型切换功能正常
- [ ] 分页功能正常
- [ ] 点击客户名称可跳转到客户详情页
### 6.2 UI验收
- [ ] 图标样式与现有图标按钮一致
- [ ] 图标大小、颜色、间距合理
- [ ] Tooltip 正确显示"查看客户"
- [ ] 图标位置合理,不拥挤
### 6.3 性能验收
- [ ] 页面加载速度无明显变化
- [ ] 模态框打开速度正常(<1秒
- [ ] 客户列表加载速度正常(<2秒
### 6.4 兼容性验收
- [ ] 现有4个图标按钮功能正常
- [ ] 行政区域的查看客户功能正常
- [ ] 浏览器控制台无报错
## 7. 后续优化
### 7.1 短期优化(可选)
-
### 7.2 长期优化(可选)
1. **统一按钮风格**: 考虑将所有图标按钮改为文字按钮,提高可读性
2. **权限控制**: 根据用户角色控制"查看客户"按钮的显示/隐藏
3. **数据缓存**: 对频繁查看的区域客户数据进行缓存,提升加载速度
## 8. 附录
### 8.1 相关文件
- `ruoyi-ui/src/map/BMapPolygonEditor.vue` - 特色区域地图编辑器
- `ruoyi-ui/src/views/grid/map/draw-area/customer-modal.vue` - 客户查看模态框
- `ruoyi-ui/src/views/grid/map/draw-area/components/menulist-modal.vue` - 特色区域列表
- `ruoyi-ui/src/views/grid/map/draw-area/index.vue` - 行政区域页面
### 8.2 参考资料
- Element UI 文档: https://element.eleme.cn/
- 若依框架文档: http://doc.ruoyi.vip/
### 8.3 变更记录
| 版本 | 日期 | 修改人 | 修改内容 |
|------|------|--------|----------|
| 1.0 | 2026-02-28 | Claude Code | 初始版本 |

View File

@@ -1,389 +0,0 @@
# 特色区域查看客户功能实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 在特色区域详情窗口添加"查看客户"图标按钮,复用现有的 customer-modal 组件,实现客户列表查看功能。
**架构:** 组件复用架构 - BMapPolygonEditor.vue 引入 CustomerModal.vue通过 refs 调用模态框的 onOpen() 方法。
**技术栈:** Vue 2.x, Element UI, 若依框架
**设计文档:** `docs/plans/2026-02-28-featured-area-customer-view-design.md`
---
## 任务 1: 引入 CustomerModal 组件
**文件:**
- 修改: `ruoyi-ui/src/map/BMapPolygonEditor.vue:20-30`import 区域)
**步骤 1: 在 script 标签内添加 import 语句**
在第22行MenuEdit 导入语句之后)添加:
```javascript
import CustomerModal from "@/views/grid/map/draw-area/customer-modal.vue"
```
**步骤 2: 在 components 中注册组件**
在第26-30行的 components 对象中添加 CustomerModal
```javascript
components: {
MenuEdit,
BMapPolygonEditor,
MenuEdit,
CustomerModal, // 新增
},
```
**步骤 3: 验证 import 路径正确**
运行前端项目验证无报错:
```bash
cd ruoyi-ui
npm run dev
```
预期: 浏览器控制台无 "Failed to mount component" 错误
**步骤 4: 提交代码**
```bash
git add ruoyi-ui/src/map/BMapPolygonEditor.vue
git commit -m "feat(featured-areas): 引入 CustomerModal 组件"
```
---
## 任务 2: 在 template 中添加组件引用
**文件:**
- 修改: `ruoyi-ui/src/map/BMapPolygonEditor.vue:270-275`template 末尾)
**步骤 1: 在 template 末尾添加组件标签**
在第273行`</transition>` 之后,`<div class="search-box">` 之前)添加:
```vue
</transition>
<!-- 查看客户模态框 -->
<customer-modal
ref="customerModal"
cardType="featured"
:detailInfo="areaForm"
:btnType="'2'"
/>
<div class="search-box">
```
**步骤 2: 验证组件引用**
在浏览器中打开特色区域页面,验证控制台无报错。
**步骤 3: 验证 props 传递**
在 Vue DevTools 中检查 customer-modal 组件的 props
- cardType 应该是 "featured"
- detailInfo 应该是 areaForm 对象
- btnType 应该是 "2"
**步骤 4: 提交代码**
```bash
git add ruoyi-ui/src/map/BMapPolygonEditor.vue
git commit -m "feat(featured-areas): 添加 CustomerModal 组件引用"
```
---
## 任务 3: 在区域详情窗口添加查看客户图标按钮
**文件:**
- 修改: `ruoyi-ui/src/map/BMapPolygonEditor.vue:221-240`edit-operate 按钮区域)
**步骤 1: 定位按钮插入位置**
找到第221-240行的代码区域这是 `infoType === 'SHOW'` 的按钮区域:
- 第229行修改信息按钮结束
- 第240行删除区域按钮开始
我们需要在修改信息按钮之后插入查看客户按钮。
**步骤 2: 添加查看客户图标按钮**
在第239行修改信息 tooltip 结束之后第240行删除区域 tooltip 开始)之前插入:
```vue
</el-tooltip>
<!-- 查看客户 -->
<el-tooltip placement="top" effect="light" content="查看客户">
<i
class="el-icon-user icon-area"
@click.stop="previewCustomer"
/>
</el-tooltip>
<!-- 删除区域 -->
<el-tooltip placement="top" effect="light" content="删除区域">
```
**步骤 3: 验证图标样式**
在浏览器中:
1. 打开特色区域页面
2. 点击一个已绘制的区域
3. 查看区域详情窗口底部的图标按钮区域
4. 验证新添加的"用户图标"样式与其他图标一致
**步骤 4: 验证 tooltip 显示**
1. 鼠标悬停在新添加的用户图标上
2. 验证 tooltip 显示"查看客户"
3. 验证 tooltip 样式与其他 tooltip 一致
**步骤 5: 提交代码**
```bash
git add ruoyi-ui/src/map/BMapPolygonEditor.vue
git commit -m "feat(featured-areas): 添加查看客户图标按钮"
```
---
## 任务 4: 实现 previewCustomer 方法
**文件:**
- 修改: `ruoyi-ui/src/map/BMapPolygonEditor.vue:900-1000`methods 区域)
**步骤 1: 定位 methods 区域**
找到 methods 对象的最后,准备添加新方法。建议添加在 `updateAreaShape()` 方法之后。
**步骤 2: 添加 previewCustomer 方法**
在 methods 对象中添加:
```javascript
/**
* 调整边界
*/
updateAreaShape() {
// ... 现有代码
},
/**
* 查看客户
*/
previewCustomer() {
this.$refs.customerModal.onOpen()
}
```
**步骤 3: 验证方法调用**
在浏览器控制台中测试:
1. 打开特色区域页面
2. 点击一个区域,打开区域详情窗口
3. 打开 Vue DevTools
4. 找到 BMapPolygonEditor 组件
5. 在控制台执行:`$vm0.previewCustomer()`
6. 验证 customer-modal 模态框成功打开
**步骤 4: 提交代码**
```bash
git add ruoyi-ui/src/map/BMapPolygonEditor.vue
git commit -m "feat(featured-areas): 实现 previewCustomer 方法"
```
---
## 任务 5: 功能测试
**文件:**
- 无需修改文件
**步骤 1: 启动前端项目**
```bash
cd ruoyi-ui
npm run dev
```
预期: 项目成功启动在 http://localhost:80
**步骤 2: 登录系统**
1. 访问 http://localhost:80
2. 使用测试账号登录:
- 用户名: admin
- 密码: admin123
**步骤 3: 进入特色区域页面**
1. 导航到"网格管理" > "区域绘制"
2. 点击"特色区域"标签btnType=2
3. 等待地图加载完成
**步骤 4: 测试查看客户按钮**
1. 在左侧菜单中选择一个特色区域图层
2. 在地图上点击一个已绘制的特色区域
3. 验证右上角弹出区域详情窗口
4. 查看窗口底部的图标按钮区域
5. 验证显示"用户图标"(查看客户按钮)
**步骤 5: 测试点击功能**
1. 点击"查看客户"图标
2. 验证:
- customer-modal 对话框成功打开
- 对话框标题显示"查看客户"
- 顶部显示区域名称和规模信息
- 客户列表成功加载
- 显示三种客户类型标签(企业/个人/商户)
**步骤 6: 测试客户类型切换**
1. 点击"个人"标签
2. 验证客户列表刷新,显示个人客户
3. 点击"商户"标签
4. 验证客户列表刷新,显示商户客户
5. 点击"企业"标签
6. 验证客户列表刷新,显示企业客户
**步骤 7: 测试分页功能**
1. 如果客户数量超过10条验证分页器显示
2. 点击下一页
3. 验证客户列表刷新,显示第二页数据
4. 修改每页显示条数
5. 验证列表重新加载
**步骤 8: 测试客户详情跳转**
1. 点击表格中的客户名称el-button
2. 验证路由跳转到客户详情页面
3. 验证详情页面显示正确的客户信息
**步骤 9: 测试现有功能兼容性**
1. 关闭客户模态框
2. 依次点击其他4个图标按钮
- 重新申请
- 修改信息
- 删除区域
- 调整边界
3. 验证这些按钮功能正常,无报错
**步骤 10: 测试行政区域兼容性**
1. 切换到"行政区域"标签btnType=1
2. 点击一个行政区域
3. 验证行政区域的"查看客户"按钮功能正常
4. 验证两个区域类型的查看客户功能互不影响
**步骤 11: 检查浏览器控制台**
1. 打开浏览器开发者工具
2. 切换到 Console 标签
3. 执行所有测试步骤
4. 验证控制台无 JavaScript 报错
5. 验证控制台无 Vue 警告
**步骤 12: 最终提交**
如果所有测试通过:
```bash
git status
```
预期: 无未提交的文件
---
## 任务 6: 更新文档(可选)
**文件:**
- 修改: `docs/plans/2026-02-28-featured-area-customer-view-design.md`
**步骤 1: 更新验收标准**
在验收标准的各项前打勾:
```markdown
### 6.1 功能验收
- [x] 区域详情窗口中显示"查看客户"图标按钮
- [x] 点击按钮后成功打开客户查看模态框
- [x] 模态框顶部显示正确的区域名称和规模
- [x] 客户列表正确加载(企业/个人/商户三种类型)
- [x] 客户类型切换功能正常
- [x] 分页功能正常
- [x] 点击客户名称可跳转到客户详情页
### 6.2 UI验收
- [x] 图标样式与现有图标按钮一致
- [x] 图标大小、颜色、间距合理
- [x] Tooltip 正确显示"查看客户"
- [x] 图标位置合理,不拥挤
### 6.3 性能验收
- [x] 页面加载速度无明显变化
- [x] 模态框打开速度正常(<1秒
- [x] 客户列表加载速度正常(<2秒
### 6.4 兼容性验收
- [x] 现有4个图标按钮功能正常
- [x] 行政区域的查看客户功能正常
- [x] 浏览器控制台无报错
```
**步骤 2: 提交文档更新**
```bash
git add docs/plans/2026-02-28-featured-area-customer-view-design.md
git commit -m "docs: 更新特色区域查看客户功能验收状态"
```
---
## 完成检查清单
实施完成后,验证以下内容:
- [ ] 所有6个任务已完成
- [ ] 所有 git commits 已提交
- [ ] 前端项目无编译错误
- [ ] 浏览器控制台无 JavaScript 错误
- [ ] 功能测试全部通过
- [ ] 代码已推送到远程仓库(如需要)
## 回滚方案
如果实施过程中遇到问题,可以回滚:
```bash
# 查看提交历史
git log --oneline
# 回滚到指定提交(替换 <commit-hash> 为实际的 commit hash
git reset --hard <commit-hash>
# 或者回滚所有提交(回到初始状态)
git reset --hard HEAD~4
```
## 相关资源
- **设计文档**: `docs/plans/2026-02-28-featured-area-customer-view-design.md`
- **修改文件**: `ruoyi-ui/src/map/BMapPolygonEditor.vue`
- **复用组件**: `ruoyi-ui/src/views/grid/map/draw-area/customer-modal.vue`
- **参考实现**: `ruoyi-ui/src/views/grid/map/draw-area/components/menulist-modal.vue:413-416`
- **Element UI 文档**: https://element.eleme.cn/#/zh-CN/component/tooltip
- **Vue 2.x 文档**: https://v2.cn.vuejs.org/

88
ibs-group/pom.xml Normal file
View File

@@ -0,0 +1,88 @@
<?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>
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.8.8</version>
</parent>
<artifactId>ibs-group</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Mockito依赖 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version> <!-- 请根据需要选择合适的版本 -->
<scope>test</scope>
</dependency>
<!-- JUnit依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version> <!-- 请根据需要选择合适的版本 -->
<scope>test</scope>
</dependency>
<!-- 通用工具-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.31</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.postgresql</groupId>-->
<!-- <artifactId>postgresql</artifactId>-->
<!-- <version>42.3.3</version>-->
<!-- </dependency>-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel-core</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.2</version>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ibs</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,164 @@
package com.ruoyi.group.controller;
import com.alibaba.fastjson2.JSON;
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.group.domain.dto.CustGroupMemberTemplate;
import com.ruoyi.group.domain.dto.CustGroupQueryDTO;
import com.ruoyi.group.domain.dto.GridImportDTO;
import com.ruoyi.group.domain.entity.CustGroup;
import com.ruoyi.group.domain.vo.CustGroupVO;
import com.ruoyi.group.service.ICustGroupService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.List;
/**
* 客群管理Controller
*
* @author ruoyi
*/
@Api(tags = "客群管理接口")
@RestController
@RequestMapping("/group/cust")
public class CustGroupController extends BaseController {
@Resource
private ICustGroupService custGroupService;
/**
* 查询客群列表
*/
@ApiOperation("查询客群列表")
@Log(title = "客群管理-查询客群列表")
@GetMapping("/list")
public TableDataInfo listCustGroup(CustGroupQueryDTO dto) {
startPage();
List<CustGroupVO> list = custGroupService.listCustGroup(dto);
return getDataTable(list);
}
/**
* 获取客群详情
*/
@ApiOperation("获取客群详情")
@Log(title = "客群管理-获取客群详情")
@GetMapping("/{id}")
public AjaxResult getCustGroup(@PathVariable Long id) {
CustGroupVO vo = custGroupService.getCustGroup(id);
return AjaxResult.success(vo);
}
/**
* 异步创建客群(网格导入)
*/
@ApiOperation("异步创建客群(网格导入)")
@Log(title = "客群管理-网格导入创建客群", businessType = BusinessType.INSERT)
@PostMapping("/createByGrid")
public AjaxResult createCustGroupByGrid(@RequestBody @Valid GridImportDTO gridImportDTO) {
String id = custGroupService.createCustGroupByGrid(gridImportDTO);
return AjaxResult.success(id);
}
/**
* 异步创建客群(模板导入)
*/
@ApiOperation("异步创建客群(模板导入)")
@Log(title = "客群管理-异步创建客群", businessType = BusinessType.INSERT)
@PostMapping(value = "/createByTemplate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public AjaxResult createCustGroupByTemplate(@RequestPart("dto") @Valid String dtoJson,
@RequestPart("file") MultipartFile file) {
CustGroup custGroup = JSON.parseObject(dtoJson, CustGroup.class);
return AjaxResult.success(custGroupService.createCustGroupByTemplate(custGroup, file));
}
/**
* 更新客群
*/
@ApiOperation("更新客群")
@Log(title = "客群管理-更新客群", businessType = BusinessType.UPDATE)
@PostMapping("/update")
public AjaxResult updateCustGroup(@RequestBody @Valid CustGroup custGroup) {
String result = custGroupService.updateCustGroup(custGroup);
return AjaxResult.success(result);
}
/**
* 更新客群(网格导入)
*/
@ApiOperation("更新客群(网格导入)")
@Log(title = "客群管理-更新客群(网格导入)", businessType = BusinessType.UPDATE)
@PostMapping("/updateByGrid")
public AjaxResult updateCustGroupByGrid(@RequestBody @Valid GridImportDTO gridImportDTO) {
String result = custGroupService.updateCustGroupByGrid(gridImportDTO);
return AjaxResult.success(result);
}
/**
* 更新客群(模板导入)
*/
@ApiOperation("更新客群(模板导入)")
@Log(title = "客群管理-更新客群(模板导入)", businessType = BusinessType.UPDATE)
@PostMapping(value = "/updateByTemplate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public AjaxResult updateCustGroupByTemplate(@RequestPart("dto") @Valid String dtoJson,
@RequestPart("file") MultipartFile file) {
CustGroup custGroup = JSON.parseObject(dtoJson, CustGroup.class);
return AjaxResult.success(custGroupService.updateCustGroupByTemplate(custGroup, file));
}
/**
* 轮询客群创建状态
*/
@ApiOperation("轮询客群创建状态")
@Log(title = "客群管理-轮询客群创建状态")
@GetMapping("/createStatus/{id}")
public AjaxResult getCreateStatus(@PathVariable Long id) {
String status = custGroupService.getCreateStatus(id);
return AjaxResult.success(status);
}
/**
* 客户信息模板下载
*/
@ApiOperation("客户信息模板")
@Log(title = "客群管理-客户信息模板", businessType = BusinessType.EXPORT)
@PostMapping("/download")
public void download(HttpServletResponse response) {
ExcelUtil<CustGroupMemberTemplate> util = new ExcelUtil<>(CustGroupMemberTemplate.class);
util.exportExcel(response, null, "客户信息模板");
}
/**
* 删除客群
*/
@ApiOperation("删除客群")
@Log(title = "客群管理-删除客群", businessType = BusinessType.DELETE)
@PostMapping("/delete")
public AjaxResult deleteCustGroup(@RequestBody List<Long> idList) {
String result = custGroupService.deleteCustGroup(idList);
return AjaxResult.success(result);
}
/**
* 手动移除客群客户
*/
@ApiOperation("手动移除客群客户")
@Log(title = "客群管理-手动移除客户", businessType = BusinessType.DELETE)
@PostMapping("/removeMembers")
public AjaxResult removeMembers(@RequestParam Long groupId, @RequestBody List<Long> memberIds) {
String result = custGroupService.removeMembers(groupId, memberIds);
return AjaxResult.success(result);
}
}

View File

@@ -0,0 +1,49 @@
package com.ruoyi.group.domain.dto;
import com.ruoyi.common.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 客群客户Excel模板
*
* @author ruoyi
*/
@Data
public class CustGroupMemberTemplate {
/**
* 客户类型
*/
@ApiModelProperty(value = "客户类型:个人/企业/商户", name = "custType")
@Excel(name = "客户类型(个人/企业/商户)", prompt = "必填", combo = "个人,企业,商户")
private String custType;
/**
* 客户号
*/
@ApiModelProperty(value = "客户号", name = "custId")
@Excel(name = "客户号", prompt = "必填")
private String custId;
/**
* 客户姓名
*/
@ApiModelProperty(value = "客户姓名", name = "custName")
@Excel(name = "客户姓名")
private String custName;
/**
* 客户身份证号
*/
@ApiModelProperty(value = "客户身份证号(个人客户必填)", name = "custIdc")
@Excel(name = "客户身份证号(个人客户必填)")
private String custIdc;
/**
* 统信码
*/
@ApiModelProperty(value = "统信码(企业/商户客户必填)", name = "socialCreditCode")
@Excel(name = "统信码(企业/商户客户必填)")
private String socialCreditCode;
}

View File

@@ -0,0 +1,53 @@
package com.ruoyi.group.domain.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 客群查询DTO
*
* @author ruoyi
*/
@Data
public class CustGroupQueryDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 客群名称(模糊查询)
*/
@ApiModelProperty(value = "客群名称", name = "groupName")
private String groupName;
/**
* 客群模式0=静态, 1=动态
*/
@ApiModelProperty(value = "客群模式", name = "groupMode")
private String groupMode;
/**
* 创建方式1=模板导入, 2=绩效网格, 3=地理网格, 4=自定义网格
*/
@ApiModelProperty(value = "创建方式", name = "createMode")
private String createMode;
/**
* 客群状态0=正常, 1=已禁用
*/
@ApiModelProperty(value = "客群状态", name = "groupStatus")
private String groupStatus;
/**
* 页码
*/
@ApiModelProperty(value = "页码", name = "pageNum")
private Integer pageNum = 1;
/**
* 每页大小
*/
@ApiModelProperty(value = "每页大小", name = "pageSize")
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.group.domain.dto;
import com.ruoyi.group.domain.entity.CustGroup;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 网格导入客群DTO
*
* @author ruoyi
*/
@Data
public class GridImportDTO {
/**
* 客群信息
*/
@ApiModelProperty(value = "客群信息", name = "custGroup")
@Valid
@NotNull(message = "客群信息不能为空")
private CustGroup custGroup;
/**
* 网格类型0=绩效网格, 1=地理网格, 2=绘制网格
*/
@ApiModelProperty(value = "网格类型0=绩效网格, 1=地理网格, 2=绘制网格", name = "gridType")
@NotBlank(message = "网格类型不能为空")
private String gridType;
/**
* 绩效网格业务类型retail=零售, corporate=公司gridType=0时必填
*/
@ApiModelProperty(value = "绩效网格业务类型retail=零售, corporate=公司", name = "cmpmBizType")
private String cmpmBizType;
/**
* 绩效网格客户经理柜员号列表gridType=0时必填
*/
@ApiModelProperty(value = "客户经理柜员号列表", name = "userNames")
private List<String> userNames;
/**
* 地理网格ID列表gridType=1时必填
*/
@ApiModelProperty(value = "地理网格ID列表", name = "regionGridIds")
private List<Long> regionGridIds;
/**
* 绘制网格ID列表gridType=2时必填
*/
@ApiModelProperty(value = "绘制网格ID列表", name = "drawGridIds")
private List<Long> drawGridIds;
}

View File

@@ -0,0 +1,172 @@
package com.ruoyi.group.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 客群实体
*
* @author ruoyi
*/
@Data
@TableName("ibs_cust_group")
public class CustGroup {
/**
* 主键ID
*/
@ApiModelProperty(value = "主键ID", name = "id")
@TableId(type = IdType.AUTO)
private Long id;
/**
* 客群名称
*/
@ApiModelProperty(value = "客群名称", name = "groupName")
private String groupName;
/**
* 客群模式0=静态, 1=动态
*/
@ApiModelProperty(value = "客群模式0=静态, 1=动态", name = "groupMode")
private String groupMode;
/**
* 创建方式1=模板导入, 2=绩效网格, 3=地理网格, 4=自定义网格
*/
@ApiModelProperty(value = "创建方式1=模板导入, 2=绩效网格, 3=地理网格, 4=自定义网格", name = "createMode")
private String createMode;
/**
* 柜员号
*/
@ApiModelProperty(value = "柜员号", name = "userName")
private String userName;
/**
* 柜员名称
*/
@ApiModelProperty(value = "柜员名称", name = "nickName")
private String nickName;
/**
* 所属机构ID
*/
@ApiModelProperty(value = "所属机构ID", name = "deptId")
@TableField(fill = FieldFill.INSERT)
private Long deptId;
/**
* 是否开启共享0=否, 1=是
*/
@ApiModelProperty(value = "是否开启共享0=否, 1=是", name = "shareEnabled")
private Integer shareEnabled;
/**
* 可见部门ID列表逗号分隔
*/
@ApiModelProperty(value = "可见部门ID列表逗号分隔", name = "shareDeptIds")
private String shareDeptIds;
/**
* 共享部门ID列表非表字段用于接收前端传参
*/
@ApiModelProperty(value = "共享部门ID列表", name = "shareDeptIdList")
@TableField(exist = false)
private List<Long> shareDeptIdList;
/**
* 客群状态0=正常, 1=已禁用
*/
@ApiModelProperty(value = "客群状态0=正常, 1=已禁用", name = "groupStatus")
private String groupStatus;
/**
* 创建状态0=创建中, 1=创建成功, 2=创建失败
*/
@ApiModelProperty(value = "创建状态0=创建中, 1=创建成功, 2=创建失败", name = "createStatus")
private String createStatus;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 删除标识0=正常, 1=删除
*/
@TableLogic(value = "0", delval = "1")
@TableField("del_flag")
private Integer delFlag;
/**
* 备注
*/
@ApiModelProperty(value = "备注", name = "remark")
private String remark;
/**
* 网格类型0=绩效网格, 1=地理网格, 2=绘制网格(动态客群使用)
*/
@ApiModelProperty(value = "网格类型", name = "gridType")
private String gridType;
/**
* 绩效业务类型retail=零售, corporate=公司(动态客群使用)
*/
@ApiModelProperty(value = "绩效业务类型", name = "cmpmBizType")
private String cmpmBizType;
/**
* 客户经理列表(逗号分隔,动态客群使用)
*/
@ApiModelProperty(value = "客户经理列表", name = "gridUserNames")
private String gridUserNames;
/**
* 地理网格ID列表逗号分隔动态客群使用
*/
@ApiModelProperty(value = "地理网格ID列表", name = "regionGridIds")
private String regionGridIds;
/**
* 绘制网格ID列表逗号分隔动态客群使用
*/
@ApiModelProperty(value = "绘制网格ID列表", name = "drawGridIds")
private String drawGridIds;
/**
* 有效期截止时间
*/
@ApiModelProperty(value = "有效期截止时间", name = "validTime")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date validTime;
}

View File

@@ -0,0 +1,89 @@
package com.ruoyi.group.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客群客户关联实体
*
* @author ruoyi
*/
@Data
@TableName("ibs_cust_group_member")
public class CustGroupMember {
/**
* 主键ID
*/
@ApiModelProperty(value = "主键ID", name = "id")
@TableId(type = IdType.AUTO)
private Long id;
/**
* 客群ID
*/
@ApiModelProperty(value = "客群ID", name = "groupId")
private Long groupId;
/**
* 客户类型0=个人, 1=商户, 2=企业
*/
@ApiModelProperty(value = "客户类型0=个人, 1=商户, 2=企业", name = "custType")
private String custType;
/**
* 客户号
*/
@ApiModelProperty(value = "客户号", name = "custId")
private String custId;
/**
* 客户姓名(冗余)
*/
@ApiModelProperty(value = "客户姓名(冗余)", name = "custName")
private String custName;
/**
* 客户身份证号
*/
@ApiModelProperty(value = "客户身份证号", name = "custIdc")
private String custIdc;
/**
* 统信码(商户/企业有)
*/
@ApiModelProperty(value = "统信码(商户/企业有)", name = "socialCreditCode")
private String socialCreditCode;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 删除标识0=正常, 1=删除
*/
@TableLogic(value = "0", delval = "1")
@TableField("del_flag")
private Integer delFlag;
/**
* 手动移除标识0=否, 1=是(被手动移除的客户不会被定时任务重新导入)
*/
@ApiModelProperty(value = "手动移除标识0=否, 1=是", name = "manualRemove")
@TableField("manual_remove")
private Integer manualRemove;
}

View File

@@ -0,0 +1,66 @@
package com.ruoyi.group.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 客群客户VO
*
* @author ruoyi
*/
@Data
public class CustGroupMemberVO {
/**
* 主键ID
*/
@ApiModelProperty(value = "主键ID", name = "id")
private Long id;
/**
* 客群ID
*/
@ApiModelProperty(value = "客群ID", name = "groupId")
private Long groupId;
/**
* 客户类型0=个人, 1=商户, 2=企业
*/
@ApiModelProperty(value = "客户类型0=个人, 1=商户, 2=企业", name = "custType")
private String custType;
/**
* 客户号
*/
@ApiModelProperty(value = "客户号", name = "custId")
private String custId;
/**
* 客户姓名
*/
@ApiModelProperty(value = "客户姓名", name = "custName")
private String custName;
/**
* 客户身份证号
*/
@ApiModelProperty(value = "客户身份证号", name = "custIdc")
private String custIdc;
/**
* 统信码(商户/企业有)
*/
@ApiModelProperty(value = "统信码(商户/企业有)", name = "socialCreditCode")
private String socialCreditCode;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间", name = "createTime")
private Date createTime;
}

View File

@@ -0,0 +1,122 @@
package com.ruoyi.group.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 客群VO
*
* @author ruoyi
*/
@Data
public class CustGroupVO {
/**
* 主键ID
*/
@ApiModelProperty(value = "主键ID", name = "id")
private Long id;
/**
* 客群名称
*/
@ApiModelProperty(value = "客群名称", name = "groupName")
private String groupName;
/**
* 客群模式0=静态, 1=动态
*/
@ApiModelProperty(value = "客群模式0=静态, 1=动态", name = "groupMode")
private String groupMode;
/**
* 创建方式1=模板导入, 2=绩效网格, 3=地理网格, 4=自定义网格
*/
@ApiModelProperty(value = "创建方式1=模板导入, 2=绩效网格, 3=地理网格, 4=自定义网格", name = "createMode")
private String createMode;
/**
* 柜员号
*/
@ApiModelProperty(value = "柜员号", name = "userName")
private String userName;
/**
* 柜员名称
*/
@ApiModelProperty(value = "柜员名称", name = "nickName")
private String nickName;
/**
* 所属机构ID
*/
@ApiModelProperty(value = "所属机构ID", name = "deptId")
private Long deptId;
/**
* 是否开启共享0=否, 1=是
*/
@ApiModelProperty(value = "是否开启共享0=否, 1=是", name = "shareEnabled")
private Integer shareEnabled;
/**
* 可见部门ID列表
*/
@ApiModelProperty(value = "可见部门ID列表", name = "shareDeptIds")
private List<Long> shareDeptIds;
/**
* 客群状态0=正常, 1=已禁用
*/
@ApiModelProperty(value = "客群状态0=正常, 1=已禁用", name = "groupStatus")
private String groupStatus;
/**
* 客户数量
*/
@ApiModelProperty(value = "客户数量", name = "custCount")
private Integer custCount;
/**
* 创建者
*/
@ApiModelProperty(value = "创建者", name = "createBy")
private String createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间", name = "createTime")
private Date createTime;
/**
* 更新者
*/
@ApiModelProperty(value = "更新者", name = "updateBy")
private String updateBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "更新时间", name = "updateTime")
private Date updateTime;
/**
* 备注
*/
@ApiModelProperty(value = "备注", name = "remark")
private String remark;
/**
* 客户列表
*/
@ApiModelProperty(value = "客户列表", name = "custList")
private List<CustGroupMemberVO> custList;
}

View File

@@ -0,0 +1,34 @@
package com.ruoyi.group.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.group.domain.dto.CustGroupQueryDTO;
import com.ruoyi.group.domain.entity.CustGroup;
import com.ruoyi.group.domain.vo.CustGroupVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 客群Mapper接口
*
* @author ruoyi
*/
public interface CustGroupMapper extends BaseMapper<CustGroup> {
/**
* 查询客群数量根据部门ID和共享设置
*
* @param deptId 部门ID
* @param userName 用户名
* @return 数量
*/
// Long countByDeptOrUser(@Param("deptId") Long deptId, @Param("userName") Long userName);
/**
* 查询客群列表
*
* @param dto 查询条件
* @return 客群VO列表
*/
List<CustGroupVO> selectCustGroupList(@Param("dto") CustGroupQueryDTO dto);
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.group.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.group.domain.entity.CustGroupMember;
import org.apache.ibatis.annotations.Param;
/**
* 客群客户关联Mapper接口
*
* @author ruoyi
*/
public interface CustGroupMemberMapper extends BaseMapper<CustGroupMember> {
/**
* 查询客群客户数量
*
* @param groupId 客群ID
* @return 数量
*/
Long countByGroupId(@Param("groupId") Long groupId);
}

View File

@@ -0,0 +1,120 @@
package com.ruoyi.group.service;
import com.ruoyi.group.domain.dto.CustGroupQueryDTO;
import com.ruoyi.group.domain.dto.GridImportDTO;
import com.ruoyi.group.domain.entity.CustGroup;
import com.ruoyi.group.domain.vo.CustGroupVO;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 客群Service接口
*
* @author ruoyi
*/
public interface ICustGroupService {
/**
* 查询客群列表
*
* @param dto 查询条件
* @return 客群VO列表
*/
List<CustGroupVO> listCustGroup(CustGroupQueryDTO dto);
/**
* 异步创建客群(模板导入)
*
* @param custGroup 客群实体
* @param file Excel文件
* @return 客群ID
*/
String createCustGroupByTemplate(CustGroup custGroup, MultipartFile file);
/**
* 异步创建客群(网格导入)
*
* @param gridImportDTO 网格导入条件
* @return 客群ID
*/
String createCustGroupByGrid(GridImportDTO gridImportDTO);
/**
* 更新客群(网格导入)
*
* @param gridImportDTO 网格导入条件
* @return 结果消息
*/
String updateCustGroupByGrid(GridImportDTO gridImportDTO);
/**
* 更新客群(模板导入)
*
* @param custGroup 客群实体
* @param file Excel文件
* @return 结果消息
*/
String updateCustGroupByTemplate(CustGroup custGroup, MultipartFile file);
/**
* 更新客群
*
* @param custGroup 客群实体
* @return 结果消息
*/
String updateCustGroup(CustGroup custGroup);
/**
* 删除客群
*
* @param idList 客群ID列表
* @return 结果消息
*/
String deleteCustGroup(List<Long> idList);
/**
* 获取客群详情
*
* @param id 客群ID
* @return 客群VO
*/
CustGroupVO getCustGroup(Long id);
/**
* 检查客群名称是否存在
*
* @param groupName 客群名称
* @return true=存在, false=不存在
*/
boolean checkGroupNameExist(String groupName);
/**
* 查询客群创建状态
*
* @param id 客群ID
* @return 创建状态0=创建中, 1=创建成功, 2=创建失败
*/
String getCreateStatus(Long id);
/**
* 手动移除客群客户
*
* @param groupId 客群ID
* @param memberIds 客群成员ID列表
* @return 结果消息
*/
String removeMembers(Long groupId, List<Long> memberIds);
/**
* 更新动态客群(定时任务调用)
* 根据原始导入条件重新查询并更新客户列表
*/
void updateDynamicCustGroups();
/**
* 检查并禁用过期客群(定时任务调用)
*/
void checkAndDisableExpiredGroups();
}

View File

@@ -0,0 +1,817 @@
package com.ruoyi.group.service.impl;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import com.ruoyi.ibs.cmpm.domain.entity.GridCmpm;
import com.ruoyi.ibs.cmpm.mapper.GridCmpmMapper;
import com.ruoyi.ibs.draw.domain.entity.DrawGridShapeRelate;
import com.ruoyi.ibs.draw.domain.entity.DrawShapeCust;
import com.ruoyi.ibs.draw.mapper.DrawGridShapeRelateMapper;
import com.ruoyi.ibs.draw.mapper.DrawShapeCustMapper;
import com.ruoyi.ibs.grid.domain.vo.CustVO;
import com.ruoyi.ibs.grid.service.RegionGridListService;
import com.ruoyi.group.domain.dto.CustGroupMemberTemplate;
import com.ruoyi.group.domain.dto.CustGroupQueryDTO;
import com.ruoyi.group.domain.dto.GridImportDTO;
import com.ruoyi.group.domain.entity.CustGroup;
import com.ruoyi.group.domain.entity.CustGroupMember;
import com.ruoyi.group.domain.vo.CustGroupMemberVO;
import com.ruoyi.group.domain.vo.CustGroupVO;
import com.ruoyi.group.mapper.CustGroupMapper;
import com.ruoyi.group.mapper.CustGroupMemberMapper;
import com.ruoyi.group.service.ICustGroupService;
import com.ruoyi.ibs.grid.domain.entity.RegionCustUser;
import com.ruoyi.ibs.grid.domain.entity.RegionGrid;
import com.ruoyi.ibs.grid.mapper.RegionCustUserMapper;
import com.ruoyi.ibs.grid.mapper.RegionGridMapper;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
/**
* 客群Service实现
*
* @author ruoyi
*/
@Service
@Slf4j
public class CustGroupServiceImpl implements ICustGroupService {
@Resource
private CustGroupMapper custGroupMapper;
@Resource
private CustGroupMemberMapper custGroupMemberMapper;
@Resource(name = "excelImportExecutor")
private ExecutorService executorService;
@Resource
private GridCmpmMapper gridCmpmMapper;
@Resource
private RegionCustUserMapper regionCustUserMapper;
@Resource
private RegionGridMapper regionGridMapper;
@Resource
private RegionGridListService regionGridListService;
@Resource
private DrawShapeCustMapper drawShapeCustMapper;
@Resource
private DrawGridShapeRelateMapper drawGridShapeRelateMapper;
@Resource
private SysDeptMapper sysDeptMapper;
@Override
public List<CustGroupVO> listCustGroup(CustGroupQueryDTO dto) {
return custGroupMapper.selectCustGroupList(dto);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String createCustGroupByTemplate(CustGroup custGroup, MultipartFile file) {
// 检查客群名称是否存在
if (checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在");
}
// 获取当前用户信息
SysUser user = SecurityUtils.getLoginUser().getUser();
custGroup.setUserName(user.getUserName());
custGroup.setNickName(user.getNickName());
// 设置默认值
custGroup.setGroupMode("0"); // 静态
custGroup.setCreateMode("1"); // 模板导入
if (StringUtils.isEmpty(custGroup.getGroupStatus())) {
custGroup.setGroupStatus("0");
}
// 设置创建状态为创建中
custGroup.setCreateStatus("0");
// 插入客群
custGroupMapper.insert(custGroup);
// 获取当前用户部门编码(异步线程中无法获取)
String headId = SecurityUtils.getHeadId();
// 异步导入客户
executorService.submit(() -> doImportCustGroupByTemplate(custGroup, file, headId));
return String.valueOf(custGroup.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public String createCustGroupByGrid(GridImportDTO gridImportDTO) {
CustGroup custGroup = gridImportDTO.getCustGroup();
// 检查客群名称是否存在
if (checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在");
}
// 获取当前用户信息
SysUser user = SecurityUtils.getLoginUser().getUser();
custGroup.setUserName(user.getUserName());
custGroup.setNickName(user.getNickName());
// 默认状态为正常
if (StringUtils.isEmpty(custGroup.getGroupStatus())) {
custGroup.setGroupStatus("0");
}
// 设置创建模式和状态
custGroup.setGroupMode(custGroup.getGroupMode());
// 根据网格类型设置创建方式0=模板导入, 2=绩效网格, 3=地理网格, 4=绘制网格
String gridType = gridImportDTO.getGridType();
if ("0".equals(gridType)) {
custGroup.setCreateMode("2"); // 绩效网格
} else if ("1".equals(gridType)) {
custGroup.setCreateMode("3"); // 地理网格
} else if ("2".equals(gridType)) {
custGroup.setCreateMode("4"); // 绘制网格
} else {
throw new ServiceException("无效的网格类型");
}
// 保存网格导入条件(用于动态客群后续更新)
custGroup.setGridType(gridImportDTO.getGridType());
if ("0".equals(gridType)) {
custGroup.setCmpmBizType(gridImportDTO.getCmpmBizType());
if (gridImportDTO.getUserNames() != null && !gridImportDTO.getUserNames().isEmpty()) {
custGroup.setGridUserNames(String.join(",", gridImportDTO.getUserNames()));
}
} else if ("1".equals(gridType)) {
if (gridImportDTO.getRegionGridIds() != null && !gridImportDTO.getRegionGridIds().isEmpty()) {
custGroup.setRegionGridIds(gridImportDTO.getRegionGridIds().stream()
.map(String::valueOf).collect(Collectors.joining(",")));
}
} else if ("2".equals(gridType)) {
if (gridImportDTO.getDrawGridIds() != null && !gridImportDTO.getDrawGridIds().isEmpty()) {
custGroup.setDrawGridIds(gridImportDTO.getDrawGridIds().stream()
.map(String::valueOf).collect(Collectors.joining(",")));
}
}
// 设置创建状态为创建中
custGroup.setCreateStatus("0");
// 插入客群
custGroupMapper.insert(custGroup);
// 重新设置回DTO确保异步线程能获取到正确的ID和状态
gridImportDTO.setCustGroup(custGroup);
// 获取当前用户部门编码(异步线程中无法获取)
String headId = SecurityUtils.getHeadId();
// 异步导入客户
executorService.submit(() -> doImportCustGroupByGrid(gridImportDTO, headId));
return String.valueOf(custGroup.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public String updateCustGroup(CustGroup custGroup) {
CustGroup existGroup = custGroupMapper.selectById(custGroup.getId());
if (existGroup == null) {
throw new ServiceException("客群不存在");
}
// 检查客群名称是否存在(排除自己)
if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在");
}
custGroupMapper.updateById(custGroup);
return "客群更新成功";
}
@Override
@Transactional(rollbackFor = Exception.class)
public String updateCustGroupByGrid(GridImportDTO gridImportDTO) {
CustGroup custGroup = gridImportDTO.getCustGroup();
// 检查客群是否存在
CustGroup existGroup = custGroupMapper.selectById(custGroup.getId());
if (existGroup == null) {
throw new ServiceException("客群不存在");
}
// 更新客群基本信息
if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在");
}
// 重新查询数据库,获取最新状态
CustGroup latestGroup = custGroupMapper.selectById(custGroup.getId());
latestGroup.setGroupName(custGroup.getGroupName());
latestGroup.setGroupMode(custGroup.getGroupMode());
latestGroup.setRemark(custGroup.getRemark());
latestGroup.setValidTime(custGroup.getValidTime());
latestGroup.setShareEnabled(custGroup.getShareEnabled());
latestGroup.setShareDeptIds(custGroup.getShareDeptIds());
// 更新数据库
custGroupMapper.updateById(latestGroup);
// 重新设置回DTO确保异步线程能获取到正确的ID和状态
gridImportDTO.setCustGroup(latestGroup);
// 获取当前用户部门编码(异步线程中无法获取)
String headId = SecurityUtils.getHeadId();
// 异步导入客户
executorService.submit(() -> doImportCustGroupByGrid(gridImportDTO, headId));
return "客群更新中";
}
@Override
@Transactional(rollbackFor = Exception.class)
public String updateCustGroupByTemplate(CustGroup custGroup, MultipartFile file) {
// 检查客群是否存在
CustGroup existGroup = custGroupMapper.selectById(custGroup.getId());
if (existGroup == null) {
throw new ServiceException("客群不存在");
}
// 更新客群基本信息
if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在");
}
// 重新查询数据库,获取最新状态
CustGroup latestGroup = custGroupMapper.selectById(custGroup.getId());
latestGroup.setGroupName(custGroup.getGroupName());
latestGroup.setGroupMode(custGroup.getGroupMode());
latestGroup.setRemark(custGroup.getRemark());
latestGroup.setValidTime(custGroup.getValidTime());
latestGroup.setShareEnabled(custGroup.getShareEnabled());
latestGroup.setShareDeptIds(custGroup.getShareDeptIds());
// 更新数据库
custGroupMapper.updateById(latestGroup);
// 获取当前用户部门编码(异步线程中无法获取)
String headId = SecurityUtils.getHeadId();
// 异步导入客户(使用最新状态的对象)
executorService.submit(() -> doImportCustGroupByTemplate(latestGroup, file, headId));
return "客群更新中";
}
@Override
@Transactional(rollbackFor = Exception.class)
public String deleteCustGroup(List<Long> idList) {
if (idList == null || idList.isEmpty()) {
throw new ServiceException("请选择要删除的客群");
}
for (Long id : idList) {
// 删除客群客户关联
LambdaQueryWrapper<CustGroupMember> memberWrapper = new LambdaQueryWrapper<>();
memberWrapper.eq(CustGroupMember::getGroupId, id);
custGroupMemberMapper.delete(memberWrapper);
// 删除客群
custGroupMapper.deleteById(id);
}
return "客群删除成功";
}
@Override
@Transactional(rollbackFor = Exception.class)
public String removeMembers(Long groupId, List<Long> memberIds) {
if (memberIds == null || memberIds.isEmpty()) {
throw new ServiceException("请选择要移除的客户");
}
// 检查客群是否存在
CustGroup custGroup = custGroupMapper.selectById(groupId);
if (custGroup == null) {
throw new ServiceException("客群不存在");
}
// 标记为手动移除(软删除)
for (Long memberId : memberIds) {
CustGroupMember member = custGroupMemberMapper.selectById(memberId);
if (member != null && member.getGroupId().equals(groupId)) {
member.setManualRemove(1);
member.setDelFlag(1);
custGroupMemberMapper.updateById(member);
}
}
return "成功移除 " + memberIds.size() + " 个客户";
}
@Override
public CustGroupVO getCustGroup(Long id) {
CustGroup custGroup = custGroupMapper.selectById(id);
if (custGroup == null) {
throw new ServiceException("客群不存在");
}
CustGroupVO vo = new CustGroupVO();
vo.setId(custGroup.getId());
vo.setGroupName(custGroup.getGroupName());
vo.setGroupMode(custGroup.getGroupMode());
vo.setCreateMode(custGroup.getCreateMode());
vo.setUserName(custGroup.getUserName());
vo.setNickName(custGroup.getNickName());
vo.setDeptId(custGroup.getDeptId());
vo.setShareEnabled(custGroup.getShareEnabled());
vo.setGroupStatus(custGroup.getGroupStatus());
vo.setRemark(custGroup.getRemark());
vo.setCreateBy(custGroup.getCreateBy());
vo.setCreateTime(custGroup.getCreateTime());
vo.setUpdateBy(custGroup.getUpdateBy());
vo.setUpdateTime(custGroup.getUpdateTime());
// 处理共享部门ID列表
if (StringUtils.isNotEmpty(custGroup.getShareDeptIds())) {
List<Long> deptIds = new ArrayList<>();
for (String idStr : custGroup.getShareDeptIds().split(",")) {
if (StringUtils.isNotEmpty(idStr)) {
deptIds.add(Long.valueOf(idStr));
}
}
vo.setShareDeptIds(deptIds);
}
// 查询客户列表
LambdaQueryWrapper<CustGroupMember> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CustGroupMember::getGroupId, id);
List<CustGroupMember> memberList = custGroupMemberMapper.selectList(wrapper);
List<CustGroupMemberVO> memberVOList = new ArrayList<>();
for (CustGroupMember member : memberList) {
CustGroupMemberVO memberVO = new CustGroupMemberVO();
memberVO.setId(member.getId());
memberVO.setGroupId(member.getGroupId());
memberVO.setCustType(member.getCustType());
memberVO.setCustId(member.getCustId());
memberVO.setCustName(member.getCustName());
memberVO.setCustIdc(member.getCustIdc());
memberVO.setSocialCreditCode(member.getSocialCreditCode());
memberVO.setCreateTime(member.getCreateTime());
memberVOList.add(memberVO);
}
vo.setCustList(memberVOList);
vo.setCustCount(memberVOList.size());
return vo;
}
@Override
public boolean checkGroupNameExist(String groupName) {
LambdaQueryWrapper<CustGroup> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CustGroup::getGroupName, groupName);
return custGroupMapper.selectCount(wrapper) > 0;
}
@Override
public String getCreateStatus(Long id) {
CustGroup custGroup = custGroupMapper.selectById(id);
if (custGroup == null) {
throw new ServiceException("客群不存在");
}
return custGroup.getCreateStatus();
}
@Override
public void updateDynamicCustGroups() {
log.info("开始更新动态客群...");
// 查询所有动态客群
LambdaQueryWrapper<CustGroup> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CustGroup::getGroupMode, "1"); // 1=动态
wrapper.eq(CustGroup::getGroupStatus, "0"); // 0=正常
wrapper.eq(CustGroup::getCreateStatus, "1"); // 1=创建成功
List<CustGroup> dynamicGroups = custGroupMapper.selectList(wrapper);
if (dynamicGroups.isEmpty()) {
log.info("没有需要更新的动态客群");
return;
}
log.info("找到 {} 个动态客群需要更新", dynamicGroups.size());
Date now = new Date();
for (CustGroup custGroup : dynamicGroups) {
// 检查有效期,过期的客群跳过更新
if (custGroup.getValidTime() != null && custGroup.getValidTime().before(now)) {
log.info("动态客群已过期跳过更新客群ID{},客群名称:{},有效期:{}",
custGroup.getId(), custGroup.getGroupName(), custGroup.getValidTime());
continue;
}
try {
updateDynamicCustGroup(custGroup);
log.info("动态客群更新成功客群ID{},客群名称:{}", custGroup.getId(), custGroup.getGroupName());
} catch (Exception e) {
log.error("动态客群更新失败客群ID{},客群名称:{}", custGroup.getId(), custGroup.getGroupName(), e);
}
}
log.info("动态客群更新完成,总计:{},成功:{},失败:{}",
dynamicGroups.size(),
dynamicGroups.stream().filter(g -> {
// 假设更新成功的状态设置
LambdaQueryWrapper<CustGroup> w = new LambdaQueryWrapper<>();
w.eq(CustGroup::getId, g.getId());
// 这里简单统计,实际可以通过更精确的方式
return true;
}).count(),
0);
}
@Override
public void checkAndDisableExpiredGroups() {
log.info("开始检查并禁用过期客群...");
Date now = new Date();
// 查询所有正常状态且有过期时间的客群
LambdaQueryWrapper<CustGroup> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CustGroup::getGroupStatus, "0"); // 0=正常
wrapper.isNotNull(CustGroup::getValidTime);
wrapper.le(CustGroup::getValidTime, now); // validTime <= now
List<CustGroup> expiredGroups = custGroupMapper.selectList(wrapper);
if (expiredGroups.isEmpty()) {
log.info("没有需要禁用的过期客群");
return;
}
log.info("找到 {} 个过期客群需要禁用", expiredGroups.size());
for (CustGroup custGroup : expiredGroups) {
try {
custGroup.setGroupStatus("1"); // 1=已禁用
custGroupMapper.updateById(custGroup);
log.info("客群已禁用客群ID{},客群名称:{},有效期:{}",
custGroup.getId(), custGroup.getGroupName(), custGroup.getValidTime());
} catch (Exception e) {
log.error("禁用过期客群失败客群ID{},客群名称:{}", custGroup.getId(), custGroup.getGroupName(), e);
}
}
log.info("过期客群禁用完成,总计:{}", expiredGroups.size());
}
/**
* 更新单个动态客群(增量更新方式)
*/
private void updateDynamicCustGroup(CustGroup custGroup) {
// 从客群部门获取 headId定时任务中无登录用户上下文
// headId 是 deptId 的前三位
String headId = "";
if (custGroup.getDeptId() != null) {
String deptIdStr = String.valueOf(custGroup.getDeptId());
headId = deptIdStr.substring(0, 3);
}
// 查询现有客户(排除手动移除的客户,只查询正常状态的客户)
LambdaQueryWrapper<CustGroupMember> existWrapper = new LambdaQueryWrapper<>();
existWrapper.eq(CustGroupMember::getGroupId, custGroup.getId())
.eq(CustGroupMember::getDelFlag, 0)
.eq(CustGroupMember::getManualRemove, 0)
.select(CustGroupMember::getCustId, CustGroupMember::getCustType);
List<CustGroupMember> existMembers = custGroupMemberMapper.selectList(existWrapper);
Set<String> existKeySet = existMembers.stream()
.map(m -> m.getCustId() + "_" + m.getCustType())
.collect(Collectors.toSet());
// 构造 GridImportDTO 复用现有导入方法
GridImportDTO gridImportDTO = new GridImportDTO();
gridImportDTO.setCustGroup(custGroup);
gridImportDTO.setGridType(custGroup.getGridType());
gridImportDTO.setCmpmBizType(custGroup.getCmpmBizType());
// 根据网格类型设置参数并查询
List<CustGroupMember> newMemberList = new ArrayList<>();
String gridType = custGroup.getGridType();
if ("0".equals(gridType)) {
// 绩效网格
if (StringUtils.isNotEmpty(custGroup.getGridUserNames())) {
gridImportDTO.setUserNames(Arrays.asList(custGroup.getGridUserNames().split(",")));
}
newMemberList.addAll(importFromCmpmGrid(custGroup, gridImportDTO, headId));
} else if ("1".equals(gridType)) {
// 地理网格
if (StringUtils.isNotEmpty(custGroup.getRegionGridIds())) {
gridImportDTO.setRegionGridIds(Arrays.stream(custGroup.getRegionGridIds().split(","))
.map(Long::valueOf).collect(Collectors.toList()));
}
newMemberList.addAll(importFromRegionGrid(custGroup, gridImportDTO));
} else if ("2".equals(gridType)) {
// 绘制网格
if (StringUtils.isNotEmpty(custGroup.getDrawGridIds())) {
gridImportDTO.setDrawGridIds(Arrays.stream(custGroup.getDrawGridIds().split(","))
.map(Long::valueOf).collect(Collectors.toList()));
}
newMemberList.addAll(importFromDrawGrid(custGroup, gridImportDTO));
}
// 计算差异
Set<String> newKeySet = newMemberList.stream()
.map(m -> m.getCustId() + "_" + m.getCustType())
.collect(Collectors.toSet());
// 需要删除的客户:存在于旧列表但不存在于新列表
List<String> toDeleteKeys = new ArrayList<>(existKeySet);
toDeleteKeys.removeAll(newKeySet);
// 需要新增的客户:存在于新列表但不存在于旧列表
List<CustGroupMember> toAddMembers = newMemberList.stream()
.filter(m -> !existKeySet.contains(m.getCustId() + "_" + m.getCustType()))
.collect(Collectors.toList());
// 删除不再存在的客户
if (!toDeleteKeys.isEmpty()) {
for (String key : toDeleteKeys) {
String[] parts = key.split("_");
LambdaQueryWrapper<CustGroupMember> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(CustGroupMember::getGroupId, custGroup.getId())
.eq(CustGroupMember::getCustId, parts[0])
.eq(CustGroupMember::getCustType, parts[1]);
custGroupMemberMapper.delete(deleteWrapper);
}
}
// 插入新增客户
int addCount = 0;
for (CustGroupMember member : toAddMembers) {
try {
custGroupMemberMapper.insert(member);
addCount++;
} catch (org.springframework.dao.DuplicateKeyException e) {
log.debug("客户已存在跳过groupId={}, custId={}", member.getGroupId(), member.getCustId());
}
}
log.info("客群客户更新完成客群ID{},原数量:{},新数量:{},新增:{},删除:{}",
custGroup.getId(), existMembers.size(), newMemberList.size(), addCount, toDeleteKeys.size());
}
/**
* 异步导入客户
*/
private void doImportCustGroupByTemplate(CustGroup custGroup, MultipartFile file, String headId) {
try {
// 解析Excel
List<CustGroupMemberTemplate> templateList = EasyExcel.read(file.getInputStream())
.head(CustGroupMemberTemplate.class)
.sheet()
.doReadSync();
if (templateList.isEmpty()) {
throw new ServiceException("导入列表为空");
}
// 校验和去重
Set<String> uniqueCustIds = new HashSet<>();
List<CustGroupMember> memberList = new ArrayList<>();
for (CustGroupMemberTemplate template : templateList) {
// 校验客户类型
String custType = template.getCustType();
if (StringUtils.isEmpty(custType)) {
throw new ServiceException("客户类型不能为空");
}
// 转换客户类型值
String custTypeValue;
switch (custType) {
case "个人":
custTypeValue = "0";
break;
case "企业":
custTypeValue = "2";
break;
case "商户":
custTypeValue = "1";
break;
default:
throw new ServiceException("客户类型填写错误,只能填写:个人、企业、商户");
}
// 客户号不能为空
if (StringUtils.isEmpty(template.getCustId())) {
throw new ServiceException("客户号不能为空");
}
// 检查重复
if (!uniqueCustIds.add(template.getCustId())) {
continue; // 跳过重复客户
}
// 创建客户成员
CustGroupMember member = new CustGroupMember();
member.setGroupId(custGroup.getId());
member.setCustType(custTypeValue);
member.setCustId(template.getCustId());
member.setCustName(template.getCustName());
member.setCustIdc(template.getCustIdc());
member.setSocialCreditCode(template.getSocialCreditCode());
member.setCreateTime(new Date());
// 个人客户必须有身份证号
if ("0".equals(custTypeValue) && StringUtils.isEmpty(member.getCustIdc())) {
throw new ServiceException("个人客户[" + template.getCustId() + "]身份证号不能为空");
}
// 企业/商户客户必须有统信码
if (("1".equals(custTypeValue) || "2".equals(custTypeValue))
&& StringUtils.isEmpty(member.getSocialCreditCode())) {
throw new ServiceException("企业/商户客户[" + template.getCustId() + "]统信码不能为空");
}
memberList.add(member);
}
// 批量插入
int batchSize = 1000;
int successCount = 0;
int skippedCount = 0;
int restoredCount = 0;
for (int i = 0; i < memberList.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, memberList.size());
List<CustGroupMember> batchList = memberList.subList(i, endIndex);
for (CustGroupMember member : batchList) {
try {
custGroupMemberMapper.insert(member);
successCount++;
} catch (DuplicateKeyException e) {
// 客户已存在,检查是否是被手动移除的
LambdaQueryWrapper<CustGroupMember> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustGroupMember::getGroupId, member.getGroupId())
.eq(CustGroupMember::getCustId, member.getCustId())
.eq(CustGroupMember::getCustType, member.getCustType());
CustGroupMember existMember = custGroupMemberMapper.selectOne(queryWrapper);
if (existMember != null && existMember.getManualRemove() != null && existMember.getManualRemove() == 1) {
// 是被手动移除的客户,清除标记并恢复
existMember.setManualRemove(0);
existMember.setDelFlag(0);
existMember.setCustName(member.getCustName());
custGroupMemberMapper.updateById(existMember);
restoredCount++;
log.debug("恢复手动移除的客户groupId={}, custId={}", member.getGroupId(), member.getCustId());
} else {
// 正常存在的客户,跳过
skippedCount++;
log.debug("客户已存在跳过groupId={}, custId={}", member.getGroupId(), member.getCustId());
}
}
}
}
log.info("客群客户导入完成模板客群ID{},成功:{},跳过重复:{},恢复:{}",
custGroup.getId(), successCount, skippedCount, restoredCount);
// 更新创建状态为成功
custGroup.setCreateStatus("1");
custGroupMapper.updateById(custGroup);
} catch (Exception e) {
// 更新创建状态为失败
custGroup.setCreateStatus("2");
custGroupMapper.updateById(custGroup);
log.error("客群客户导入失败客群ID{}", custGroup.getId(), e);
throw new ServiceException("客群客户导入失败: " + e.getMessage());
}
}
/**
* 异步导入客户(网格方式)
*/
private void doImportCustGroupByGrid(GridImportDTO gridImportDTO, String headId) {
CustGroup custGroup = gridImportDTO.getCustGroup();
try {
List<CustGroupMember> memberList = new ArrayList<>();
String gridType = gridImportDTO.getGridType();
// 根据网格类型查询客户
if ("0".equals(gridType)) {
// 绩效网格
memberList.addAll(importFromCmpmGrid(custGroup, gridImportDTO, headId));
} else if ("1".equals(gridType)) {
// 地理网格
memberList.addAll(importFromRegionGrid(custGroup, gridImportDTO));
} else if ("2".equals(gridType)) {
// 绘制网格
memberList.addAll(importFromDrawGrid(custGroup, gridImportDTO));
}
if (memberList.isEmpty()) {
throw new ServiceException("未查询到任何客户");
}
// 批量插入
int batchSize = 1000;
int successCount = 0;
int skippedCount = 0;
int restoredCount = 0;
for (int i = 0; i < memberList.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, memberList.size());
List<CustGroupMember> batchList = memberList.subList(i, endIndex);
for (CustGroupMember member : batchList) {
try {
custGroupMemberMapper.insert(member);
successCount++;
} catch (DuplicateKeyException e) {
// 客户已存在,检查是否是被手动移除的
LambdaQueryWrapper<CustGroupMember> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustGroupMember::getGroupId, member.getGroupId())
.eq(CustGroupMember::getCustId, member.getCustId())
.eq(CustGroupMember::getCustType, member.getCustType());
CustGroupMember existMember = custGroupMemberMapper.selectOne(queryWrapper);
if (existMember != null && existMember.getManualRemove() != null && existMember.getManualRemove() == 1) {
// 是被手动移除的客户,清除标记并恢复
existMember.setManualRemove(0);
existMember.setDelFlag(0);
existMember.setCustName(member.getCustName());
custGroupMemberMapper.updateById(existMember);
restoredCount++;
log.debug("恢复手动移除的客户groupId={}, custId={}", member.getGroupId(), member.getCustId());
} else {
// 正常存在的客户,跳过
skippedCount++;
log.debug("客户已存在跳过groupId={}, custId={}", member.getGroupId(), member.getCustId());
}
}
}
}
log.info("客群客户导入完成网格客群ID{},成功:{},跳过重复:{},恢复:{}",
custGroup.getId(), successCount, skippedCount, restoredCount);
// 更新创建状态为成功
custGroup.setCreateStatus("1");
custGroupMapper.updateById(custGroup);
} catch (Exception e) {
// 更新创建状态为失败
custGroup.setCreateStatus("2");
custGroupMapper.updateById(custGroup);
log.error("客群客户导入失败客群ID{}", custGroup.getId(), e);
throw new ServiceException("客群客户导入失败: " + e.getMessage());
}
}
/**
* 从绩效网格导入客户
*/
private List<CustGroupMember> importFromCmpmGrid(CustGroup custGroup, GridImportDTO gridImportDTO, String headId) {
List<CustGroupMember> memberList = new ArrayList<>();
// 确定要查询的网格类型
List<String> gridTypes = new ArrayList<>();
String cmpmBizType = gridImportDTO.getCmpmBizType();
if ("retail".equals(cmpmBizType)) {
gridTypes.add("retail");
} else if ("corporate".equals(cmpmBizType)) {
gridTypes.add("corporate");
gridTypes.add("corporate_account");
} else {
throw new ServiceException("请选择绩效网格业务类型(零售/公司)");
}
// 查询客户
for (String userName : gridImportDTO.getUserNames()) {
for (String gridType : gridTypes) {
List<GridCmpm> cmpmList = gridCmpmMapper.getGridCmpmByUserName(userName, headId, gridType);
for (GridCmpm cmpm : cmpmList) {
CustGroupMember member = new CustGroupMember();
member.setGroupId(custGroup.getId());
member.setCustId(cmpm.getCustId());
member.setCustName(cmpm.getCustName());
member.setCustType(cmpm.getCustType());
member.setCustIdc(cmpm.getCustIdc());
member.setSocialCreditCode(cmpm.getUsci());
member.setCreateTime(new Date());
memberList.add(member);
}
}
}
return memberList;
}
/**
* 从地理网格导入客户
*/
private List<CustGroupMember> importFromRegionGrid(CustGroup custGroup, GridImportDTO gridImportDTO) {
List<CustGroupMember> memberList = new ArrayList<>();
// 查询地理网格获取编码
if (gridImportDTO.getRegionGridIds() == null || gridImportDTO.getRegionGridIds().isEmpty()) {
throw new ServiceException("请选择地理网格");
}
List<RegionGrid> regionGrids = regionGridMapper.selectBatchIds(gridImportDTO.getRegionGridIds());
// 使用 selectAllCustFromGrid 方法查询所有客户(不限制客户类型)
for (RegionGrid regionGrid : regionGrids) {
List<RegionCustUser> custUsers = regionGridListService.selectAllCustFromGrid(regionGrid);
for (RegionCustUser custUser : custUsers) {
CustGroupMember member = new CustGroupMember();
member.setGroupId(custGroup.getId());
member.setCustId(custUser.getCustId());
member.setCustName(custUser.getCustName());
member.setCustType(custUser.getCustType());
member.setCreateTime(new Date());
memberList.add(member);
}
}
return memberList;
}
/**
* 从绘制网格导入客户
*/
private List<CustGroupMember> importFromDrawGrid(CustGroup custGroup, GridImportDTO gridImportDTO) {
List<CustGroupMember> memberList = new ArrayList<>();
if (gridImportDTO.getDrawGridIds() == null || gridImportDTO.getDrawGridIds().isEmpty()) {
throw new ServiceException("请选择绘制网格");
}
// 查询绘制网格关联的图形ID
for (Long gridId : gridImportDTO.getDrawGridIds()) {
LambdaQueryWrapper<DrawGridShapeRelate> relateWrapper = new LambdaQueryWrapper<>();
relateWrapper.eq(DrawGridShapeRelate::getGridId, gridId);
List<DrawGridShapeRelate> relates = drawGridShapeRelateMapper.selectList(relateWrapper);
for (DrawGridShapeRelate relate : relates) {
// 根据图形ID查询客户
LambdaQueryWrapper<DrawShapeCust> custWrapper = new LambdaQueryWrapper<>();
custWrapper.eq(DrawShapeCust::getShapeId, relate.getShapeId());
List<DrawShapeCust> shapeCusts = drawShapeCustMapper.selectList(custWrapper);
for (DrawShapeCust shapeCust : shapeCusts) {
CustGroupMember member = new CustGroupMember();
member.setGroupId(custGroup.getId());
member.setCustId(shapeCust.getCustId());
member.setCustName(shapeCust.getCustName());
member.setCustType(shapeCust.getCustType());
member.setCreateTime(new Date());
memberList.add(member);
}
}
}
return memberList;
}
}

View File

@@ -0,0 +1,46 @@
<?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.group.mapper.CustGroupMapper">
<select id="selectCustGroupList" resultType="CustGroupVO">
SELECT
cg.id,
cg.group_name,
cg.group_mode,
cg.create_mode,
cg.user_name,
cg.nick_name,
cg.dept_id,
cg.share_enabled,
cg.share_dept_ids,
cg.group_status,
cg.create_by,
cg.create_time,
cg.update_by,
cg.update_time,
cg.remark,
cg.del_flag,
(SELECT COUNT(*) FROM ibs_cust_group_member cgm WHERE cgm.group_id = cg.id AND cgm.del_flag = '0') AS cust_count
FROM ibs_cust_group cg
<where>
cg.del_flag = '0'
and create_status = '1'
<if test="dto.groupName != null and dto.groupName != ''">
AND cg.group_name LIKE CONCAT('%', #{dto.groupName}, '%')
</if>
<if test="dto.groupMode != null and dto.groupMode != ''">
AND cg.group_mode = #{dto.groupMode}
</if>
<if test="dto.createMode != null and dto.createMode != ''">
AND cg.create_mode = #{dto.createMode}
</if>
<if test="dto.groupStatus != null and dto.groupStatus != ''">
AND cg.group_status = #{dto.groupStatus}
</if>
</where>
ORDER BY cg.create_time DESC
</select>
</mapper>

View File

@@ -97,6 +97,13 @@ public class GridCmpmController extends BaseController {
return gridCmpmService.selectCustManagerResult();
}
@GetMapping("/custManager/custLevel/list")
@Log(title = "绩效网格-管户报表星级列表")
@ApiOperation("管户报表星级列表")
public AjaxResult getCustLevelListForManager() {
return success(gridCmpmService.getCustLevelListForManager());
}
@GetMapping("/custLevel/count")
@Log(title = "绩效网格-查询客户分层等级")
@ApiOperation("查询客户分层等级")

View File

@@ -18,4 +18,8 @@ public class CustManagerDTO {
@ApiModelProperty(value = "网点名",notes = "")
private String branchId;
/** 状态类型current本月、last上月、rise上升、fall下降 */
@ApiModelProperty(value = "状态类型", notes = "current本月、last上月、rise上升、fall下降")
private String statusType;
}

View File

@@ -152,6 +152,13 @@ public class GridCmpmService {
return new DwbRetailResultVO();
}
/**
* 获取管户报表星级列表
*/
public List<String> getCustLevelListForManager() {
return gridCmpmMapper.getCustLevelList();
}
/**
* 每月定时设置统计数据
*/
@@ -226,7 +233,6 @@ public class GridCmpmService {
// 获取上月各星级客户数
Map<String, Integer> historyLevelCountMap = getCustLevelCountMap(custManagerDTO, "last");
Map<String, Integer> custLevelCompLm = calculateLevelChanges(historyLevelCountMap, currentLevelCountMap);
vo.setCustLevelCompLm(custLevelCompLm);
// 在 vo.setCustLevelCompLm 之前,按顺序重新组织数据
String[] order = {"5星", "4星", "3星", "2星", "1星", "基础", "长尾"};

View File

@@ -35,4 +35,12 @@ public interface RegionGridListService {
List<RegionGridListVO> getSecGridListByManager(RegionGridListDTO regionGridListDTO);
/**
* 查询网格内所有客户(不限制客户类型,用于导入客群)
* @param regionGrid 地理网格对象
* @return 网格内所有客户列表
*/
List<RegionCustUser> selectAllCustFromGrid(RegionGrid regionGrid);
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.ibs.grid.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.ibs.draw.domain.dto.grid.CustCountDTO;
@@ -429,4 +430,25 @@ public class RegionGridListServiceImpl implements RegionGridListService {
return secGridListByManager;
}
/**
* 查询网格内所有客户(不限制客户类型,用于导入客群)
* @param regionGrid 地理网格对象
* @return 网格内所有客户列表
*/
@Override
public List<RegionCustUser> selectAllCustFromGrid(RegionGrid regionGrid) {
LambdaQueryWrapper<RegionCustUser> queryWrapper = new LambdaQueryWrapper<>();
// 根据网格等级判断使用一级还是二级网格ID查询
if (regionGrid.getGridLevel().equals("1")) {
queryWrapper.eq(RegionCustUser::getTopGridId, regionGrid.getGridId());
} else if (regionGrid.getGridLevel().equals("2")) {
queryWrapper.eq(RegionCustUser::getSecGridId, regionGrid.getGridId());
} else {
throw new ServiceException("无效的网格等级: " + regionGrid.getGridLevel());
}
// 不限制客户类型,查询所有类型的客户
return regionCustUserMapper.selectList(queryWrapper);
}
}

View File

@@ -142,6 +142,28 @@
<if test="managerId != null and managerId !='' ">and manager_id = #{managerId}</if>
<if test="outletId != null and outletId !='' ">and outlet_id = #{outletId}</if>
<if test="branchId != null and branchId !='' ">and branch_id = #{branchId}</if>
<if test="statusType != null and statusType !=''">
<choose>
<when test="statusType == 'current'">
<!-- 本月:本月是指定星级的客户 -->
and cust_level = #{custLevel}
</when>
<when test="statusType == 'last'">
<!-- 上月:上月是指定星级的客户 -->
and cust_level_lm = #{custLevel}
</when>
<when test="statusType == 'rise'">
<!-- 上升:上升到指定星级的客户(本月是指定星级,上月星级更低) -->
and cust_level = #{custLevel}
and cust_level_comp_lm like '上升%'
</when>
<when test="statusType == 'fall'">
<!-- 下降:从指定星级下降的客户(上月是指定星级,本月星级更低) -->
and cust_level_lm = #{custLevel}
and cust_level_comp_lm like '下降%'
</when>
</choose>
</if>
</where>
</select>
@@ -438,6 +460,7 @@
select distinct
cust_level
from dwb_retail_cust_level_manager_detail_875
order by cust_level
</select>
<select id="getCustCountByLevel" resultType="int">

View File

@@ -226,6 +226,7 @@
<module>ruoyi-generator</module>
<module>ruoyi-common</module>
<module>ibs</module>
<module>ibs-group</module>
</modules>
<packaging>pom</packaging>

View File

@@ -74,6 +74,13 @@
<version>${ruoyi.version}</version>
</dependency>
<!-- 客群管理-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ibs-group</artifactId>
<version>${ruoyi.version}</version>
</dependency>
</dependencies>
<build>

View File

@@ -79,7 +79,7 @@ spring:
# 地址
host: 116.62.17.81
# 端口默认为6379
port: 6380
port: 6379
# 数据库索引
database: 0
# 密码

View File

@@ -100,50 +100,6 @@ public class SysLoginService
return tokenService.createToken(loginUser);
}
/**
* 测试登录
*
* @param username 用户名
* @param password 密码
* @return 结果
*/
public String login(String username, String password)
{
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
finally
{
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
/**
* 校验验证码
*

View File

@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = DEV支行数智管理平台系统
VUE_APP_TITLE = UAT支行数智管理平台系统
# 开发环境配置
ENV = 'development'

View File

@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = UAT支行数智管理平台系统
VUE_APP_TITLE = PRE支行数智管理平台系统
NODE_ENV = staging

4
ruoyi-ui/.gitignore vendored
View File

@@ -21,6 +21,4 @@ selenium-debug.log
dist.zip
package-lock.json
yarn.lock
vue.config.js
yarn.lock

View File

@@ -18,7 +18,7 @@
</style>
<script>
// 这里一定要配置ak 否则无法使用
window.BMAP_AUTHENTIC_KEY = "mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"
window.BMAP_AUTHENTIC_KEY = "t6k6UC2IZR40Un8kkqM4RXlaQb4FulyM"
</script>
<script>
const result = new URLSearchParams(window.location.search)

View File

@@ -18,7 +18,7 @@
</style>
<script>
// 这里一定要配置ak 否则无法使用
window.BMAP_AUTHENTIC_KEY = "mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"
window.BMAP_AUTHENTIC_KEY = "t6k6UC2IZR40Un8kkqM4RXlaQb4FulyM"
</script>
<script>

View File

@@ -18,7 +18,7 @@
</style>
<script>
// 这里一定要配置ak 否则无法使用
window.BMAP_AUTHENTIC_KEY = "mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"
window.BMAP_AUTHENTIC_KEY = "t6k6UC2IZR40Un8kkqM4RXlaQb4FulyM"
</script>
<script>
const result = new URLSearchParams(window.location.search)

View File

@@ -23,7 +23,7 @@
</style>
<script>
// 这里一定要配置ak 否则无法使用
window.BMAP_AUTHENTIC_KEY = "mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"
window.BMAP_AUTHENTIC_KEY = "t6k6UC2IZR40Un8kkqM4RXlaQb4FulyM"
</script>
<script type="text/javascript" src="./script/getscript.js?type=webgl&v=1.0&services=&t=20230529114224"></script>
<!-- 引入logisticsgl的sdk -->

View File

@@ -39,7 +39,7 @@
<script>
const sdk = new QuHuaSdk({
ak: 'L7KaAZUYPVSD40nYT09rWWgIdZKUesiX',
webAk: 'mokVj0S4sGE9av6NBwy8WHY0xnQsucbE',
webAk: 't6k6UC2IZR40Un8kkqM4RXlaQb4FulyM',
domId: 'box',
defaultCenterCity: "杭州市", // 非必填
_baseUrl: window.NODE_ENV === "production" ? "http://64.202.32.20:5001/logisticsWeb-quhua-intranet" : "http://158.234.96.76:5001/logisticsWeb-quhua-intranet", // 固定格式,必填

View File

@@ -19,7 +19,7 @@
</style>
<script>
// 这里一定要配置ak 否则无法使用
window.BMAP_AUTHENTIC_KEY = "mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"
window.BMAP_AUTHENTIC_KEY = "t6k6UC2IZR40Un8kkqM4RXlaQb4FulyM"
</script>
<script>
const result = new URLSearchParams(window.location.search)

View File

@@ -34,7 +34,7 @@
</style>
<script>
// 这里一定要配置ak 否则无法使用
window.BMAP_AUTHENTIC_KEY = "mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"
window.BMAP_AUTHENTIC_KEY = "t6k6UC2IZR40Un8kkqM4RXlaQb4FulyM"
</script>
<script>

View File

@@ -19,7 +19,7 @@
</style>
<script>
// 这里一定要配置ak 否则无法使用
window.BMAP_AUTHENTIC_KEY = "mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"
window.BMAP_AUTHENTIC_KEY = "t6k6UC2IZR40Un8kkqM4RXlaQb4FulyM"
</script>
<script>
const result = new URLSearchParams(window.location.search)

View File

@@ -8,10 +8,9 @@
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script>
// 这里一定要配置ak 否则无法使用
window.BMAP_AUTHENTIC_KEY = "mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"
window.BMAP_AUTHENTIC_KEY = "t6k6UC2IZR40Un8kkqM4RXlaQb4FulyM"
</script>
<script type="text/javascript" src="<%= VUE_APP_BAIDU_PATH %>"></script>
<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=mokVj0S4sGE9av6NBwy8WHY0xnQsucbE"></script> -->
<link rel="stylesheet" type="text/css" href="<%= VUE_APP_BAIDU_CSS_PATH %>" />
<script type="text/javascript" src="\baidu\script\index.umd.min.js"></script>

View File

@@ -14,4 +14,12 @@ export function gridCmpmCustManagerResult(data) {
method: 'get',
params: data
})
}
// 获取管户报表星级列表
export function getCustLevelList() {
return request({
url: '/grid/cmpm/custManager/custLevel/list',
method: 'get'
})
}

View File

@@ -237,14 +237,6 @@
@click.stop="updateAreaInfo"
/>
</el-tooltip>
<!-- 查看客户 -->
<el-tooltip placement="top" effect="light" content="查看客户">
<i
class="el-icon-user icon-area"
@click.stop="previewCustomer"
/>
</el-tooltip>
<!-- 删除区域 -->
<el-tooltip placement="top" effect="light" content="删除区域">
<el-popconfirm
title="确定删除吗?"
@@ -279,13 +271,6 @@
</div>
</div>
</transition>
<!-- 查看客户模态框 -->
<customer-modal
ref="customerModal"
cardType="featured"
:detailInfo="areaForm"
:btnType="'2'"
/>
<div class="search-box">
<el-form size="small" class="myForm">
<el-form-item style="width: 130px">
@@ -352,7 +337,6 @@ import { businessBelongList } from '@/views/grid/create/utils'
import { mapGetters } from 'vuex'
import { Message } from 'element-ui'
import { isEmpty } from 'lodash'
import CustomerModal from '@/views/grid/map/draw-area/customer-modal.vue'
const polygonOptions = {
strokeColor: '#5E87DB',
strokeWeight: 3,
@@ -371,9 +355,6 @@ const labelOptions = {
}
export default {
name: 'BMapPolygonEditor',
components: {
CustomerModal
},
props: ['layerInfo'],
data() {
return {
@@ -1226,12 +1207,6 @@ export default {
this.$store.dispatch('setIsDrawing', true)
this.currentPolygon.enableEditing()
},
/**
* 查看客户
*/
previewCustomer() {
this.$refs.customerModal.onOpen()
},
setForm(data) {
this.areaForm = data
this.visible = true

View File

@@ -35,7 +35,7 @@ export default {
initSdk(){
this.sdk = new QuHuaSdk({
ak: 'L7KaAZUYPVSD40nYT09rWWgIdZKUesiX',
webAk: 'mokVj0S4sGE9av6NBwy8WHY0xnQsucbE',
webAk: 't6k6UC2IZR40Un8kkqM4RXlaQb4FulyM',
domId: 'box',
defaultCenterCity: "杭州市", // 非必填
_baseUrl: "http://158.234.96.76:5001/logisticsWeb-quhua-intranet", // 固定格式,必填
@@ -137,7 +137,7 @@ export default {
onChange(){
this.sdk = new QuHuaSdk({
ak: 'L7KaAZUYPVSD40nYT09rWWgIdZKUesiX',
webAk: 'mokVj0S4sGE9av6NBwy8WHY0xnQsucbE',
webAk: 't6k6UC2IZR40Un8kkqM4RXlaQb4FulyM',
domId: 'box',
defaultCenterCity: "杭州市", // 非必填
_baseUrl: "http://158.234.96.76:5001/logisticsWeb-quhua-intranet", // 固定格式,必填

View File

@@ -1,12 +1,13 @@
<template>
<div>
<!-- 统计卡片 -->
<div :gutter="24" class="sum-box">
<el-card class="box-card">
<div class="my-span-checklist-title">
总资产余额
</div>
<div class="my-span-checklist-main">
<span>{{ cardInfo.custAumBal }}</span>
<span>{{ formatYi(cardInfo.custAumBal) }}</span>
</div>
</el-card>
<el-card class="box-card">
@@ -14,7 +15,7 @@
总资产余额较上月变动
</div>
<div class="my-span-checklist-main">
<span>{{ cardInfo.aumBalCompLm }}</span>
<span>{{ formatYi(cardInfo.aumBalCompLm) }}</span>
</div>
</el-card>
<el-card class="box-card">
@@ -22,7 +23,7 @@
总资产余额月日均
</div>
<div class="my-span-checklist-main">
<span>{{ cardInfo.custAumMonthAvg }}</span>
<span>{{ formatYi(cardInfo.custAumMonthAvg) }}</span>
</div>
</el-card>
<el-card class="box-card">
@@ -66,6 +67,33 @@
</div>
</el-card>
</div>
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="90px">
<el-form-item label="客户星级" prop="custLevel">
<el-select v-model="queryParams.custLevel" placeholder="请选择" clearable style="width: 150px">
<el-option
v-for="level in custLevelOptions"
:key="level"
:label="level"
:value="level"
/>
</el-select>
</el-form-item>
<el-form-item label="星级状态" prop="statusType">
<el-select v-model="queryParams.statusType" placeholder="请选择" clearable style="width: 150px">
<el-option label="本月" value="current" />
<el-option label="上月" value="last" />
<el-option label="上升" value="rise" />
<el-option label="下降" value="fall" />
</el-select>
</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-card class="header-statics" style="display: none">
<ul class="statics-cnt">
<li>
@@ -122,7 +150,7 @@
</template>
<script>
import { gridCmpmCustManagerList, gridCmpmCustManagerResult } from '@/api/gridSearch/accountManageReport/index'
import { gridCmpmCustManagerList, gridCmpmCustManagerResult, getCustLevelList } from '@/api/gridSearch/accountManageReport/index'
export default {
data() {
return {
@@ -137,19 +165,50 @@ import { gridCmpmCustManagerList, gridCmpmCustManagerResult } from '@/api/gridSe
},
tableData: [],
loading: false,
custLevelOptions: [],
queryParams: {
pageNum: 1,
pageSize: 10
pageSize: 10,
custLevel: null,
statusType: null
},
total: 0,
topData: []
}
},
mounted() {
// 从 URL 参数获取筛选条件
const { custLevel, statusType } = this.$route.query
if (custLevel) {
this.queryParams.custLevel = custLevel
}
if (statusType) {
this.queryParams.statusType = statusType
}
this.loadCustLevelOptions()
this.getData()
this.getSum()
},
methods: {
/** 加载客户星级选项 */
loadCustLevelOptions() {
getCustLevelList().then(res => {
this.custLevelOptions = res.data || []
}).catch(() => {
this.custLevelOptions = []
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getData()
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams.custLevel = null
this.queryParams.statusType = null
this.handleQuery()
},
getData() {
this.loading = true;
gridCmpmCustManagerList(this.queryParams).then(res => {
@@ -183,10 +242,21 @@ import { gridCmpmCustManagerList, gridCmpmCustManagerResult } from '@/api/gridSe
},
toSum(obj) {
let sum = 0
Object.keys(obj).map(key => {
sum = sum + obj[key] * 1
Object.keys(obj).forEach(key => {
// 只计算2星级及以上的key中包含"2星"、"3星"、"4星"、"5星"
if (key.includes('2星') || key.includes('3星') || key.includes('4星') || key.includes('5星')) {
sum += obj[key]
}
})
return sum
},
/** 格式化为亿元 */
formatYi(value) {
if (value === null || value === '' || value === undefined) return '-'
const num = parseFloat(value)
if (isNaN(num)) return '-'
// 换算为亿元保留2位小数
return (num / 100000000).toFixed(2) + '亿元'
}
}
}

View File

@@ -18,9 +18,9 @@
</div>
<div class="top_num">
<span style="font-size: 14px;">较上月变动</span>
<span v-if="String(item.inc).includes('-')" style=" font-size: 14px;color: #EF3F35">{{ changeData(item.inc)
<span v-if="String(item.inc).includes('-')" style=" font-size: 14px;color: #00B453">{{ changeData(item.inc)
}}<i class="el-icon-caret-bottom"></i></span>
<span v-else style=" font-size: 14px;color: #00B453">{{ changeData(item.inc) }}<i
<span v-else style=" font-size: 14px;color: #EF3F35">{{ changeData(item.inc) }}<i
class="el-icon-caret-top"></i></span>
</div>
</div>
@@ -30,18 +30,24 @@
<div class="vt-main top-vt-main">
<div class="top_num">
<span>上月值</span>
<span style="font-size: 14px;">{{ item.yiAmt }}</span>
<span style="font-size: 14px; cursor: pointer; color: #409EFF;" @click="goToCustManager(item.itemNm, 'last')">{{ item.yiAmt }}</span>
</div>
<div class="top_num">
<span>当月值</span>
<span style="font-size: 14px;">{{ item.inc }}</span>
<span style="font-size: 14px; cursor: pointer; color: #409EFF;" @click="goToCustManager(item.itemNm, 'current')">{{ item.inc }}</span>
</div>
<div class="top_num">
<span style="font-size: 14px;">较上月变动</span>
<span v-if="String(item.curAmt).includes('-')" style=" font-size: 14px;color: #EF3F35">{{ item.curAmt
}}<i class="el-icon-caret-bottom"></i></span>
<span v-else style=" font-size: 14px;color: #00B453">{{ item.curAmt }}<i
class="el-icon-caret-top"></i></span>
<span v-if="String(item.curAmt).includes('-')"
style="font-size: 14px; color: #00B453; cursor: pointer;"
@click="goToCustManager(item.itemNm, 'fall')">
{{ item.curAmt }}<i class="el-icon-caret-bottom"></i>
</span>
<span v-else
style="font-size: 14px; color: #EF3F35; cursor: pointer;"
@click="goToCustManager(item.itemNm, 'rise')">
{{ item.curAmt }}<i class="el-icon-caret-top"></i>
</span>
</div>
</div>
</div>
@@ -875,6 +881,23 @@ export default {
}
},
methods: {
/** 跳转到管户报表页面 */
goToCustManager(itemNm, statusType) {
// 从 itemNm 中提取星级,如 "3星客户" → "3星""基础客户" → "基础"
// 使用正则匹配以"星"或"基础"或"长尾"开头的内容
let custLevel = itemNm.replace('客户', '').trim()
// 如果没有"客户"后缀,直接使用整个名称
if (custLevel === itemNm) {
custLevel = itemNm
}
this.$router.push({
path: '/gridSearch/accountManageReport',
query: {
custLevel: custLevel,
statusType: statusType
}
})
},
getData() {
this.loading = true
if (this.selectedTab === '3') {