Compare commits

...

2 Commits

Author SHA1 Message Date
e3e26574c6 Merge branch 'master-yly'
# Conflicts:
#	CLAUDE.md
2026-03-18 16:43:40 +08:00
5996173abd 0318-海宁菜单调整+北仑客群部分开发 2026-03-18 16:39:23 +08:00
61 changed files with 3343 additions and 633 deletions

313
CLAUDE.md
View File

@@ -2,215 +2,186 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
## Project Overview
**数字支行辅助管理系统(IBS)** - 基于 若依框架 v3.8.8 的前后端分离全栈项目,专注于银行支行的网格化营销、客户管理和走访业务。
This is a full-stack banking management system (数字支行辅助管理系统) built on the RuoYi framework v3.8.8. It provides grid-based customer relationship management, customer grouping, visit tracking, and performance statistics for banking institutions.
## 常用命令
- **Backend**: Spring Boot 2.5.14 + MyBatis + MySQL + Redis + JWT
- **Frontend**: Vue 2.6.12 + Element UI 2.15.14
- **Java Version**: 1.8
### 后端开发
## Architecture
```bash
# Maven 打包(跳过测试)
mvn clean package -Dmaven.test.skip=true
### Backend Module Structure
# 运行已打包的 JAR
cd ruoyi-admin/target
java -jar -Xms256m -Xmx1024m ruoyi-admin.jar
The backend is a multi-module Maven project with the following modules:
# 后端服务地址
http://localhost:8080
# Swagger API 文档
http://localhost:8080/swagger-ui/index.html
# 测试登录接口获取 token
POST /login/test?username=admin&password=admin123
```
ruoyi/
├── ruoyi-admin/ # Web entry point, main application (RuoYiApplication)
├── ruoyi-framework/ # Core framework (security, config, interceptors)
├── ruoyi-system/ # System management (users, roles, menus, depts)
├── ruoyi-common/ # Common utilities, annotations, domain classes
├── ruoyi-quartz/ # Scheduled task management
├── ruoyi-generator/ # Code generation tools
├── ibs/ # Main business module: grid customer management (网格营销)
└── ibs-group/ # Customer group management module (客户分组)
```
### 前端开发
### Business Domain Structure
Each business module (e.g., `ibs`, `ibs-group`) follows this package structure:
```
com.ruoyi.<module>/
├── controller/ # REST controllers (handle HTTP requests)
├── service/ # Business logic layer
├── mapper/ # MyBatis mappers (database access)
├── domain/
│ ├── entity/ # Database entities
│ ├── dto/ # Data Transfer Objects (request)
│ └── vo/ # View Objects (response)
└── handler/ # Custom MyBatis type handlers
```
### Frontend Structure
```
ruoyi-ui/
├── src/
│ ├── api/ # API request modules (organized by feature)
│ ├── views/ # Page components
│ ├── components/ # Reusable components
│ ├── store/ # Vuex state management
│ ├── router/ # Vue Router configuration
│ ├── utils/ # Utility functions
│ └── directive/ # Custom Vue directives
```
## Build and Run Commands
### Backend (Maven)
```bash
# Clean build artifacts
cd bin && clean.bat
# Package the project (creates JAR in ruoyi-admin/target/)
cd bin && package.bat
# Run the backend server (requires packaged JAR)
cd bin && run.bat
# Or run directly with Maven from ruoyi-admin/
mvn spring-boot:run
# The main class is: com.ruoyi.RuoYiApplication
# Default port: 8080
```
### Frontend (npm/Vue CLI)
```bash
cd ruoyi-ui
# 安装依赖
npm install
# 开发环境运行(端口 80,代理到 localhost:8080)
# Development server (runs on port 80)
npm run dev
# 生产环境构建
# For older Node.js versions with OpenSSL issues
npm run dev_t
# Build for production
npm run build:prod
# 代码检查
# Build for staging
npm run build:stage
# Build for pre-production
npm run build:pre
# Lint code
npm run lint
```
### 数据库连接
## Development Configuration
```bash
# 通过 MCP MySQL 工具连接
# 地址: 116.62.17.81:3306
# 数据库: ibs
# 用户名: root
```
### Application Profiles
## 核心架构
Backend uses Spring profiles located in `ruoyi-admin/src/main/resources/`:
- `application.yml` - Base configuration
- `application-dev.yml` - Development environment
- `application-uat.yml` - UAT environment
- `application-pre.yml` - Pre-production
- `application-pro.yml` - Production
### 后端模块结构
Active profile is set in `application.yml` (default: `dev`).
```
ruoyi-admin/ # 主入口模块,包含启动类和配置文件
ruoyi-framework/ # 框架核心:安全配置、缓存、数据源等
ruoyi-system/ # 系统管理:用户、角色、菜单、字典等
ruoyi-common/ # 通用工具:工具类、注解、常量等
ruoyi-quartz/ # 定时任务模块
ruoyi-generator/ # 代码生成器
ibs/ # ★ 业务模块:数字支行核心业务 ★
```
### Frontend API Proxy
### IBS 业务模块 (核心业务)
The Vue dev server proxies API requests to `http://localhost:8080`:
- Frontend dev server: `http://localhost:80`
- API requests: `/dev-api/*``http://localhost:8080/*`
位置: `ibs/src/main/java/com/ruoyi/ibs/`
## Key Patterns and Conventions
**主要业务包:**
### Backend Code Patterns
| 包名 | 功能 | 说明 |
|------|------|------|
| `grid` | 网格管理 | 支行网格划分、分配、统计 |
| `cmpm` | 客户经理管理 | 客户经理信息维护 |
| `list` | 客户列表管理 | 零售/商户/企业客户管理 |
| `visit` | 走访管理 | 走访任务、记录、轨迹 |
| `task` | 任务管理 | 营销任务分配和跟踪 |
| `draw` | 绘图/网格绘制 | 基于百度地图的网格绘制 |
| `custmap` | 客户地图 | 客户地理分布可视化 |
| `dashboard` | 仪表盘 | 数据统计和展示 |
| `datavisual` | 数据可视化 | 报表和图表 |
| `rules` | 规则配置 | 业务规则配置 |
| `qxhy` | 青县惠银接口 | 外部系统对接 |
| `websocket` | WebSocket通信 | 实时通信支持 |
1. **Controller-Service-Mapper Layering**
- Controllers handle HTTP requests/responses, use `@RestController`
- Services contain business logic, use `@Service`
- Mappers use MyBatis annotations or XML files in `resources/mapper/`
**业务模块命名规范:**
- 新建模块命名: `ibs` + 主要功能(如 `ibs-grid`, `ibs-customer`)
- Controller 放在新建模块中,不与若依框架混合
- Entity 使用 `@Data` 注解
- Service 使用 `@Resource` 注解,不继承 `ServiceImpl`
- DAO 使用 MyBatis Plus,复杂操作在 XML 中编写 SQL
2. **DTO/VO Pattern**
- DTOs (Data Transfer Objects) for incoming requests
- VOs (View Objects) for outgoing responses
- Entities map directly to database tables
### 前端结构
3. **Security & Authentication**
- JWT-based authentication via `TokenService`
- Role-based access: `head`, `branch`, `outlet`, `manager`
- Use `SecurityUtils` for current user context: `SecurityUtils.getUsername()`, `SecurityUtils.getDeptId()`, `SecurityUtils.userRole()`
```
ruoyi-ui/src/
├── api/ # API 接口定义
├── views/ # 页面视图
│ ├── grid/ # 网格管理相关页面
│ ├── customer/ # 客户管理
│ ├── taskManage/ # 任务管理
│ ├── dashboard/ # 仪表盘
│ └── ...
├── components/ # 公共组件
├── map/ # 地图相关(百度地图集成)
├── store/ # Vuex 状态管理
└── router/ # 路由配置
```
4. **Pagination**
- Uses `PageHelper` for database pagination
- Controllers return `TableDataInfo` with `total` and `rows`
### 数据库表命名规范
5. **Caching**
- Uses `RedisCache` for Redis operations
- Cache keys often follow pattern: `{module}:{feature}:{key}`
- 新建表需加项目前缀: `ibs_` + 表名
- 示例: `ibs_grid`, `ibs_customer`, `ibs_visit_record`
### Frontend Code Patterns
## 关键技术点
1. **API Modules**
- Each feature has a dedicated API file in `src/api/`
- Uses `request()` wrapper around axios
- API base URL is configured in `src/utils/request.js`
### 1. 地图集成
2. **Vuex Store**
- Modules in `src/store/modules/`: user, app, permission, settings, tagsView
- State persists via `vuex-persistedstate`
项目深度集成百度地图 API,用于:
- 网格绘制和编辑
- 客户地理位置标注
- 走访轨迹记录
- 客户分布热力图
3. **Permission Directives**
- `v-hasPermi` for button-level permissions
- `v-hasRole` for role-based display
**相关配置:**
- 百度地图 AK 在前端配置
- 使用 JTS 库进行地理空间计算
## Common Business Concepts
### 2. 批量导入优化
- **网格**: Grid-based territory management for customer assignment
- **客户经理**: Relationship managers assigned to customers
- **客户星级**: Customer rating levels (5星, 4星, 3星, 2星, 1星, 基础, 长尾)
- **AUM (Asset Under Management)**: Customer assets, tracked with monthly averages
- **PAD走访**: Mobile visit records for customer interactions
设计批量导入功能时:
- 使用批量操作提高响应速度
- 导入结果只展示失败数据,不展示成功数据
- 使用 EasyExcel 处理 Excel
## MyBatis Configuration
### 3. 多端支持
- Mapper XML files: `classpath*:mapper/**/*Mapper.xml`
- Type aliases: `com.ruoyi.**.domain`
- Custom type handlers: `com.ruoyi.ibs.handler`
- Pagination: `pagehelper` with MySQL dialect
- PC 端: 主要管理和配置功能
- PAD 端: 走访记录功能(移动端)
## Notes
### 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/` 目录
- The project uses Chinese comments and variable names in many places
- Scheduled tasks use Quartz via `ruoyi-quartz` module
- File upload path configured in `application.yml`: `ruoyi.profile`
- Swagger/Knife4j API docs available when running

View File

@@ -50,14 +50,14 @@ public class CustGroupController extends BaseController {
}
/**
* 获取客群详情
* 根据ID查询客群详情
*/
@ApiOperation("获取客群详情")
@Log(title = "客群管理-获取客群详情")
@ApiOperation("根据ID查询客群详情")
@Log(title = "客群管理-查询客群详情")
@GetMapping("/{id}")
public AjaxResult getCustGroup(@PathVariable Long id) {
CustGroupVO vo = custGroupService.getCustGroup(id);
return AjaxResult.success(vo);
CustGroupVO custGroup = custGroupService.getCustGroup(id);
return AjaxResult.success(custGroup);
}
/**
@@ -83,17 +83,6 @@ public class CustGroupController extends BaseController {
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);
}
/**
* 更新客群(网格导入)
*/
@@ -150,15 +139,4 @@ public class CustGroupController extends BaseController {
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,54 @@
package com.ruoyi.group.controller;
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.group.domain.dto.CustGroupMemberQueryDTO;
import com.ruoyi.group.domain.vo.CustGroupMemberVO;
import com.ruoyi.group.service.ICustGroupMemberService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 客群客户Controller
*
* @author ruoyi
*/
@Api(tags = "客群客户接口")
@RestController
@RequestMapping("/group/member")
public class CustGroupMemberController extends BaseController {
@Resource
private ICustGroupMemberService custGroupMemberService;
/**
* 分页查询客群客户列表
*/
@ApiOperation("分页查询客群客户列表")
@Log(title = "客群客户-查询客户列表")
@GetMapping("/list/{groupId}")
public TableDataInfo listCustGroupMembers(@PathVariable Long groupId,
CustGroupMemberQueryDTO dto) {
startPage();
List<CustGroupMemberVO> list = custGroupMemberService.listCustGroupMembers(groupId, dto);
return getDataTable(list);
}
/**
* 手动移除客群客户
*/
@ApiOperation("手动移除客群客户")
@Log(title = "客群客户-手动移除客户", businessType = BusinessType.DELETE)
@PostMapping("/remove")
public AjaxResult removeMembers(@RequestParam Long groupId, @RequestBody List<Long> memberIds) {
String result = custGroupMemberService.removeMembers(groupId, memberIds);
return AjaxResult.success(result);
}
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.group.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 客群客户查询DTO
*
* @author ruoyi
*/
@Data
@ApiModel(description = "客群客户查询条件")
public class CustGroupMemberQueryDTO {
/**
* 客户类型0=个人, 1=商户, 2=企业
*/
@ApiModelProperty(value = "客户类型")
private String custType;
/**
* 客户姓名
*/
@ApiModelProperty(value = "客户姓名")
private String custName;
}

View File

@@ -59,7 +59,6 @@ public class CustGroup {
* 所属机构ID
*/
@ApiModelProperty(value = "所属机构ID", name = "deptId")
@TableField(fill = FieldFill.INSERT)
private Long deptId;
/**
@@ -74,13 +73,6 @@ public class CustGroup {
@ApiModelProperty(value = "可见部门ID列表逗号分隔", name = "shareDeptIds")
private String shareDeptIds;
/**
* 共享部门ID列表非表字段用于接收前端传参
*/
@ApiModelProperty(value = "共享部门ID列表", name = "shareDeptIdList")
@TableField(exist = false)
private List<Long> shareDeptIdList;
/**
* 客群状态0=正常, 1=已禁用
*/
@@ -109,14 +101,12 @@ public class CustGroup {
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**

View File

@@ -63,14 +63,12 @@ public class CustGroupMember {
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**

View File

@@ -65,10 +65,10 @@ public class CustGroupVO {
private Integer shareEnabled;
/**
* 可见部门ID列表
* 可见部门ID列表(逗号分隔)
*/
@ApiModelProperty(value = "可见部门ID列表", name = "shareDeptIds")
private List<Long> shareDeptIds;
@ApiModelProperty(value = "可见部门ID列表(逗号分隔)", name = "shareDeptIds")
private String shareDeptIds;
/**
* 客群状态0=正常, 1=已禁用
@@ -114,6 +114,49 @@ public class CustGroupVO {
@ApiModelProperty(value = "备注", name = "remark")
private String remark;
/**
* 有效期截止时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "有效期截止时间", name = "validTime")
private Date validTime;
/**
* 创建状态0=创建中, 1=创建成功, 2=创建失败
*/
@ApiModelProperty(value = "创建状态0=创建中, 1=创建成功, 2=创建失败", name = "createStatus")
private String createStatus;
/**
* 网格类型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;
/**
* 客户列表
*/

View File

@@ -4,6 +4,7 @@ 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.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -13,6 +14,7 @@ import java.util.List;
*
* @author ruoyi
*/
@Mapper
public interface CustGroupMapper extends BaseMapper<CustGroup> {
/**
@@ -31,4 +33,12 @@ public interface CustGroupMapper extends BaseMapper<CustGroup> {
* @return 客群VO列表
*/
List<CustGroupVO> selectCustGroupList(@Param("dto") CustGroupQueryDTO dto);
/**
* 根据ID查询客群详情
*
* @param id 客群ID
* @return 客群VO
*/
CustGroupVO selectCustGroupById(@Param("id") Long id);
}

View File

@@ -1,21 +1,36 @@
package com.ruoyi.group.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.group.domain.dto.CustGroupMemberQueryDTO;
import com.ruoyi.group.domain.entity.CustGroupMember;
import com.ruoyi.group.domain.vo.CustGroupMemberVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 客群客户关联Mapper接口
*
* @author ruoyi
*/
@Mapper
public interface CustGroupMemberMapper extends BaseMapper<CustGroupMember> {
/**
* 查询客群客户数量
* 分页查询客群客户列表
*
* @param groupId 客群ID
* @return 数量
* @param dto 查询条件
* @return 客户列表
*/
Long countByGroupId(@Param("groupId") Long groupId);
List<CustGroupMemberVO> selectCustGroupMemberList(@Param("groupId") Long groupId,
@Param("dto") CustGroupMemberQueryDTO dto);
/**
* 批量插入客群客户INSERT IGNORE遇到重复键自动跳过
*
* @param memberList 客户列表
*/
void batchInsertMembers(@Param("list") List<CustGroupMember> memberList);
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.group.service;
import com.ruoyi.group.domain.dto.CustGroupMemberQueryDTO;
import com.ruoyi.group.domain.vo.CustGroupMemberVO;
import java.util.List;
/**
* 客群客户服务接口
*
* @author ruoyi
*/
public interface ICustGroupMemberService {
/**
* 分页查询客群客户列表
*
* @param groupId 客群ID
* @param dto 查询条件
* @return 客户列表
*/
List<CustGroupMemberVO> listCustGroupMembers(Long groupId, CustGroupMemberQueryDTO dto);
/**
* 手动移除客群客户
*
* @param groupId 客群ID
* @param memberIds 客户ID列表
* @return 结果
*/
String removeMembers(Long groupId, List<Long> memberIds);
}

View File

@@ -23,6 +23,14 @@ public interface ICustGroupService {
*/
List<CustGroupVO> listCustGroup(CustGroupQueryDTO dto);
/**
* 根据ID查询客群详情
*
* @param id 客群ID
* @return 客群VO
*/
CustGroupVO getCustGroup(Long id);
/**
* 异步创建客群(模板导入)
*
@@ -57,14 +65,6 @@ public interface ICustGroupService {
*/
String updateCustGroupByTemplate(CustGroup custGroup, MultipartFile file);
/**
* 更新客群
*
* @param custGroup 客群实体
* @return 结果消息
*/
String updateCustGroup(CustGroup custGroup);
/**
* 删除客群
*
@@ -73,14 +73,6 @@ public interface ICustGroupService {
*/
String deleteCustGroup(List<Long> idList);
/**
* 获取客群详情
*
* @param id 客群ID
* @return 客群VO
*/
CustGroupVO getCustGroup(Long id);
/**
* 检查客群名称是否存在
*
@@ -97,15 +89,6 @@ public interface ICustGroupService {
*/
String getCreateStatus(Long id);
/**
* 手动移除客群客户
*
* @param groupId 客群ID
* @param memberIds 客群成员ID列表
* @return 结果消息
*/
String removeMembers(Long groupId, List<Long> memberIds);
/**
* 更新动态客群(定时任务调用)
* 根据原始导入条件重新查询并更新客户列表

View File

@@ -0,0 +1,57 @@
package com.ruoyi.group.service.impl;
import com.ruoyi.group.domain.dto.CustGroupMemberQueryDTO;
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.mapper.CustGroupMapper;
import com.ruoyi.group.mapper.CustGroupMemberMapper;
import com.ruoyi.group.service.ICustGroupMemberService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
/**
* 客群客户服务实现类
*
* @author ruoyi
*/
@Service
public class CustGroupMemberServiceImpl implements ICustGroupMemberService {
@Resource
private CustGroupMemberMapper custGroupMemberMapper;
@Resource
private CustGroupMapper custGroupMapper;
@Override
public List<CustGroupMemberVO> listCustGroupMembers(Long groupId, CustGroupMemberQueryDTO dto) {
return custGroupMemberMapper.selectCustGroupMemberList(groupId, dto);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String removeMembers(Long groupId, List<Long> memberIds) {
// 检查客群是否存在
CustGroup custGroup = custGroupMapper.selectById(groupId);
if (custGroup == null) {
return "客群不存在";
}
// 删除客户关联
memberIds.forEach(memberId -> {
CustGroupMember member = custGroupMemberMapper.selectById(memberId);
if (member != null && member.getGroupId().equals(groupId)) {
// 设置手动移除标识
member.setManualRemove(1);
// 逻辑删除
custGroupMemberMapper.deleteById(memberId);
}
});
return "移除成功";
}
}

View File

@@ -2,36 +2,28 @@ 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.cmpm.domain.vo.GridCmpmVO;
import com.ruoyi.ibs.cmpm.service.GridCmpmService;
import com.ruoyi.ibs.draw.mapper.DrawGridCustUserUnbindMapper;
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 com.ruoyi.ibs.handler.DynamicTableNameHelper;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -61,31 +53,31 @@ public class CustGroupServiceImpl implements ICustGroupService {
private ExecutorService executorService;
@Resource
private GridCmpmMapper gridCmpmMapper;
@Resource
private RegionCustUserMapper regionCustUserMapper;
@Resource
private RegionGridMapper regionGridMapper;
private GridCmpmService gridCmpmService;
@Resource
private RegionGridListService regionGridListService;
@Resource
private DrawShapeCustMapper drawShapeCustMapper;
private DrawGridCustUserUnbindMapper drawGridCustUserUnbindMapper;
@Resource
private DrawGridShapeRelateMapper drawGridShapeRelateMapper;
@Resource
private SysDeptMapper sysDeptMapper;
private TransactionTemplate transactionTemplate;
@Override
public List<CustGroupVO> listCustGroup(CustGroupQueryDTO dto) {
return custGroupMapper.selectCustGroupList(dto);
}
@Override
public CustGroupVO getCustGroup(Long id) {
CustGroupVO custGroup = custGroupMapper.selectCustGroupById(id);
if (custGroup == null) {
throw new ServiceException("客群不存在");
}
return custGroup;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String createCustGroupByTemplate(CustGroup custGroup, MultipartFile file) {
@@ -176,21 +168,6 @@ public class CustGroupServiceImpl implements ICustGroupService {
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) {
@@ -200,10 +177,17 @@ public class CustGroupServiceImpl implements ICustGroupService {
if (existGroup == null) {
throw new ServiceException("客群不存在");
}
// 检查客群是否正在创建或更新
if ("0".equals(existGroup.getCreateStatus())) {
throw new ServiceException("客群正在处理中,请稍后再试");
}
// 更新客群基本信息
if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在");
}
// 检查网格条件是否发生变化
boolean gridConditionChanged = isGridConditionChanged(existGroup, gridImportDTO);
// 重新查询数据库,获取最新状态
CustGroup latestGroup = custGroupMapper.selectById(custGroup.getId());
latestGroup.setGroupName(custGroup.getGroupName());
@@ -212,7 +196,48 @@ public class CustGroupServiceImpl implements ICustGroupService {
latestGroup.setValidTime(custGroup.getValidTime());
latestGroup.setShareEnabled(custGroup.getShareEnabled());
latestGroup.setShareDeptIds(custGroup.getShareDeptIds());
// 保存新的网格导入条件
String gridType = gridImportDTO.getGridType();
latestGroup.setGridType(gridType);
if ("0".equals(gridType)) {
latestGroup.setCmpmBizType(gridImportDTO.getCmpmBizType());
if (gridImportDTO.getUserNames() != null && !gridImportDTO.getUserNames().isEmpty()) {
latestGroup.setGridUserNames(String.join(",", gridImportDTO.getUserNames()));
} else {
latestGroup.setGridUserNames(null);
}
} else if ("1".equals(gridType)) {
if (gridImportDTO.getRegionGridIds() != null && !gridImportDTO.getRegionGridIds().isEmpty()) {
latestGroup.setRegionGridIds(gridImportDTO.getRegionGridIds().stream()
.map(String::valueOf).collect(Collectors.joining(",")));
} else {
latestGroup.setRegionGridIds(null);
}
} else if ("2".equals(gridType)) {
if (gridImportDTO.getDrawGridIds() != null && !gridImportDTO.getDrawGridIds().isEmpty()) {
latestGroup.setDrawGridIds(gridImportDTO.getDrawGridIds().stream()
.map(String::valueOf).collect(Collectors.joining(",")));
} else {
latestGroup.setDrawGridIds(null);
}
}
// 更新数据库
latestGroup.setUpdateBy(SecurityUtils.getUsername());
latestGroup.setUpdateTime(new Date());
custGroupMapper.updateById(latestGroup);
// 如果网格条件没有变化,直接返回成功
if (!gridConditionChanged) {
log.info("客群网格条件未变化跳过客户重新导入客群ID{}", custGroup.getId());
return "客群更新成功(客户列表无需变更)";
}
// 网格条件发生变化,需要重新导入客户
log.info("客群网格条件已变化开始重新导入客户客群ID{}", custGroup.getId());
// 设置更新状态为"更新中"
latestGroup.setCreateStatus("0");
custGroupMapper.updateById(latestGroup);
// 重新设置回DTO确保异步线程能获取到正确的ID和状态
gridImportDTO.setCustGroup(latestGroup);
@@ -223,6 +248,80 @@ public class CustGroupServiceImpl implements ICustGroupService {
return "客群更新中";
}
/**
* 检查网格条件是否发生变化
*/
private boolean isGridConditionChanged(CustGroup existGroup, GridImportDTO gridImportDTO) {
// 首先检查网格类型是否变化
String newGridType = gridImportDTO.getGridType();
String oldGridType = existGroup.getGridType();
// 如果旧记录没有网格类型信息,说明需要导入
if (oldGridType == null) {
return true;
}
// 网格类型变化
if (!newGridType.equals(oldGridType)) {
log.info("网格类型变化:旧={}, 新={}", oldGridType, newGridType);
return true;
}
// 根据网格类型检查具体条件
if ("0".equals(newGridType)) {
// 绩效网格:比较业务类型和客户经理列表
String oldBizType = existGroup.getCmpmBizType();
String newBizType = gridImportDTO.getCmpmBizType();
List<String> newUserNames = gridImportDTO.getUserNames();
if (!Objects.equals(oldBizType, newBizType)) {
log.info("绩效业务类型变化:旧={}, 新={}", oldBizType, newBizType);
return true;
}
// 比较客户经理列表
String oldUserNames = existGroup.getGridUserNames();
String newUserNamesStr = newUserNames == null || newUserNames.isEmpty()
? null : String.join(",", newUserNames);
if (!Objects.equals(oldUserNames, newUserNamesStr)) {
log.info("客户经理列表变化:旧={}, 新={}", oldUserNames, newUserNamesStr);
return true;
}
} else if ("1".equals(newGridType)) {
// 地理网格比较网格ID列表
String oldRegionIds = existGroup.getRegionGridIds();
List<Long> newRegionIds = gridImportDTO.getRegionGridIds();
String newRegionIdsStr = newRegionIds == null || newRegionIds.isEmpty()
? null : newRegionIds.stream().map(String::valueOf).sorted().collect(Collectors.joining(","));
// 归一化比较(排序后比较)
String oldRegionIdsSorted = oldRegionIds == null ? null :
Arrays.stream(oldRegionIds.split(",")).sorted().collect(Collectors.joining(","));
if (!Objects.equals(oldRegionIdsSorted, newRegionIdsStr)) {
log.info("地理网格ID列表变化旧={}, 新={}", oldRegionIdsSorted, newRegionIdsStr);
return true;
}
} else if ("2".equals(newGridType)) {
// 绘制网格比较网格ID列表
String oldDrawIds = existGroup.getDrawGridIds();
List<Long> newDrawIds = gridImportDTO.getDrawGridIds();
String newDrawIdsStr = newDrawIds == null || newDrawIds.isEmpty()
? null : newDrawIds.stream().map(String::valueOf).sorted().collect(Collectors.joining(","));
// 归一化比较(排序后比较)
String oldDrawIdsSorted = oldDrawIds == null ? null :
Arrays.stream(oldDrawIds.split(",")).sorted().collect(Collectors.joining(","));
if (!Objects.equals(oldDrawIdsSorted, newDrawIdsStr)) {
log.info("绘制网格ID列表变化旧={}, 新={}", oldDrawIdsSorted, newDrawIdsStr);
return true;
}
}
return false;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String updateCustGroupByTemplate(CustGroup custGroup, MultipartFile file) {
@@ -231,6 +330,10 @@ public class CustGroupServiceImpl implements ICustGroupService {
if (existGroup == null) {
throw new ServiceException("客群不存在");
}
// 检查客群是否正在创建或更新
if ("0".equals(existGroup.getCreateStatus())) {
throw new ServiceException("客群正在处理中,请稍后再试");
}
// 更新客群基本信息
if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在");
@@ -243,7 +346,11 @@ public class CustGroupServiceImpl implements ICustGroupService {
latestGroup.setValidTime(custGroup.getValidTime());
latestGroup.setShareEnabled(custGroup.getShareEnabled());
latestGroup.setShareDeptIds(custGroup.getShareDeptIds());
// 设置更新状态为"更新中"
latestGroup.setCreateStatus("0");
// 更新数据库
latestGroup.setUpdateBy(SecurityUtils.getUsername());
latestGroup.setUpdateTime(new Date());
custGroupMapper.updateById(latestGroup);
// 获取当前用户部门编码(异步线程中无法获取)
String headId = SecurityUtils.getHeadId();
@@ -269,82 +376,6 @@ public class CustGroupServiceImpl implements ICustGroupService {
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<>();
@@ -486,14 +517,14 @@ public class CustGroupServiceImpl implements ICustGroupService {
gridImportDTO.setRegionGridIds(Arrays.stream(custGroup.getRegionGridIds().split(","))
.map(Long::valueOf).collect(Collectors.toList()));
}
newMemberList.addAll(importFromRegionGrid(custGroup, gridImportDTO));
newMemberList.addAll(importFromRegionGrid(custGroup, gridImportDTO, headId));
} 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));
newMemberList.addAll(importFromDrawGrid(custGroup, gridImportDTO, headId));
}
// 计算差异
@@ -601,51 +632,71 @@ public class CustGroupServiceImpl implements ICustGroupService {
}
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());
// 使用编程式事务:先删除旧客户,再插入新客户
transactionTemplate.executeWithoutResult(status -> {
// 删除该客群的所有旧客户
log.info("开始删除客群旧客户模板导入客群ID{}", custGroup.getId());
LambdaQueryWrapper<CustGroupMember> memberWrapper = new LambdaQueryWrapper<>();
memberWrapper.eq(CustGroupMember::getGroupId, custGroup.getId());
custGroupMemberMapper.delete(memberWrapper);
log.info("客群旧客户删除完成模板导入客群ID{}", custGroup.getId());
// 批量插入新客户
log.info("开始批量插入客户模板导入客群ID{},客户总数:{}", custGroup.getId(), memberList.size());
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);
log.info("处理批次 [{}/{}],本批大小:{}", i / batchSize + 1, (memberList.size() + batchSize - 1) / batchSize, batchList.size());
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);
log.info("客群客户导入完成模板客群ID{},成功:{},跳过重复:{},恢复:{}",
custGroup.getId(), successCount, skippedCount, restoredCount);
// 更新创建状态为成功
custGroup.setCreateStatus("1");
custGroup.setUpdateBy(custGroup.getCreateBy());
custGroup.setUpdateTime(new Date());
custGroupMapper.updateById(custGroup);
});
} catch (Exception e) {
log.error("客群客户导入失败客群ID{},异常:{}", custGroup.getId(), e.getMessage(), e);
// 注意:由于删除和插入在同一事务中,插入失败会自动回滚删除操作,无需手动清理
// 更新创建状态为失败
custGroup.setCreateStatus("2");
custGroup.setUpdateBy(custGroup.getCreateBy());
custGroup.setUpdateTime(new Date());
custGroupMapper.updateById(custGroup);
log.error("客群客户导入失败客群ID{}", custGroup.getId(), e);
throw new ServiceException("客群客户导入失败: " + e.getMessage());
}
}
@@ -664,63 +715,122 @@ public class CustGroupServiceImpl implements ICustGroupService {
memberList.addAll(importFromCmpmGrid(custGroup, gridImportDTO, headId));
} else if ("1".equals(gridType)) {
// 地理网格
memberList.addAll(importFromRegionGrid(custGroup, gridImportDTO));
memberList.addAll(importFromRegionGrid(custGroup, gridImportDTO, headId));
} else if ("2".equals(gridType)) {
// 绘制网格
memberList.addAll(importFromDrawGrid(custGroup, gridImportDTO));
memberList.addAll(importFromDrawGrid(custGroup, gridImportDTO, headId));
}
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());
// 使用编程式事务:先删除旧客户,再插入新客户
transactionTemplate.executeWithoutResult(status -> {
// 删除该客群的所有旧客户
log.info("开始删除客群旧客户客群ID{}", custGroup.getId());
LambdaQueryWrapper<CustGroupMember> memberWrapper = new LambdaQueryWrapper<>();
memberWrapper.eq(CustGroupMember::getGroupId, custGroup.getId());
custGroupMemberMapper.delete(memberWrapper);
log.info("客群旧客户删除完成客群ID{}", custGroup.getId());
// 批量插入新客户
log.info("开始批量插入客户客群ID{},客户总数:{}", custGroup.getId(), memberList.size());
// 分批批量插入每批1000条
int batchSize = 1000;
int totalInserted = 0;
int totalRestored = 0;
int totalSkipped = 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);
log.info("处理批次 [{}/{}],本批大小:{}", i / batchSize + 1,
(memberList.size() + batchSize - 1) / batchSize, batchList.size());
// SQL层面的批量插入
custGroupMemberMapper.batchInsertMembers(batchList);
// 查询本批中被手动移除的客户,需要恢复
List<CustGroupMember> toRestore = findManualRemovedToRestore(custGroup.getId(), batchList);
if (!toRestore.isEmpty()) {
// 恢复被手动移除的客户
for (CustGroupMember m : toRestore) {
batchList.stream()
.filter(b -> b.getCustId().equals(m.getCustId()) && b.getCustType().equals(m.getCustType()))
.findFirst()
.ifPresent(origin -> m.setCustName(origin.getCustName()));
m.setManualRemove(0);
custGroupMemberMapper.updateById(m);
}
log.info("本批恢复被手动移除的客户:{} 条", toRestore.size());
totalRestored += toRestore.size();
}
totalInserted += batchList.size() - toRestore.size();
totalSkipped += toRestore.size();
}
}
log.info("客群客户导入完成网格客群ID{}成功{}跳过重复:{}恢复{}",
custGroup.getId(), successCount, skippedCount, restoredCount);
// 更新创建状态为成功
custGroup.setCreateStatus("1");
custGroupMapper.updateById(custGroup);
log.info("客群客户导入完成网格客群ID{}插入{}复:{}跳过{}",
custGroup.getId(), totalInserted, totalRestored, totalSkipped);
// 更新创建状态为成功
custGroup.setCreateStatus("1");
custGroup.setUpdateBy(custGroup.getCreateBy());
custGroup.setUpdateTime(new Date());
log.info("准备更新客群状态为成功客群ID{}", custGroup.getId());
custGroupMapper.updateById(custGroup);
log.info("客群状态更新成功完成客群ID{}", custGroup.getId());
});
} catch (Exception e) {
// 先记录原始异常(必须第一时间记录,避免后续异常覆盖)
log.error("==========客群客户导入异常========== 客群ID{},异常类型:{},异常消息:{}",
custGroup.getId(), e.getClass().getName(), e.getMessage(), e);
// 注意:由于删除和插入在同一事务中,插入失败会自动回滚删除操作,无需手动清理
// 更新创建状态为失败
custGroup.setCreateStatus("2");
custGroupMapper.updateById(custGroup);
log.error("客群客户导入失败客群ID{}", custGroup.getId(), e);
try {
custGroup.setCreateStatus("2");
custGroup.setUpdateBy(custGroup.getCreateBy());
custGroup.setUpdateTime(new Date());
log.info("准备更新客群状态为失败客群ID{}", custGroup.getId());
custGroupMapper.updateById(custGroup);
log.info("客群状态更新为失败完成客群ID{}", custGroup.getId());
} catch (Exception updateException) {
log.error("==========更新客群状态为失败也异常了========== 客群ID{},异常类型:{},异常消息:{}",
custGroup.getId(), updateException.getClass().getName(), updateException.getMessage(), updateException);
}
throw new ServiceException("客群客户导入失败: " + e.getMessage());
}
}
/**
* 查找需要恢复的被手动移除的客户
*
* @param groupId 客群ID
* @param batchList 本批导入的客户列表
* @return 需要恢复的客户列表
*/
private List<CustGroupMember> findManualRemovedToRestore(Long groupId, List<CustGroupMember> batchList) {
// 构建本批客户的 (custId, custType) 集合
Set<String> batchKeys = batchList.stream()
.map(m -> m.getCustId() + "|" + m.getCustType())
.collect(Collectors.toSet());
// 查询该客群所有被手动移除的客户
LambdaQueryWrapper<CustGroupMember> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CustGroupMember::getGroupId, groupId)
.eq(CustGroupMember::getManualRemove, 1)
.eq(CustGroupMember::getDelFlag, 0);
List<CustGroupMember> manualRemovedList = custGroupMemberMapper.selectList(queryWrapper);
// 筛选出本批中需要恢复的
return manualRemovedList.stream()
.filter(m -> batchKeys.contains(m.getCustId() + "|" + m.getCustType()))
.collect(Collectors.toList());
}
/**
* 从绩效网格导入客户
*/
@@ -735,13 +845,13 @@ public class CustGroupServiceImpl implements ICustGroupService {
gridTypes.add("corporate");
gridTypes.add("corporate_account");
} else {
throw new ServiceException("请选择绩效网格业务类型(零售/公司");
throw new ServiceException("请选择绩效网格业务类型(零售/对公/对公账户");
}
// 查询客户
for (String userName : gridImportDTO.getUserNames()) {
for (String gridType : gridTypes) {
List<GridCmpm> cmpmList = gridCmpmMapper.getGridCmpmByUserName(userName, headId, gridType);
for (GridCmpm cmpm : cmpmList) {
List<GridCmpmVO> cmpmList = gridCmpmService.selectManageListForImport(gridType, userName, headId);
for (GridCmpmVO cmpm : cmpmList) {
CustGroupMember member = new CustGroupMember();
member.setGroupId(custGroup.getId());
member.setCustId(cmpm.getCustId());
@@ -760,25 +870,21 @@ public class CustGroupServiceImpl implements ICustGroupService {
/**
* 从地理网格导入客户
*/
private List<CustGroupMember> importFromRegionGrid(CustGroup custGroup, GridImportDTO gridImportDTO) {
private List<CustGroupMember> importFromRegionGrid(CustGroup custGroup, GridImportDTO gridImportDTO, String headId) {
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);
}
// 直接根据网格ID列表批量查询所有客户SQL中直接拼接headId绕过MyBatis-Plus拦截器
List<RegionCustUser> custUsers = regionGridListService.selectAllCustByGridIds(gridImportDTO.getRegionGridIds(), headId);
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;
}
@@ -786,27 +892,21 @@ public class CustGroupServiceImpl implements ICustGroupService {
/**
* 从绘制网格导入客户
*/
private List<CustGroupMember> importFromDrawGrid(CustGroup custGroup, GridImportDTO gridImportDTO) {
private List<CustGroupMember> importFromDrawGrid(CustGroup custGroup, GridImportDTO gridImportDTO, String headId) {
List<CustGroupMember> memberList = new ArrayList<>();
if (gridImportDTO.getDrawGridIds() == null || gridImportDTO.getDrawGridIds().isEmpty()) {
throw new ServiceException("请选择绘制网格");
}
// 查询绘制网格关联的图形ID
// 使用 selectCustByDrawGridId 方法直接在SQL中拼接headId绕过拦截器
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) {
List<RegionCustUser> custUsers = drawGridCustUserUnbindMapper.selectCustByDrawGridId(gridId, headId);
if (custUsers != null && !custUsers.isEmpty()) {
for (RegionCustUser custUser : custUsers) {
CustGroupMember member = new CustGroupMember();
member.setGroupId(custGroup.getId());
member.setCustId(shapeCust.getCustId());
member.setCustName(shapeCust.getCustName());
member.setCustType(shapeCust.getCustType());
member.setCustId(custUser.getCustId());
member.setCustName(custUser.getCustName());
member.setCustType(custUser.getCustType());
member.setCreateTime(new Date());
memberList.add(member);
}

View File

@@ -43,4 +43,33 @@
ORDER BY cg.create_time DESC
</select>
<select id="selectCustGroupById" 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.valid_time,
cg.create_by,
cg.create_time,
cg.update_by,
cg.update_time,
cg.remark,
cg.create_status,
cg.grid_type,
cg.cmpm_biz_type,
cg.grid_user_names,
cg.region_grid_ids,
cg.draw_grid_ids,
(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.id = #{id} AND cg.del_flag = '0'
</select>
</mapper>

View File

@@ -0,0 +1,41 @@
<?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.CustGroupMemberMapper">
<select id="selectCustGroupMemberList" resultType="CustGroupMemberVO">
SELECT
cgm.id,
cgm.group_id,
cgm.cust_type,
cgm.cust_id,
cgm.cust_name,
cgm.cust_idc,
cgm.social_credit_code,
cgm.create_time
FROM ibs_cust_group_member cgm
<where>
cgm.group_id = #{groupId}
AND cgm.del_flag = '0'
<if test="dto != null and dto.custType != null and dto.custType != ''">
AND cgm.cust_type = #{dto.custType}
</if>
<if test="dto != null and dto.custName != null and dto.custName != ''">
AND cgm.cust_name LIKE CONCAT('%', #{dto.custName}, '%')
</if>
</where>
ORDER BY cgm.create_time ASC
</select>
<!-- 批量插入客群客户INSERT IGNORE遇到重复键自动跳过 -->
<insert id="batchInsertMembers">
INSERT IGNORE INTO ibs_cust_group_member
(group_id, cust_type, cust_id, cust_name, cust_idc, social_credit_code, create_by, create_time, del_flag, manual_remove)
VALUES
<foreach collection="list" item="item" index="index" separator=",">
(#{item.groupId}, #{item.custType}, #{item.custId}, #{item.custName}, #{item.custIdc}, #{item.socialCreditCode}, #{item.createBy}, NOW(), '0', '0')
</foreach>
</insert>
</mapper>

View File

@@ -117,4 +117,14 @@ public class GridCmpmController extends BaseController {
public AjaxResult selectCustBaseInfoList(@RequestBody CustBaseInfo custBaseInfo) {
return AjaxResult.success( gridCmpmCustService.selectCustInfoList (custBaseInfo)) ;
}
/**
* 根据网格类型获取客户经理列表
*/
@GetMapping("/managerList")
@Log(title = "绩效网格-获取客户经理列表")
@ApiOperation("获取客户经理列表")
public AjaxResult getManagerListByGridType(@RequestParam String gridType) {
return AjaxResult.success(gridCmpmService.getManagerListByGridType(gridType));
}
}

View File

@@ -8,11 +8,15 @@ import com.ruoyi.ibs.cmpm.domain.entity.GridCmpm;
import com.ruoyi.ibs.cmpm.domain.vo.DwbRetailCustLevelManagerDetailVO;
import com.ruoyi.ibs.cmpm.domain.vo.DwbRetailResultVO;
import com.ruoyi.ibs.cmpm.domain.vo.GridCmpmClaimVO;
import com.ruoyi.ibs.cmpm.domain.vo.GridCmpmVO;
import com.ruoyi.ibs.customerselect.domain.CustBaseInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.session.SqlSessionFactory;
import java.util.List;
import java.util.Map;
/**
* @Author 吴凯程
@@ -60,6 +64,32 @@ public interface GridCmpmMapper {
List<String> selectManagerList();
/**
* 根据网格类型和总行ID查询客户经理列表
* @param gridType 网格类型零售retail、对公corporate、对公账户corporate_account
* @param headId 总行ID
* @return 客户经理列表user_name, nick_name
*/
List<Map<String, String>> getManagerListByGridType(@Param("gridType") String gridType, @Param("headId") String headId);
/**
* 根据网格类型、总行ID和客户经理查询客户列表分表查询适用于客群导入
* @param gridType 网格类型
* @param userName 客户经理柜员号
* @param headId 总行ID
* @return 客户列表
*/
List<GridCmpmVO> getCustomerListForImport(@Param("gridType") String gridType, @Param("userName") String userName, @Param("headId") String headId);
/**
* 根据网格类型、总行ID和客户经理流式查询客户列表使用Cursor适用于大数据量场景
* @param gridType 网格类型
* @param userName 客户经理柜员号
* @param headId 总行ID
* @return 客户列表游标
*/
Cursor<GridCmpmVO> getCustomerListForImportCursor(@Param("gridType") String gridType, @Param("userName") String userName, @Param("headId") String headId);
// List<CustBaseInfo> selectCustInfoRetailFromGridCmpm(CustBaseInfo custBaseInfo);
//

View File

@@ -302,4 +302,26 @@ public class GridCmpmService {
return changesMap;
}
/**
* 根据网格类型和总行ID查询客户经理列表
* @param gridType 网格类型零售retail、对公corporate、对公账户corporate_account
* @return 客户经理列表
*/
public List<Map<String, String>> getManagerListByGridType(String gridType) {
String headId = SecurityUtils.getHeadId();
return gridCmpmMapper.getManagerListByGridType(gridType, headId);
}
/**
* 根据参数查询绩效网格客户列表不依赖SecurityUtils适用于异步线程
* 所有参数由调用者传入,不在方法内部获取用户上下文
* @param gridType 网格类型
* @param userName 客户经理柜员号
* @param headId 总行ID
* @return 客户列表
*/
public List<GridCmpmVO> selectManageListForImport(String gridType, String userName, String headId) {
return gridCmpmMapper.getCustomerListForImport(gridType, userName, headId);
}
}

View File

@@ -80,6 +80,16 @@ public class DrawGridController extends BaseController {
return getDataTable(gridList, page);
}
@ApiOperation("获取网格列表(用于客群创建,简化查询)")
@Log(title = "自定义绘制网格--获取网格列表(客群创建用)")
@GetMapping("/simpleList")
public R<List<DrawGridListVO>> getSimpleGridList(DrawGridListDTO drawGridListDTO) {
drawGridListDTO.setUserName(SecurityUtils.getUsername());
drawGridListDTO.setDeptId(SecurityUtils.getDeptId());
List<DrawGridListVO> gridList = drawGridService.getSimpleGridList(drawGridListDTO);
return R.ok(gridList);
}
@ApiOperation("分页获取网格内客户列表")
@Log(title = "自定义绘制网格--分页获取网格内客户列表")
@GetMapping("/cust/list")

View File

@@ -4,7 +4,9 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ibs.draw.domain.dto.grid.DrawGridCustListDTO;
import com.ruoyi.ibs.draw.domain.entity.DrawGridCustUserUnbind;
import com.ruoyi.ibs.draw.domain.vo.DrawGridCustVO;
import com.ruoyi.ibs.grid.domain.entity.RegionCustUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -18,5 +20,13 @@ public interface DrawGridCustUserUnbindMapper extends BaseMapper<DrawGridCustUse
List<DrawGridCustVO> getCustListByManager(DrawGridCustListDTO drawGridCustListDTO);
/**
* 根据绘制网格ID查询所有客户用于客群导入拼接headId绕过拦截器
* @param gridId 绘制网格ID
* @param headId 总行机构号前三位(用于拼接动态表名)
* @return 客户列表
*/
List<RegionCustUser> selectCustByDrawGridId(@Param("gridId") Long gridId, @Param("headId") String headId);
}

View File

@@ -3,6 +3,10 @@ package com.ruoyi.ibs.draw.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ibs.draw.domain.entity.DrawGridShapeRelate;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* @Author 吴凯程
@@ -10,4 +14,19 @@ import org.apache.ibatis.annotations.Mapper;
**/
@Mapper
public interface DrawGridShapeRelateMapper extends BaseMapper<DrawGridShapeRelate> {
/**
* 根据网格ID查询关联的图形用于异步导入客群原生SQL绕过拦截器
* @param gridId 网格ID
* @return 图形关联列表
*/
List<DrawGridShapeRelate> selectByGridId(@Param("gridId") Long gridId);
/**
* 根据网格ID查询所有客户用于客群导入直接拼接headId绕过拦截器
* @param gridId 网格ID
* @param headId 部门代码(用于拼接动态表名)
* @return 客户列表,包含 custId, custName, custType
*/
List<Map<String, Object>> selectCustListByGridId(@Param("gridId") Long gridId, @Param("headId") String headId);
}

View File

@@ -24,6 +24,13 @@ public interface DrawGridService {
List<DrawGridListVO> getGridList(DrawGridListDTO drawGridListDTO);
/**
* 获取网格列表(用于客群创建,简化查询不统计客户数量)
* @param drawGridListDTO 查询条件
* @return 网格列表(不含客户数量)
*/
List<DrawGridListVO> getSimpleGridList(DrawGridListDTO drawGridListDTO);

View File

@@ -334,6 +334,15 @@ public class DrawGridServiceImpl implements DrawGridService {
return gridList;
}
@Override
public List<DrawGridListVO> getSimpleGridList(DrawGridListDTO drawGridListDTO) {
if (SecurityUtils.userRole().equals("manager")) {
return drawGridMapper.getGridListByManager(drawGridListDTO);
} else {
return drawGridMapper.getGridList(drawGridListDTO);
}
}
private CustCountDTO getCustCountByGrid(Long gridId) {
CustCountDTO custCountDTO = new CustCountDTO();
LambdaQueryWrapper<DrawGridShapeRelate> queryWrapper = new LambdaQueryWrapper<>();

View File

@@ -12,6 +12,7 @@ import com.ruoyi.ibs.grid.domain.dto.GridCustListDTO;
import com.ruoyi.ibs.grid.domain.dto.RegionGridListDTO;
import com.ruoyi.ibs.grid.domain.entity.RegionGrid;
import com.ruoyi.ibs.grid.domain.vo.RegionCustUserVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridGroupVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridListVO;
import com.ruoyi.ibs.grid.mapper.RegionGridMapper;
import com.ruoyi.ibs.grid.service.RegionGridListService;
@@ -108,6 +109,14 @@ public class RegionGridListController extends BaseController {
return AjaxResult.success("开始更新行社所有网格");
}
/**
* 查询地理网格列表(专用于客群创建)
*/
@ApiOperation("查询地理网格列表(专用于客群创建)")
@Log(title = "地理网格-查询地理网格列表(客群创建用)")
@GetMapping("/groupList")
public AjaxResult getRegionGridListForGroup(RegionGridListDTO dto) {
return AjaxResult.success(regionGridListService.getRegionGridListForGroup(dto));
}
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.ibs.grid.domain.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 地理网格选择VO - 专用于客群创建时的网格选择
* 只包含必要字段,提升查询效率
*
* @author ruoyi
*/
@Data
public class RegionGridGroupVO implements Serializable {
/**
* 网格主键
*/
@ApiModelProperty(value = "网格主键")
private Long gridId;
/**
* 网格名称
*/
@ApiModelProperty(value = "网格名称")
private String gridName;
}

View File

@@ -40,6 +40,13 @@ public interface RegionCustUserMapper extends BaseMapper<RegionCustUser> {
Long countCustInSecGrid(@Param("gridId") Long gridId, @Param("custType") String custType);
/**
* 根据网格ID列表查询所有客户用于异步导入客群直接根据gridIds查询无需区分等级
* @param gridIds 网格ID列表
* @param headId 总行机构号前三位(用于拼接动态表名)
*/
List<RegionCustUser> selectAllCustByGridIds(@Param("gridIds") List<Long> gridIds, @Param("headId") String headId);

View File

@@ -5,6 +5,7 @@ import com.ruoyi.ibs.grid.domain.dto.GridCustListDTO;
import com.ruoyi.ibs.grid.domain.dto.RegionGridListDTO;
import com.ruoyi.ibs.grid.domain.entity.RegionGrid;
import com.ruoyi.ibs.grid.domain.vo.RegionGridCustVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridGroupVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridListVO;
import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo;
import org.apache.ibatis.annotations.Param;
@@ -69,4 +70,10 @@ public interface RegionGridMapper extends BaseMapper<RegionGrid> {
List<Long> getSecGridIdByTopGridId(@Param("gridId") Long gridId);
/**
* 查询地理网格列表(简化版)- 专用于客群创建
* 只返回gridId和gridName避免N+1查询问题
*/
List<RegionGridGroupVO> getRegionGridListForGroup(RegionGridListDTO regionGridListDTO);
}

View File

@@ -8,6 +8,7 @@ import com.ruoyi.ibs.grid.domain.entity.RegionCustUser;
import com.ruoyi.ibs.grid.domain.entity.RegionGrid;
import com.ruoyi.ibs.grid.domain.vo.RegionCustUserVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridCustVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridGroupVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridListVO;
import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo;
@@ -36,11 +37,20 @@ public interface RegionGridListService {
List<RegionGridListVO> getSecGridListByManager(RegionGridListDTO regionGridListDTO);
/**
* 查询网格内所有客户(不限制客户类型,用于导入客群)
* @param regionGrid 地理网格对象
* @return 网格内所有客户列表
* 根据网格ID列表批量查询所有客户用于导入客群,优化版
* @param gridIds 网格ID列表
* @param headId 总行机构号前三位(用于拼接动态表名)
* @return 客户列表(去重)
*/
List<RegionCustUser> selectAllCustFromGrid(RegionGrid regionGrid);
List<RegionCustUser> selectAllCustByGridIds(List<Long> gridIds, String headId);
/**
* 查询地理网格列表(简化版)- 专用于客群创建
* 只返回gridId和gridName避免N+1查询问题
* @param regionGridListDTO 查询条件
* @return 网格列表仅包含ID和名称
*/
List<RegionGridGroupVO> getRegionGridListForGroup(RegionGridListDTO regionGridListDTO);
}

View File

@@ -12,6 +12,7 @@ import com.ruoyi.ibs.grid.domain.dto.UnbindDTO;
import com.ruoyi.ibs.grid.domain.entity.*;
import com.ruoyi.ibs.grid.domain.vo.RegionCustUserVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridCustVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridGroupVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridListVO;
import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo;
import com.ruoyi.ibs.grid.mapper.*;
@@ -432,23 +433,33 @@ public class RegionGridListServiceImpl implements RegionGridListService {
}
/**
* 查询网格内所有客户(不限制客户类型,用于导入客群)
* @param regionGrid 地理网格对象
* @return 网格内所有客户列表
* 根据网格ID列表批量查询所有客户用于导入客群,优化版
* @param gridIds 网格ID列表
* @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());
public List<RegionCustUser> selectAllCustByGridIds(List<Long> gridIds, String headId) {
return regionCustUserMapper.selectAllCustByGridIds(gridIds, headId);
}
/**
* 查询地理网格列表(简化版)- 专用于客群创建
* 只返回gridId和gridName避免N+1查询问题
* @param regionGridListDTO 查询条件
* @return 网格列表仅包含ID和名称
*/
@Override
public List<RegionGridGroupVO> getRegionGridListForGroup(RegionGridListDTO regionGridListDTO) {
// 设置当前用户部门ID用于权限过滤
regionGridListDTO.setDeptId(SecurityUtils.getDeptId());
// 如果是总行用户,设置归属部室
if (SecurityUtils.isHead()) {
regionGridListDTO.setOpsDept(SecurityUtils.getOpsDept());
}
// 不限制客户类型,查询所有类型的客户
return regionCustUserMapper.selectList(queryWrapper);
// 直接查询一次SQL完成无N+1问题
return regionGridMapper.getRegionGridListForGroup(regionGridListDTO);
}
}

View File

@@ -41,6 +41,14 @@ public class CustInfoBusinessVo {
@ApiModelProperty(value = "手动打标")
private List<TreeNode> tagManual;
/** 九维画像分数 */
@ApiModelProperty(value = "九维画像分数")
private Ent9vPortraitOrc ent9vPortrait;
/** 九维画像详细信息 */
@ApiModelProperty(value = "九维画像详细信息")
private NineVFinalInfoOrc nineVFinalInfo;
public List<TreeNode> getTagManual() {
return tagManual;
}
@@ -187,4 +195,20 @@ public class CustInfoBusinessVo {
public void setTabEnmuVos(List<TreeNode> tabEnmuVos) {
this.tabEnmuVos = tabEnmuVos;
}
public Ent9vPortraitOrc getEnt9vPortrait() {
return ent9vPortrait;
}
public void setEnt9vPortrait(Ent9vPortraitOrc ent9vPortrait) {
this.ent9vPortrait = ent9vPortrait;
}
public NineVFinalInfoOrc getNineVFinalInfo() {
return nineVFinalInfo;
}
public void setNineVFinalInfo(NineVFinalInfoOrc nineVFinalInfo) {
this.nineVFinalInfo = nineVFinalInfo;
}
}

View File

@@ -0,0 +1,66 @@
package com.ruoyi.ibs.list.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 企业九维画像分数对象 ent_9v_portrait_orc_825
*
* @author ruoyi
* @date 2026-03-18
*/
@Data
@TableName("ent_9v_portrait_orc_825")
public class Ent9vPortraitOrc implements Serializable {
private static final long serialVersionUID = 1L;
/** 统一社会信用代码 */
private String uniscid;
/** 客户内码 */
private String cstId;
/** 机构号 */
private String orgNo;
/** score_1合规经营 */
private BigDecimal score1;
/** score_2风险准入 */
private BigDecimal score2;
/** score_3高管信用评价 */
private String score3;
/** score_4股东信用评价 */
private BigDecimal score4;
/** score_5社会贡献度 */
private BigDecimal score5;
/** score_6稳定经营 */
private BigDecimal score6;
/** score_7经营能力 */
private BigDecimal score7;
/** score_8偿债能力 */
private BigDecimal score8;
/** score_9潜在代偿资源 */
private BigDecimal score9;
/** 九维总分 */
private BigDecimal scoreAll;
/** 九维总分排名 */
private BigDecimal scoreAllRank;
/** 会计日期 */
private String datDt;
}

View File

@@ -0,0 +1,226 @@
package com.ruoyi.ibs.list.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* 企业九维画像详细信息对象 9v_final_info_orc_825
*
* @author ruoyi
* @date 2026-03-18
*/
@Data
@TableName("9v_final_info_orc_825")
public class NineVFinalInfoOrc implements Serializable {
private static final long serialVersionUID = 1L;
/** 统一社会信用代码 */
private String uniscid;
/** 机构号 */
private String orgNo;
/** 是否存在经营异常名录信息 */
private String score11;
/** 是否存在严重违法失信企业名单信息 */
private String score12;
/** 企业环境行为信仰登记是否为"E"或D */
private String score13;
/** 是否存在税务重大税收违法黑名单信息 */
private String score14;
/** 是否存在拖欠工资黑名单 */
private String score15;
/** 是否存在工商吊销企业信息 */
private String score16;
/** 是否存在注销企业信息 */
private String score21;
/** 是否存在执行案件信息 */
private String score22;
/** 是否存在查封信息 */
private String score23;
/** 是否存在单位未履行生效裁判信息 */
private String score24;
/** 是否存在企业破产清算信息 */
private String score25;
/** 是否失信被执行人 */
private String score26;
/** 是否为诉讼案件被告 */
private String score27;
/** 是否存在查封信息(score_3) */
private String score31;
/** 是否存在执行案件信息(score_3) */
private String score32;
/** 是否存在个人未履行生效裁判信息 */
private String score33;
/** 是否存在拖欠工资黑名单(score_3) */
private String score34;
/** 是否失信被执行人(score_3) */
private String score35;
/** 是否存在刑事案件被告人生效判决信息 */
private String score36;
/** 是否为诉讼案件被告(score_3) */
private String score37;
/** 是否存在查封信息(score_4) */
private String score41;
/** 是否存在执行案件信息(score_4) */
private String score42;
/** 是否存在个人未履行生效裁判信息(score_4) */
private String score43;
/** 是否存在拖欠工资黑名单(score_4) */
private String score44;
/** 是否失信被执行人(score_4) */
private String score45;
/** 是否存在刑事案件被告人生效判决信息(score_4) */
private String score46;
/** 是否为诉讼案件被告(score_4) */
private String score47;
/** 是否存在企业未履行生效裁判信息 */
private String score48;
/** 前12个月纳税金额 */
private String score51;
/** 纳税等级 */
private String score52;
/** 缴纳社保人数 */
private String score53;
/** 公积金缴纳人数 */
private String score54;
/** 是否为出口退税生产清单企业 */
private String score55;
/** 市场主体经营年限 */
private String score61;
/** 股东(或发起人)或投资人信息认缴出资人数 */
private String score62;
/** 最大股东持股占比 */
private String score63;
/** 近三年法定代表人变更次数 */
private String score64;
/** 近三年股东变更次数 */
private String score65;
/** 近三年经营范围变更次数 */
private String score66;
/** 近三年经营地址变更次数 */
private String score67;
/** 近三年缴税年数 */
private String score68;
/** 法人户籍 */
private String score69;
/** 法人婚姻状况 */
private String score610;
/** 上年增值税金额 */
private String score71;
/** 今年增值税同比变动 */
private String score72;
/** 上年企业所得税金额 */
private String score73;
/** 今年所得税同比变动 */
private String score74;
/** 缴纳社保人数同比变动 */
private String score75;
/** 公积金缴纳人数同比变动 */
private String score76;
/** 当年纳税金额同比变动 */
private String score77;
/** 上年出口退税金额 */
private String score78;
/** 房产套数 */
private String score81;
/** 房产面积 */
private String score82;
/** 未抵押房产套数 */
private String score83;
/** 未抵押房产面积 */
private String score84;
/** 已抵押房产被担保债权数额 */
private String score85;
/** 企业车产金额 */
private String score86;
/** 法人及股东房产面积合计 */
private String score91;
/** 法人及股东房产套数合计 */
private String score92;
/** 法人及股东未抵押房产套数合计 */
private String score93;
/** 法人及股东未抵押房产面积 */
private String score94;
/** 法人及股东已抵押房产被担保债权数额合计 */
private String score95;
/** 法人代表车产金额 */
private String score96;
/** 生态信用扣分 */
private String scoreB015;
/** 列入环境失信黑名单时间 */
private String scoreB016Time;
/** 列入环境失信黑名单原因 */
private String scoreB016Reason;
/** 环境行为信用评价等级 */
private String scoreA020;
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.ibs.list.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ibs.list.domain.Ent9vPortraitOrc;
/**
* 企业九维画像分数Mapper接口
*
* @author ruoyi
* @date 2026-03-18
*/
public interface Ent9vPortraitOrcMapper extends BaseMapper<Ent9vPortraitOrc> {
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.ibs.list.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ibs.list.domain.NineVFinalInfoOrc;
/**
* 企业九维画像详细信息Mapper接口
*
* @author ruoyi
* @date 2026-03-18
*/
public interface NineVFinalInfoOrcMapper extends BaseMapper<NineVFinalInfoOrc> {
}

View File

@@ -24,7 +24,9 @@ import com.ruoyi.ibs.grid.util.CustExcelUtil;
import com.ruoyi.ibs.list.domain.*;
import com.ruoyi.ibs.list.mapper.CorporateShareholderMapper;
import com.ruoyi.ibs.list.mapper.CustInfoRetailMapper;
import com.ruoyi.ibs.list.mapper.Ent9vPortraitOrcMapper;
import com.ruoyi.ibs.list.mapper.FamilyMembersMapper;
import com.ruoyi.ibs.list.mapper.NineVFinalInfoOrcMapper;
import com.ruoyi.ibs.list.mapper.SignedProductsMapper;
import com.ruoyi.ibs.list.service.ICustInfoBusinessService;
import com.ruoyi.system.mapper.SysIndustryMapper;
@@ -75,6 +77,12 @@ public class CustInfoBusinessServiceImpl implements ICustInfoBusinessService
@Autowired
private FamilyMembersMapper familyMembersMapper;
@Autowired
private Ent9vPortraitOrcMapper ent9vPortraitOrcMapper;
@Autowired
private NineVFinalInfoOrcMapper nineVFinalInfoOrcMapper;
@Autowired
private CustMapMapper custMapMapper;
@@ -138,6 +146,17 @@ public class CustInfoBusinessServiceImpl implements ICustInfoBusinessService
//查询手动标签列表
List<CustManualTagVO> tagCreateVos = familyMembersMapper.selectManualTag(params);
custInfoBusinessVo.setTagManual(TreeNode.convertToTreeByParentId(tagCreateVos));
// 仅当登录机构为825时查询九维画像数据
// if ("825".equals(getHeadId())) {
// LambdaQueryWrapper<Ent9vPortraitOrc> portraitWrapper = new LambdaQueryWrapper<>();
// portraitWrapper.eq(Ent9vPortraitOrc::getUniscid, custInfoBusiness.getSocialCreditCode());
// custInfoBusinessVo.setEnt9vPortrait(ent9vPortraitOrcMapper.selectOne(portraitWrapper));
//
// LambdaQueryWrapper<NineVFinalInfoOrc> finalInfoWrapper = new LambdaQueryWrapper<>();
// finalInfoWrapper.eq(NineVFinalInfoOrc::getUniscid, custInfoBusiness.getSocialCreditCode());
// custInfoBusinessVo.setNineVFinalInfo(nineVFinalInfoOrcMapper.selectOne(finalInfoWrapper));
// }
}
return custInfoBusinessVo;
}

View File

@@ -131,4 +131,15 @@ public class WorkRecordController extends BaseController {
return toAjax(workRecordService.updateReadTime(id));
}
/**
* 查询所有预警类型
*/
@GetMapping("/alter/types")
@Log(title = "工作台-查询预警类型")
@ApiOperation("查询所有预警类型")
public AjaxResult getAlterTypes() {
List<String> types = workRecordService.getAlterTypes();
return AjaxResult.success(types);
}
}

View File

@@ -112,4 +112,12 @@ public interface WorkRecordMapper extends BaseMapper<WorkRecord> {
List<AlterConfigVO> selectAlterConfigList(String alterType);
int updateAlterConfig(AlterConfig alterConfig);
/**
* 查询所有预警类型
*
* @param headId 总部ID部门ID前三位
* @return 预警类型列表
*/
List<String> selectAlterTypes(@Param("headId") String headId);
}

View File

@@ -85,4 +85,11 @@ public interface WorkRecordService{
int updateReadTime(Long id);
AlterCountVO getAlterCount(Date reportTime);
/**
* 查询所有预警类型
*
* @return 预警类型列表
*/
List<String> getAlterTypes();
}

View File

@@ -13,6 +13,7 @@ import java.util.stream.IntStream;
import com.github.pagehelper.Page;
import com.ruoyi.common.core.page.TableDataPageInfo;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.ibs.task.domain.dto.WorkRecordDTO;
@@ -39,6 +40,8 @@ import org.springframework.transaction.annotation.Transactional;
@Service
public class WorkRecordServiceImpl implements WorkRecordService {
private final static String alterTypesRedisKey = "work:record:alter:types";
@Autowired
private WorkRecordMapper workRecordMapper;
@@ -48,6 +51,9 @@ public class WorkRecordServiceImpl implements WorkRecordService {
@Autowired
private ISysDeptService sysDeptService;
@Autowired
private RedisCache redisCache;
/**
* 查询我的工作清单
*
@@ -253,6 +259,19 @@ public class WorkRecordServiceImpl implements WorkRecordService {
return alterCountVO;
}
@Override
public List<String> getAlterTypes() {
String headId = SecurityUtils.getHeadId();
String cacheKey = alterTypesRedisKey + ":" + headId;
List<String> cachedTypes = redisCache.getCacheObject(cacheKey);
if (cachedTypes != null) {
return cachedTypes;
}
List<String> types = workRecordMapper.selectAlterTypes(headId);
redisCache.setCacheObjectToEndDay(cacheKey, types);
return types;
}
/**
* 获取当前登录用户权限范围内所有post_ids
* @return

View File

@@ -193,7 +193,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and status = #{status}
</if>
<if test="alterType != null and alterType != ''">
and alter_type LIKE CONCAT('%', #{alterType}, '%')
and alter_type = #{alterType}
</if>
</where>
order by create_time desc, status asc
@@ -232,7 +232,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and wr.status = #{status}
</if>
<if test="alterType != null and alterType != ''">
and wr.alter_type LIKE CONCAT('%', #{alterType}, '%')
and wr.alter_type = #{alterType}
</if>
<!-- "走访异常提醒"类型直接通过用户名匹配,其他类型按角色权限处理 -->
and ((wr.alter_type = '走访异常提醒' and wr.user_name = #{username}) or
@@ -268,4 +268,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where id = #{id}
</update>
<select id="selectAlterTypes" resultType="string">
select distinct alter_type
from work_record
where is_alter = 1
and alter_type is not null
and alter_type != ''
and left(user_name, 3) = #{headId}
order by alter_type
</select>
</mapper>

View File

@@ -505,4 +505,26 @@
where manager_id is not null
</select>
<!-- 根据网格类型和总行ID查询客户经理列表 -->
<select id="getManagerListByGridType" resultType="java.util.HashMap">
select distinct user_name as userName, nick_name as nickName
from grid_cmpm_${gridType}_${headId}
where user_name is not null
order by user_name
</select>
<!-- 根据网格类型、总行ID和客户经理查询客户列表分表查询适用于客群导入-->
<select id="getCustomerListForImport" resultType="GridCmpmVO">
select cust_id, cust_name, cust_type, cust_idc, usci
from grid_cmpm_${gridType}_${headId}
where user_name = #{userName}
</select>
<!-- 根据网格类型、总行ID和客户经理流式查询客户列表使用Cursor适用于大数据量场景-->
<select id="getCustomerListForImportCursor" resultType="GridCmpmVO" fetchSize="1000">
select cust_id, cust_name, cust_type, cust_idc, usci
from grid_cmpm_${gridType}_${headId}
where user_name = #{userName}
</select>
</mapper>

View File

@@ -55,8 +55,17 @@
<if test="custName != null and custName != ''">AND b.cust_name like concat('%', #{custName}, '%')</if>
<if test="custType != null and custType != ''">AND b.cust_type = #{custType}</if>
</select>
<select id="">
<!-- 根据绘制网格ID查询所有客户用于客群导入直接拼接headId绕过拦截器 -->
<select id="selectCustByDrawGridId" resultType="com.ruoyi.ibs.grid.domain.entity.RegionCustUser">
SELECT DISTINCT
sc.cust_id,
sc.cust_name,
sc.cust_type
FROM grid_draw_shape_relate sr
INNER JOIN draw_shape_cust_${headId} sc ON sc.shape_id = sr.shape_id
WHERE sr.grid_id = #{gridId}
AND sr.delete_flag = '0'
</select>
</mapper>

View File

@@ -121,4 +121,30 @@
WHERE sec_grid_id = #{gridId}
AND cust_type = #{custType}
</select>
<!-- 根据网格ID列表查询所有客户一级或二级网格 -->
<select id="selectAllCustByGridIds" resultType="RegionCustUser">
SELECT DISTINCT cust_id, cust_name, cust_type
FROM grid_region_cust_user_${headId}
WHERE (top_grid_id IN
<foreach collection="gridIds" item="gridId" index="index" open="(" separator="," close=")">
#{gridId}
</foreach>
OR sec_grid_id IN
<foreach collection="gridIds" item="gridId" index="index" open="(" separator="," close=")">
#{gridId}
</foreach>)
</select>
<!-- 根据绘制网格ID查询所有客户用于客群导入直接拼接headId绕过拦截器 -->
<select id="selectCustByDrawGridId" resultType="RegionCustUser">
SELECT DISTINCT
sc.cust_id,
sc.cust_name,
sc.cust_type
FROM grid_draw_shape_relate sr
LEFT JOIN draw_shape_cust_${headId} sc ON sc.shape_id = sr.shape_id
WHERE sr.grid_id = #{gridId}
AND sr.delete_flag = '0'
</select>
</mapper>

View File

@@ -339,4 +339,26 @@
<select id="getSecGridIdByTopGridId" resultType="Long">
select grid_id from grid_region_grid where parent_grid_id = #{gridId} and grid_level = '2' and delete_flag = '0'
</select>
<!-- 查询地理网格列表(简化版)- 专用于客群创建 -->
<select id="getRegionGridListForGroup" parameterType="RegionGridListDTO" resultType="RegionGridGroupVO">
select distinct a.grid_id, a.grid_name
from grid_region_grid a
left join grid_region_user_relate c on c.grid_id = a.grid_id
where a.delete_flag = '0'
and c.delete_flag = '0'
and a.grid_level = #{gridLevel}
and (a.dept_id in (select dept_id from sys_dept where dept_id = #{deptId} or find_in_set(#{deptId}, ancestors))
or c.relate_dept_id in (select dept_id from sys_dept where dept_id = #{deptId} or find_in_set(#{deptId}, ancestors)))
<if test="opsDept != null and opsDept != ''">
and a.ops_dept = #{opsDept}
</if>
<if test="gridDutyType != null and gridDutyType != ''">
and a.grid_duty_type = #{gridDutyType}
</if>
<if test="gridName != null and gridName != ''">
and a.grid_name like concat('%', #{gridName}, '%')
</if>
ORDER BY a.grid_name
</select>
</mapper>

168
ruoyi-ui/CLAUDE.md Normal file
View File

@@ -0,0 +1,168 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
**数字支行辅助管理系统** - 基于 RuoYi-Vue 框架的银行客户网格化管理平台。
这是一个全栈项目:
- **前端**: `ruoyi-ui/` - Vue 2.6 + Element UI
- **后端**: `../ibs/` - Java Spring Boot (若依框架)
## 常用命令
### 开发
```bash
# 安装依赖(建议使用国内镜像)
npm install --registry=https://registry.npmmirror.com
# 启动开发服务器 (默认端口80)
npm run dev
# Node.js 版本兼容性问题的启动方式
npm run dev_t
```
### 构建
```bash
# 构建生产环境
npm run build:prod
# 构建测试环境
npm run build:stage
# 构建预发布环境
npm run build:pre
```
### 代码质量
```bash
# ESLint 检查
npm run lint
```
### Mock 服务
```bash
# 启动 Mock 服务器
npm run mock
```
## 项目架构
### 目录结构
```
ruoyi-ui/
├── src/
│ ├── api/ # API 接口层,按业务模块分组
│ ├── assets/ # 静态资源图片、样式、图标SVG
│ ├── components/ # 全局通用组件
│ ├── directive/ # Vue 自定义指令
│ ├── layout/ # 布局组件(侧边栏、头部等)
│ ├── map/ # 地图相关工具/配置
│ ├── plugins/ # 插件配置
│ ├── router/ # 路由配置
│ ├── store/ # Vuex 状态管理
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ ├── main.js # 入口文件
│ └── permission.js # 路由权限控制
├── public/
│ ├── baidu/ # 百度地图静态资源
│ └── index.html
├── mock/ # Mock 数据
├── .env.development # 开发环境配置
├── .env.production # 生产环境配置
└── .env.staging # 测试环境配置
```
### 核心业务模块
| 目录 | 功能 |
|------|------|
| `views/grid/` | 网格管理(创建、列表、地图、走访、绩效) |
| `views/customer/` | 客户管理360视图、建档、客群、标签 |
| `views/system/` | 系统管理(用户、角色、菜单、字典) |
| `views/monitor/` | 系统监控(日志、在线用户、服务监控) |
| `views/configure/` | 配置管理(级别配置、参数设置、模板) |
| `views/approveCenter/` | 审批中心 |
| `views/gridSearch/` | 网格搜索(管户、绩效、公海池) |
### API 层组织
API 请求按业务模块组织在 `src/api/` 目录:
- 每个模块导出具体的请求函数
- 使用 `src/utils/request.js` 中的 axios 实例
- 默认添加 Bearer Token 认证
- 统一错误处理401/500/601等状态码
### 状态管理 (Vuex)
Store 模块位于 `src/store/modules/`
- `app` - 应用状态(侧边栏、设备类型)
- `user` - 用户信息
- `permission` - 权限路由
- `settings` - 系统设置
- `dict` - 字典数据
- `tagsView` - 标签页视图
- `featuredAreas` - 特色区域
- `rate` - 汇率相关
使用 `vuex-persistedstate` 将 settings 持久化到 localStorage。
### 路由系统
- 路由分为 `constantRoutes`(静态路由)和 `dynamicRoutes`(动态路由)
- 动态路由基于用户权限permissions动态加载
- 使用 `src/permission.js` 进行路由守卫和权限控制
- 支持嵌套路由和路由元信息配置
### 百度地图集成
项目深度集成百度地图 API
- 静态资源位于 `public/baidu/`
- 地图相关 API 在 `src/api/grid/`
- 支持 WebGL 渲染、绘制管理器、热力图等
## 环境变量配置
| 变量 | 说明 |
|------|------|
| `VUE_APP_BASE_API` | 后端 API 基础路径 |
| `VUE_APP_MOCK_API` | Mock API 路径 |
| `VUE_APP_MOCK` | 是否启用 Mock |
| `VUE_APP_STAGE_URL` | 测试环境地址 |
| `VUE_APP_BAIDU_URL` | 百度地图服务地址 |
| `VUE_APP_SHARP_URL` | SHARP 服务地址 |
## 代码规范
- ESLint 配置基于 `plugin:vue/recommended`
- 使用单引号、2空格缩进
- 组件名使用 PascalCase
- pre-commit 钩子自动执行 lint-staged
## 全局组件
以下组件已全局注册,可直接使用:
- `Pagination` - 分页组件
- `RightToolbar` - 表格工具栏
- `Editor` - 富文本编辑器
- `FileUpload` - 文件上传
- `ImageUpload` - 图片上传
- `ImagePreview` - 图片预览
- `DictTag` - 字典标签
- `treeselect` - 树形选择器
- `DownloadBtn` - 下载按钮
## 全局方法
通过 `Vue.prototype` 挂载的全局方法:
- `getDicts()` - 获取字典数据
- `getConfigKey()` - 获取配置项
- `parseTime()` - 时间格式化
- `resetForm()` - 重置表单
- `download()` - 文件下载
- `handleTree()` - 处理树形数据

View File

@@ -287,6 +287,15 @@ export function getFeaturegrid(data) {
})
}
// 获取网格列表(用于客群创建,简化查询)
export function getSimpleGridList(data) {
return request({
url: `/draw/grid/simpleList`,
method: 'get',
params: data
})
}
// 删除网格
export function featureGridDelete(data) {
return request({

View File

@@ -0,0 +1,137 @@
import request from '@/utils/request'
// 查询客群列表
export function listCustGroup(query) {
return request({
url: '/group/cust/list',
method: 'get',
params: query
})
}
// 获取客群详情
export function getCustGroup(id) {
return request({
url: '/group/cust/' + id,
method: 'get'
})
}
// 异步创建客群(网格导入)
export function createCustGroupByGrid(data) {
return request({
url: '/group/cust/createByGrid',
method: 'post',
data: data
})
}
// 异步创建客群(模板导入)
export function createCustGroupByTemplate(data) {
return request({
url: '/group/cust/createByTemplate',
method: 'post',
data: data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 更新客群
export function updateCustGroup(data) {
return request({
url: '/group/cust/update',
method: 'post',
data: data
})
}
// 更新客群(网格导入)
export function updateCustGroupByGrid(data) {
return request({
url: '/group/cust/updateByGrid',
method: 'post',
data: data
})
}
// 更新客群(模板导入)
export function updateCustGroupByTemplate(data) {
return request({
url: '/group/cust/updateByTemplate',
method: 'post',
data: data,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 轮询客群创建状态
export function getCreateStatus(id) {
return request({
url: '/group/cust/createStatus/' + id,
method: 'get'
})
}
// 客户信息模板下载
export function downloadTemplate() {
return request({
url: '/group/cust/download',
method: 'post',
responseType: 'blob'
})
}
// 删除客群
export function deleteCustGroup(idList) {
return request({
url: '/group/cust/delete',
method: 'post',
data: idList
})
}
// 手动移除客群客户
export function removeMembers(groupId, memberIds) {
return request({
url: '/group/cust/removeMembers',
method: 'post',
params: { groupId: groupId },
data: memberIds
})
}
// 检查客群名称是否存在
export function checkGroupNameExist(groupName) {
return request({
url: '/group/cust/checkName',
method: 'get',
params: { groupName: groupName }
})
}
// 根据网格类型获取客户经理列表
export function getManagerList(gridType) {
return request({
url: '/grid/cmpm/managerList',
method: 'get',
params: { gridType: gridType }
})
}
// 分页查询客群成员列表
export function listCustGroupMembers(groupId, query) {
return request({
url: '/group/member/list/' + groupId,
method: 'get',
params: query
})
}
// 查询地理网格列表(专用于客群创建)
export function getRegionGridListForGroup(query) {
return request({
url: '/grid/region/groupList',
method: 'get',
params: query
})
}

View File

@@ -257,4 +257,12 @@ export function warningCardNum(query) {
method: 'get',
params: query
})
}
// 查询所有预警类型
export function getAlterTypes() {
return request({
url: '/work/record/alter/types',
method: 'get'
})
}

View File

@@ -227,6 +227,17 @@ export const constantRoutes = [
}]
},
{
path: '/group/custGroup/detail',
component: Layout,
hidden: true,
children: [{
path: '/group/custGroup/detail',
name: 'CustGroupDetail',
meta: { title: '客群详情', activeMenu: '/group/custGroup' },
component: () => import('@/views/group/custGroup/detail')
}]
},
{
path: '/checklist/customerlist',
component: Layout,

View File

@@ -11,6 +11,7 @@ const user = {
permissions: [],
nickName: '',
userName: '',
deptId: '',
groupErr:{},
},

View File

@@ -2,9 +2,16 @@
<div class="customer-wrap">
<div>
<el-radio-group class="header-radio" v-model="selectedTab" @change="handleTabChange">
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
</template>
<template v-else>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
</template>
</el-radio-group>
<div class="customerMain">
<div :class="iscollapsed ? 'customerMain_left_sq' : 'customerMain_left_zk'">
@@ -317,6 +324,9 @@ export default {
},
computed: {
...mapGetters(['roles', 'userName']),
deptId() {
return this.$store.state.user.deptId
},
filtereDate() {
if (this.searchQuery) {
return this.tableData.filter((item) => item.companyName.includes(this.searchQuery) || item.legalName.includes(this.searchQuery))
@@ -776,6 +786,12 @@ export default {
if (query.backUrl) {
this.selectedTab = query.selectedTab
this.queryParams.custPattern = query.selectedTab
} else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
this.selectedTab = deptIdStr.startsWith('875') ? '0' : '2'
this.queryParams.custPattern = this.selectedTab
}
},
mounted() {

View File

@@ -1,5 +1,22 @@
const commonCol = function (isCom, type) {
return [
// 当headId=875时需要隐藏的字段列表
const HIDDEN_PROPS_FOR_875 = [
'regionTopGridName', // 总行行政网格名称
'regionSecGridName', // 支行行政网格名称
'belongBranchName', // 行政网格归属支行
'belongOutletName', // 行政网格归属网点
'belongUserNameList', // 行政网格客户经理
'drawGridName', // 自绘地图网格名称
'drawBranchNames', // 自绘地图网格归属支行
'drawOutletNames', // 自绘地图网格归属网点
'drawUserNames', // 自绘地图网格客户经理
'virtualGridName', // 自建名单网格名称
'virtualBranchNames', // 自建名单网格归属支行
'virtualOutletNames', // 自建名单网格归属网点
'virtualUserNames', // 自建名单网格客户经理
]
const commonCol = function (isCom, type, headId) {
const allColumns = [
{
prop: 'regionTopGridName',
label: '总行行政网格名称',
@@ -101,9 +118,16 @@ const commonCol = function (isCom, type) {
showOverflowTooltip: true,
},
]
// 当headId=875时过滤掉指定的列
if (String(headId).startsWith('875')) {
return allColumns.filter(col => !HIDDEN_PROPS_FOR_875.includes(col.prop))
}
return allColumns
}
export const placeholderMap = (type) => ({
export const placeholderMap = (type, headId) => ({
'2': {
placeholder: '搜索企业名称/法人名字',
custPattern: '2',
@@ -138,7 +162,7 @@ export const placeholderMap = (type) => ({
type: 'myCustLevel',
// desc: '建档输入/取新华社数据',
},
...commonCol('2', type),
...commonCol('2', type, headId),
{
prop: 'lpName',
label: '法人姓名',
@@ -393,7 +417,7 @@ export const placeholderMap = (type) => ({
type: 'myCustIdsn',
// desc: '建档输入/取新华社数据',
},
...commonCol('1', type),
...commonCol('1', type, headId),
{
prop: 'lpName',
label: '经营者姓名',
@@ -634,7 +658,7 @@ export const placeholderMap = (type) => ({
type: 'myCustLevel',
// desc: '取大信贷数据',
},
...commonCol('0', type),
...commonCol('0', type, headId),
{
prop: 'custPhone',
label: '联系方式',

View File

@@ -2,9 +2,16 @@
<div class="customer-wrap">
<div>
<el-radio-group class="header-radio" v-model="selectedTab" @change="handleTabChange">
<el-radio-button label="2" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button> // :disabled="isPrivate"
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
</template>
<template v-else>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
</template>
</el-radio-group>
<div class="customerMain">
<div :class="iscollapsed ? 'customerMain_left_sq' : 'customerMain_left_zk'">
@@ -283,8 +290,9 @@ export default {
const type = this.roles.includes('headPublic') ? 'isPublic' :
this.roles.includes('headPrivate') ? 'isPrivate' :
this.roles.includes('headOps') ? 'isOps' : ""
this.tableColoumns = placeholderMap(type)[val].tableColoumns
this.placeholder = placeholderMap(type)[val].placeholder
const headId = this.deptId
this.tableColoumns = placeholderMap(type, headId)[val].tableColoumns
this.placeholder = placeholderMap(type, headId)[val].placeholder
this.queryParams.custPattern = val
this.searchColoumns = this.getSearchColoumns()
}
@@ -310,6 +318,9 @@ export default {
},
computed: {
...mapGetters(['roles']),
deptId() {
return this.$store.state.user.deptId
},
filtereDate() {
if (this.searchQuery) {
return this.tableData.filter((item) => item.companyName.includes(this.searchQuery) || item.legalName.includes(this.searchQuery))
@@ -777,6 +788,12 @@ export default {
if (query.backUrl) {
this.selectedTab = query.selectedTab
this.queryParams.custPattern = query.selectedTab
} else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
this.selectedTab = deptIdStr.startsWith('875') ? '0' : '2'
this.queryParams.custPattern = this.selectedTab
}
},
mounted() {

View File

@@ -0,0 +1,680 @@
<template>
<el-dialog
:title="isEdit ? '编辑客群' : '创建客群'"
:visible.sync="visible"
width="700px"
:before-close="handleClose"
append-to-body
>
<el-alert
v-if="isEdit && isProcessing"
title="客群正在处理中,请等待处理完成后再进行编辑"
type="warning"
:closable="false"
style="margin-bottom: 15px"
/>
<el-form ref="form" :model="form" :rules="rules" label-width="120px" v-loading="submitting">
<el-form-item label="客群名称" prop="groupName">
<el-input v-model="form.groupName" placeholder="请输入客群名称" maxlength="50" />
</el-form-item>
<el-form-item label="客群模式" prop="groupMode">
<el-radio-group v-model="form.groupMode" :disabled="isEdit && form.createStatus === '1'">
<el-radio label="0">静态客群</el-radio>
<el-radio label="1">动态客群</el-radio>
</el-radio-group>
<div class="form-tip">
静态客群创建后客户列表固定除非手动更新
<br />
动态客群系统定期根据原始条件自动刷新客户列表
</div>
</el-form-item>
<el-form-item label="有效期" prop="validTime">
<el-date-picker
v-model="form.validTime"
type="datetime"
placeholder="选择有效期截止时间"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%"
/>
<div class="form-tip">留空表示永久有效</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="2" />
</el-form-item>
<el-form-item label="开启共享">
<el-switch v-model="shareEnabled" />
</el-form-item>
<el-form-item label="可见部门" v-if="shareEnabled">
<el-select
v-model="form.shareDeptIdList"
multiple
placeholder="请选择可见部门"
style="width: 100%"
>
<el-option
v-for="dept in deptOptions"
:key="dept.deptId"
:label="dept.deptName"
:value="dept.deptId"
/>
</el-select>
</el-form-item>
<el-form-item label="创建方式" prop="createMode">
<el-radio-group v-model="form.createMode" :disabled="isEdit">
<el-radio label="1">模板导入</el-radio>
<el-radio label="2">绩效网格</el-radio>
<el-radio label="3">地理网格</el-radio>
<el-radio label="4">绘制网格</el-radio>
</el-radio-group>
</el-form-item>
<!-- 模板导入 -->
<template v-if="form.createMode === '1'">
<el-form-item label="客户文件" prop="file">
<el-upload
ref="upload"
:action="uploadUrl"
:headers="uploadHeaders"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-change="handleFileChange"
:auto-upload="false"
:show-file-list="true"
:limit="1"
accept=".xlsx,.xls"
>
<el-button slot="trigger" size="small" icon="el-icon-upload">选择文件</el-button>
<el-button size="small" type="text" @click.stop="downloadTemplate" style="margin-left: 15px">
下载模板
</el-button>
<div slot="tip" class="el-upload__tip">
仅支持Excel文件文件大小不超过10MB
</div>
</el-upload>
</el-form-item>
</template>
<!-- 绩效网格 -->
<template v-if="form.createMode === '2'">
<el-form-item label="业务类型" prop="cmpmBizType">
<el-select v-model="gridForm.cmpmBizType" placeholder="请选择业务类型" style="width: 100%" @change="handleCmpmBizTypeChange">
<el-option label="零售" value="retail" />
<el-option label="对公" value="corporate" />
<el-option label="对公账户" value="corporate_account" />
</el-select>
</el-form-item>
<el-form-item label="客户经理" prop="userNames">
<el-select
v-model="gridForm.userNames"
multiple
filterable
placeholder="请选择客户经理"
style="width: 100%"
>
<el-option
v-for="user in userOptions"
:key="user.userName"
:label="user.nickName"
:value="user.userName"
/>
</el-select>
</el-form-item>
</template>
<!-- 地理网格 -->
<template v-if="form.createMode === '3'">
<el-form-item label="网格级别">
<el-radio-group v-model="regionQuery.gridLevel">
<el-radio label="1">总行行政网格</el-radio>
<el-radio label="2">支行行政网格</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="责任类型">
<el-select v-model="regionQuery.gridDutyType" placeholder="请选择责任类型" style="width: 100%" clearable>
<el-option label="责任网格" value="1" />
<el-option label="竞争网格" value="2" />
</el-select>
</el-form-item>
<el-form-item label="网格名称">
<el-input v-model="regionQuery.gridName" placeholder="请输入网格名称(可选)" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadRegionGridOptions" :loading="regionLoading">查询网格</el-button>
<el-button icon="el-icon-refresh" @click="resetRegionQuery">重置条件</el-button>
</el-form-item>
<el-form-item label="选择网格">
<el-select
v-model="gridForm.regionGridIds"
multiple
filterable
collapse-tags
placeholder="请先查询网格,然后选择"
style="width: 100%"
>
<el-option
v-for="grid in regionGridOptions"
:key="grid.gridId"
:label="grid.gridName"
:value="grid.gridId"
/>
</el-select>
</el-form-item>
</template>
<!-- 绘制网格 -->
<template v-if="form.createMode === '4'">
<el-form-item label="绘制网格" prop="drawGridIds">
<el-select
v-model="gridForm.drawGridIds"
multiple
filterable
placeholder="请选择绘制网格"
style="width: 100%"
>
<el-option
v-for="grid in drawGridOptions"
:key="grid.gridId"
:label="grid.gridName"
:value="grid.gridId"
/>
</el-select>
</el-form-item>
</template>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button>
<el-button type="primary" :loading="submitting" :disabled="isProcessing" @click="handleSubmit">
{{ submitButtonText }}
</el-button>
</div>
</el-dialog>
</template>
<script>
import {
createCustGroupByGrid,
createCustGroupByTemplate,
updateCustGroupByGrid,
updateCustGroupByTemplate,
downloadTemplate,
getManagerList,
getRegionGridListForGroup
} from '@/api/group/custGroup'
import { getToken } from '@/utils/auth'
import { listUser } from '@/api/system/user'
import { listDept } from '@/api/system/dept'
import { getSimpleGridList } from '@/api/grid/list/gridlist'
export default {
name: 'CreateDialog',
props: {
visible: {
type: Boolean,
default: false
},
groupData: {
type: Object,
default: () => ({})
},
isEdit: {
type: Boolean,
default: false
}
},
data() {
return {
// 提交中状态
submitting: false,
// 上传地址
uploadUrl: process.env.VUE_APP_BASE_API + '/group/cust/createByTemplate',
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
// 处理中提示
processTipVisible: false
}
},
computed: {
// 是否正在处理中(创建或更新)
isProcessing() {
return this.form.createStatus === '0'
},
// 提交按钮文本
submitButtonText() {
if (this.submitting) return '提交中...'
if (this.isEdit && this.isProcessing) return '客群处理中,请稍后...'
return '确 定'
}
},
data() {
return {
// 提交中状态
submitting: false,
// 上传地址
uploadUrl: process.env.VUE_APP_BASE_API + '/group/cust/createByTemplate',
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
// 处理中提示
processTipVisible: false,
// 表单数据
form: {
id: null,
groupName: null,
groupMode: '0',
createMode: '1',
groupStatus: '0',
shareEnabled: 0,
shareDeptIdList: [],
remark: null,
validTime: null
},
// 共享开关
shareEnabled: false,
// 网格表单数据
gridForm: {
gridType: '0',
cmpmBizType: null,
userNames: [],
regionGridIds: [],
drawGridIds: []
},
// 上传文件
uploadFile: null,
// 表单验证规则
rules: {
groupName: [
{ required: true, message: '请输入客群名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
groupMode: [{ required: true, message: '请选择客群模式', trigger: 'change' }],
createMode: [{ required: true, message: '请选择创建方式', trigger: 'change' }]
},
// 部门选项
deptOptions: [],
// 用户选项
userOptions: [],
// 地理网格选项
regionGridOptions: [],
// 地理网格查询条件
regionQuery: {
gridLevel: '1',
gridDutyType: null,
gridName: null
},
// 地理网格加载状态
regionLoading: false,
// 绘制网格选项
drawGridOptions: []
}
},
watch: {
visible(val) {
if (val) {
this.init()
}
},
groupData: {
handler(val) {
if (val && Object.keys(val).length > 0) {
this.form = { ...val }
this.shareEnabled = val.shareEnabled === 1
// 解析 shareDeptIds 字符串为 shareDeptIdList 数组
if (val.shareDeptIds) {
this.form.shareDeptIdList = val.shareDeptIds.split(',').filter(id => id)
} else {
this.form.shareDeptIdList = []
}
// 反显网格数据(需要在 form.createMode watch 之后执行)
this.$nextTick(() => {
this.restoreGridData(val)
})
}
},
immediate: true
},
'form.createMode'(val) {
// 重置网格表单
this.gridForm = {
gridType: val === '2' ? '0' : val === '3' ? '1' : '2',
regionGridIds: [],
drawGridIds: []
}
// 切换到地理网格模式时,重置查询条件
if (val === '3') {
this.resetRegionQuery()
}
// 切换到绘制网格模式时,加载绘制网格列表
if (val === '4') {
this.loadDrawGridOptions()
}
},
'gridForm.cmpmBizType'(val) {
// 当业务类型改变时,清空已选择的客户经理
if (val) {
this.gridForm.userNames = []
this.loadManagerOptions()
}
},
'form.createStatus'(val) {
// 监听创建状态变化,显示提示
if (this.isEdit && val === '0') {
this.processTipVisible = true
} else if (this.processTipVisible) {
this.processTipVisible = false
}
}
},
created() {
this.loadDeptOptions()
},
methods: {
/** 初始化 */
init() {
this.$nextTick(() => {
this.$refs.form && this.$refs.form.clearValidate()
// 编辑模式下,恢复网格数据
if (this.isEdit && this.groupData && Object.keys(this.groupData).length > 0) {
this.restoreGridData(this.groupData)
}
})
},
/** 反显网格数据 */
restoreGridData(data) {
if (!this.isEdit) return
const createMode = String(data.createMode)
// 根据创建方式反显网格数据
if (createMode === '2' && data.gridType === '0') {
// 绩效网格
this.gridForm.gridType = '0'
this.gridForm.cmpmBizType = data.cmpmBizType
if (data.gridUserNames) {
this.gridForm.userNames = data.gridUserNames.split(',').filter(n => n)
// 加载客户经理选项
this.loadManagerOptions()
}
} else if (createMode === '3' && data.gridType === '1') {
// 地理网格 - 需要查询所有网格以便正确反显
this.gridForm.gridType = '1'
// 设置默认查询条件(获取所有网格)
this.regionQuery = {
gridLevel: '1',
gridDutyType: null,
gridName: null
}
if (data.regionGridIds) {
this.gridForm.regionGridIds = data.regionGridIds.split(',').map(id => parseInt(id)).filter(id => id)
// 加载地理网格选项(无查询条件,获取全部)
this.loadRegionGridOptions()
}
} else if (createMode === '4' && data.gridType === '2') {
// 绘制网格
this.gridForm.gridType = '2'
if (data.drawGridIds) {
this.gridForm.drawGridIds = data.drawGridIds.split(',').map(id => parseInt(id)).filter(id => id)
// 加载绘制网格选项
this.loadDrawGridOptions()
}
}
},
/** 加载部门选项 */
loadDeptOptions() {
listDept().then(response => {
this.deptOptions = response.data || []
}).catch(() => {
this.deptOptions = []
})
},
/** 加载用户选项 */
loadUserOptions() {
listUser().then(response => {
this.userOptions = response.rows || []
}).catch(() => {
this.userOptions = []
})
},
/** 根据业务类型加载客户经理选项 */
loadManagerOptions() {
if (!this.gridForm.cmpmBizType) {
this.userOptions = []
return
}
getManagerList(this.gridForm.cmpmBizType).then(response => {
this.userOptions = response.data || []
}).catch(() => {
this.userOptions = []
})
},
/** 业务类型改变处理 */
handleCmpmBizTypeChange(val) {
// 清空已选择的客户经理
this.gridForm.userNames = []
},
/** 加载地理网格选项 */
loadRegionGridOptions() {
this.regionLoading = true
const params = {
gridLevel: this.regionQuery.gridLevel
}
if (this.regionQuery.gridDutyType) {
params.gridDutyType = this.regionQuery.gridDutyType
}
if (this.regionQuery.gridName) {
params.gridName = this.regionQuery.gridName
}
getRegionGridListForGroup(params).then(response => {
this.regionGridOptions = response.data || []
}).catch(() => {
this.regionGridOptions = []
}).finally(() => {
this.regionLoading = false
})
},
/** 重置地理网格查询条件 */
resetRegionQuery() {
this.regionQuery = {
gridLevel: '1',
gridDutyType: null,
gridName: null
}
this.gridForm.regionGridIds = []
},
/** 加载绘制网格选项 */
loadDrawGridOptions() {
getSimpleGridList().then(response => {
this.drawGridOptions = response.data || []
}).catch(() => {
this.drawGridOptions = []
})
},
/** 文件改变 */
handleFileChange(file) {
this.uploadFile = file.raw
},
/** 上传成功 */
handleUploadSuccess(response) {
this.$modal.msgSuccess('创建成功')
this.submitting = false
this.$emit('submit', { id: response.data })
this.handleClose()
},
/** 上传失败 */
handleUploadError() {
this.$modal.msgError('创建失败')
this.submitting = false
},
/** 下载模板 */
downloadTemplate() {
downloadTemplate().then(response => {
const url = window.URL.createObjectURL(new Blob([response]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', '客户信息模板.xlsx')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
},
/** 提交表单 */
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
// 处理共享设置
this.form.shareEnabled = this.shareEnabled ? 1 : 0
// 转换 shareDeptIdList 数组为 shareDeptIds 逗号分隔字符串
this.form.shareDeptIds = this.form.shareDeptIdList.join(',')
// 根据创建方式处理提交数据
if (this.form.createMode === '1') {
this.handleTemplateSubmit()
} else {
this.handleGridSubmit()
}
}
})
},
/** 模板导入提交 */
handleTemplateSubmit() {
if (!this.uploadFile && !this.isEdit) {
this.$modal.msgWarning('请选择要上传的文件')
return
}
this.submitting = true
// 构建表单数据
const formData = new FormData()
formData.append('dto', JSON.stringify(this.form))
if (this.uploadFile) {
formData.append('file', this.uploadFile)
}
if (this.isEdit) {
// 编辑模式:重新导入模板文件
updateCustGroupByTemplate(formData).then(response => {
this.$modal.msgSuccess('客群更新中,请稍后刷新查看')
this.submitting = false
this.$emit('submit', { id: this.form.id })
this.handleClose()
}).catch(() => {
this.submitting = false
})
} else {
// 新增模式
createCustGroupByTemplate(formData).then(response => {
this.$modal.msgSuccess('客群创建中,请稍后刷新查看')
this.submitting = false
this.$emit('submit', { id: response.data })
this.handleClose()
}).catch(() => {
this.submitting = false
})
}
},
/** 网格导入提交 */
handleGridSubmit() {
this.submitting = true
// 构建提交数据
const submitData = {
custGroup: { ...this.form },
gridType: this.form.createMode === '2' ? '0' : this.form.createMode === '3' ? '1' : '2',
cmpmBizType: this.gridForm.cmpmBizType,
userNames: this.gridForm.userNames,
regionGridIds: this.gridForm.regionGridIds,
drawGridIds: this.gridForm.drawGridIds
}
if (this.isEdit) {
// 编辑模式:重新导入网格
updateCustGroupByGrid(submitData).then(response => {
// 使用后端返回的消息
const msg = response.msg || '客群更新成功'
if (msg.includes('更新中')) {
this.$modal.msgSuccess('客群更新中,请稍后刷新查看')
} else {
this.$modal.msgSuccess(msg)
}
this.submitting = false
this.$emit('submit', { id: this.form.id })
this.handleClose()
}).catch(() => {
this.submitting = false
})
} else {
// 新增模式
createCustGroupByGrid(submitData).then(response => {
this.$modal.msgSuccess('客群创建中,请稍后刷新查看')
this.submitting = false
this.$emit('submit', { id: response.data })
this.handleClose()
}).catch(() => {
this.submitting = false
})
}
},
/** 关闭弹窗 */
handleClose() {
this.$emit('update:visible', false)
this.resetForm()
},
/** 重置表单 */
resetForm() {
this.form = {
id: null,
groupName: null,
groupMode: '0',
createMode: '1',
groupStatus: '0',
shareEnabled: 0,
shareDeptIdList: [],
remark: null,
validTime: null
}
this.shareEnabled = false
this.gridForm = {
gridType: '0',
cmpmBizType: null,
userNames: [],
regionGridIds: [],
drawGridIds: []
}
this.uploadFile = null
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
this.$nextTick(() => {
this.$refs.form && this.$refs.form.clearValidate()
})
}
}
}
</script>
<style scoped>
.form-tip {
font-size: 12px;
color: #909399;
line-height: 1.5;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<div class="customer-wrap detail-wrap">
<!-- 页面头部 -->
<div class="page-header">
<el-button icon="el-icon-arrow-left" size="small" @click="goBack">返回</el-button>
<span class="page-title">客群客户列表</span>
</div>
<!-- 搜索区域 -->
<div class="search-area">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
<el-form-item label="客户类型" prop="custType">
<el-select v-model="queryParams.custType" placeholder="请选择" clearable style="width: 150px">
<el-option label="个人" value="0" />
<el-option label="商户" value="1" />
<el-option label="企业" value="2" />
</el-select>
</el-form-item>
<el-form-item label="客户姓名" prop="custName">
<el-input
v-model="queryParams.custName"
placeholder="请输入客户姓名"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 表格区域 -->
<div class="main_table">
<el-table v-loading="loading" :data="memberList">
<el-table-column label="序号" type="index" width="60" align="center" :index="indexMethod" />
<el-table-column label="客户类型" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.custType === '0'" size="small">个人</el-tag>
<el-tag v-else-if="scope.row.custType === '1'" size="small" type="warning">商户</el-tag>
<el-tag v-else-if="scope.row.custType === '2'" size="small" type="success">企业</el-tag>
</template>
</el-table-column>
<el-table-column label="客户号" prop="custId" width="150" />
<el-table-column label="客户姓名" prop="custName" width="120" />
<el-table-column label="身份证号" prop="custIdc" show-overflow-tooltip />
<el-table-column label="统信码" prop="socialCreditCode" show-overflow-tooltip />
<el-table-column label="添加时间" prop="createTime" width="180" />
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div>
</template>
<script>
import { listCustGroupMembers } from '@/api/group/custGroup'
export default {
name: 'CustGroupDetail',
data() {
return {
loading: false,
groupId: null,
memberList: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
custType: null,
custName: null
}
}
},
created() {
this.groupId = this.$route.query.groupId
if (this.groupId) {
this.getList()
} else {
this.$modal.msgError('缺少客群ID参数')
this.goBack()
}
},
watch: {
'$route.query.groupId'(newGroupId) {
if (newGroupId && newGroupId !== this.groupId) {
this.groupId = newGroupId
this.queryParams.pageNum = 1
this.getList()
}
}
},
methods: {
/** 查询客户列表 */
getList(param) {
if (param) {
this.queryParams.pageNum = param.page
this.queryParams.pageSize = param.limit
}
this.loading = true
listCustGroupMembers(this.groupId, this.queryParams).then(response => {
this.memberList = response.rows
this.total = response.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
/** 搜索 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置 */
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
/** 返回 */
goBack() {
this.$router.push({ path: '/group/custGroup' })
},
/** 序号计算方法 */
indexMethod(index) {
return (this.queryParams.pageNum - 1) * this.queryParams.pageSize + index + 1
}
}
}
</script>
<style lang="scss" scoped>
.customer-wrap {
background-color: #ffffff;
overflow: hidden;
box-shadow: 0 3px 8px 0 #00000017;
border-radius: 16px 16px 0 0;
padding: 24px 30px;
&.detail-wrap {
.page-header {
display: flex;
align-items: center;
margin-bottom: 24px;
.page-title {
font-size: 16px;
font-weight: 500;
color: #222222;
margin-left: 16px;
}
}
}
.search-area {
padding: 16px 0;
border-bottom: 1px solid #ebebeb;
margin-bottom: 16px;
.el-form {
margin-bottom: -8px;
.el-form-item {
margin-bottom: 8px;
}
}
}
.main_table {
::v-deep .el-pagination {
margin-top: 15px;
}
}
}
</style>

View File

@@ -0,0 +1,288 @@
<template>
<div class="customer-wrap">
<!-- 搜索区域 -->
<div class="search-area" v-show="showSearch">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
<el-form-item label="客群名称" prop="groupName">
<el-input
v-model="queryParams.groupName"
placeholder="请输入客群名称"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="客群模式" prop="groupMode">
<el-select v-model="queryParams.groupMode" placeholder="请选择" clearable style="width: 150px">
<el-option label="静态" value="0" />
<el-option label="动态" value="1" />
</el-select>
</el-form-item>
<el-form-item label="创建方式" prop="createMode">
<el-select v-model="queryParams.createMode" placeholder="请选择" clearable style="width: 150px">
<el-option label="模板导入" value="1" />
<el-option label="绩效网格" value="2" />
<el-option label="地理网格" value="3" />
<el-option label="绘制网格" value="4" />
</el-select>
</el-form-item>
<el-form-item label="客群状态" prop="groupStatus">
<el-select v-model="queryParams.groupStatus" placeholder="请选择" clearable style="width: 150px">
<el-option label="正常" value="0" />
<el-option label="已禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 操作栏 -->
<section class="operate-cnt">
<div class="operate-left">
<el-button type="primary" icon="el-icon-plus" size="small" @click="handleAdd">新增</el-button>
<el-button type="danger" icon="el-icon-delete" size="small" :disabled="multiple" @click="handleDelete">删除</el-button>
</div>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</section>
<!-- 表格区域 -->
<div class="main_table">
<el-table v-loading="loading" :data="groupList" @selection-change="handleSelectionChange" style="width: 100%" max-height="625">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="客群名称" prop="groupName" min-width="180" show-overflow-tooltip />
<el-table-column label="客群模式" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.groupMode === '0'" type="info" size="small">静态</el-tag>
<el-tag v-else-if="scope.row.groupMode === '1'" type="success" size="small">动态</el-tag>
</template>
</el-table-column>
<el-table-column label="创建方式" align="center" width="120">
<template slot-scope="scope">
<span v-if="scope.row.createMode === '1'">模板导入</span>
<span v-else-if="scope.row.createMode === '2'">绩效网格</span>
<span v-else-if="scope.row.createMode === '3'">地理网格</span>
<span v-else-if="scope.row.createMode === '4'">绘制网格</span>
</template>
</el-table-column>
<el-table-column label="客户数量" align="center" prop="custCount" width="100" />
<el-table-column label="客群状态" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.groupStatus === '0'" type="success" size="small">正常</el-tag>
<el-tag v-else-if="scope.row.groupStatus === '1'" type="danger" size="small">已禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="创建者" prop="nickName" width="120" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" width="180" />
<el-table-column label="操作" align="center" width="180" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</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"
/>
</div>
<!-- 创建/编辑客群弹窗 -->
<create-dialog
:visible.sync="dialogVisible"
:group-data="form"
:is-edit="isEdit"
@submit="handleSubmit"
/>
</div>
</template>
<script>
import { listCustGroup, getCustGroup, deleteCustGroup } from '@/api/group/custGroup'
import CreateDialog from './components/create-dialog'
export default {
name: 'CustGroup',
components: { CreateDialog },
data() {
return {
// 加载状态
loading: false,
// 显示搜索
showSearch: true,
// 选中ID数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 客群列表
groupList: [],
// 弹窗显示
dialogVisible: false,
// 是否编辑
isEdit: false,
// 表单数据
form: {},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
groupName: null,
groupMode: null,
createMode: null,
groupStatus: null
}
}
},
created() {
this.getList()
},
methods: {
/** 查询客群列表 */
getList() {
this.loading = true
listCustGroup(this.queryParams).then(response => {
this.groupList = response.rows
this.total = response.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
/** 搜索按钮操作 */
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.isEdit = false
this.dialogVisible = true
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id = row.id || this.ids[0]
getCustGroup(id).then(response => {
this.form = response.data
this.isEdit = true
this.dialogVisible = true
})
},
/** 查看按钮操作 */
handleView(row) {
this.$router.push({
path: '/group/custGroup/detail',
query: { groupId: row.id }
})
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id ? [row.id] : this.ids
this.$modal.confirm('是否确认删除选中的客群?').then(() => {
return deleteCustGroup(ids)
}).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
/** 提交表单 */
handleSubmit() {
this.dialogVisible = false
this.getList()
},
/** 表单重置 */
reset() {
this.form = {
id: null,
groupName: null,
groupMode: '0',
createMode: null,
groupStatus: '0',
shareEnabled: 0,
shareDeptIdList: [],
remark: null,
validTime: null
}
}
}
}
</script>
<style lang="scss" scoped>
.customer-wrap {
background-color: #ffffff;
overflow: hidden;
box-shadow: 0 3px 8px 0 #00000017;
border-radius: 16px 16px 0 0;
padding: 24px 30px;
.search-area {
padding: 16px 0;
border-bottom: 1px solid #ebebeb;
margin-bottom: 16px;
.el-form {
margin-bottom: -8px;
.el-form-item {
margin-bottom: 8px;
}
}
}
.operate-cnt {
display: flex;
justify-content: space-between;
align-items: center;
margin: 24px 0 16px 0;
.operate-left {
display: flex;
gap: 8px;
}
.el-button {
border-radius: 4px;
}
}
.main_table {
::v-deep .el-pagination {
margin-top: 15px;
}
}
}
</style>

View File

@@ -20,6 +20,7 @@
<span style="font-size: 14px;">较上月变动</span>
<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-if="item.inc == 0 || item.inc === '0'" style=" font-size: 14px;color: #409EFF">{{ changeData(item.inc) }}</span>
<span v-else style=" font-size: 14px;color: #EF3F35">{{ changeData(item.inc) }}<i
class="el-icon-caret-top"></i></span>
</div>
@@ -43,6 +44,11 @@
@click="goToCustManager(item.itemNm, 'fall')">
{{ item.curAmt }}<i class="el-icon-caret-bottom"></i>
</span>
<span v-else-if="item.curAmt == 0 || item.curAmt === '0'"
style="font-size: 14px; color: #409EFF; cursor: pointer;"
@click="goToCustManager(item.itemNm, 'rise')">
{{ item.curAmt }}
</span>
<span v-else
style="font-size: 14px; color: #EF3F35; cursor: pointer;"
@click="goToCustManager(item.itemNm, 'rise')">
@@ -60,7 +66,7 @@
<el-radio-button label="4">预警任务</el-radio-button>
<el-radio-button label="5">二次走访提醒</el-radio-button>
<el-radio-button label="6">走访资源提醒</el-radio-button>
<el-radio-button label="7">营销任务</el-radio-button>
<el-radio-button label="7" v-if="!shouldHideFor875">营销任务</el-radio-button>
</el-radio-group>
</div>
<el-table v-if="selectedTab==='3'" key="3" :data="tableData" :loading="loading" style="width: 100%;margin-top: 20px;">
@@ -152,7 +158,7 @@
</template>
</el-table-column>
</el-table>
<el-table v-if="selectedTab==='7'" key="7" :data="tableData" :loading="loading" style="width: 100%;margin-top: 20px;">
<el-table v-if="selectedTab==='7' && !shouldHideFor875" key="7" :data="tableData" :loading="loading" style="width: 100%;margin-top: 20px;">
<el-table-column label="营销任务" width="200" prop="marketTaskName" min-width="100" show-overflow-tooltip />
<el-table-column label="客户姓名" prop="custName" min-width="100" show-overflow-tooltip />
<el-table-column label="客户号" width="200" prop="custId" min-width="140" show-overflow-tooltip />
@@ -194,9 +200,9 @@
<el-table-column label="结束时间" width="200" prop="endTime" min-width="100" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
</el-table>
<el-pagination @size-change="handleAgentSizeChange" @current-change="handleAgentCurrentChange"
class="warnPagination" :page-sizes="[5, 10, 20, 30]" :page-size="agentPageSize"
layout="->,total,sizes,prev,pager,next" :total="agentTotal" :current-page="agentPageNum"></el-pagination>
<el-pagination @current-change="handleAgentCurrentChange"
class="warnPagination" :page-size="5"
layout="->,total,prev,pager,next" :total="agentTotal" :current-page="agentPageNum"></el-pagination>
</div>
</div>
<div class="page-vr">
@@ -210,7 +216,7 @@
</span>
</p>
<ul class="operate-cnt">
<li v-for="(it, ind) in optArr" :key="ind" class="setLi" @mouseenter="onMouseOver(ind, true)"
<li v-for="(it, ind) in filteredOptArr" :key="ind" class="setLi" @mouseenter="onMouseOver(ind, true)"
@mouseleave="onMouseOver(ind, false)">
<div class="noSetting" v-if="it.setShow">
<svg-icon :icon-class="getCurrentImg(ind)" :class="isSetting ? 'svg-icon-imgSetting' : 'svg-icon-img'"
@@ -230,7 +236,7 @@
</li>
</ul>
</div>
<div class="page-vr-middle page-common-wrap no-padding-cnt page-vr-top" id="yjxxMain">
<div class="page-vr-middle page-common-wrap no-padding-cnt page-vr-top" id="yjxxMain" v-if="!shouldHideFor875">
<p class="page-title page-btm">预警信息</p>
<ul class="common-ul">
<li v-for="(it, ind) in warnArr" :key="ind" style="cursor: pointer;" @click="handleWarn(it)" class="yjxxLi">
@@ -857,6 +863,27 @@ export default {
},
showposition() {
return this.userName.slice(0, 3) === '875'
},
// headId为875时隐藏预警信息和营销任务
shouldHideFor875() {
return this.userName && (this.userName + '').substring(0, 3) === '875'
},
// headId为875时便捷操作只显示快速入门
filteredOptArr() {
// 当deptId以875开头时只保留快速入门
if (this.deptId && String(this.deptId).startsWith('875')) {
return this.optArr.filter(item => item.name === '快速入门')
}
return this.optArr
}
},
watch: {
selectedTab(newVal, oldVal) {
// 切换tab时重置分页为第1页
if (newVal !== oldVal) {
this.pageNum = 1
this.agentPageNum = 1
}
}
},
mounted() {
@@ -1135,8 +1162,7 @@ export default {
this.getData()
},
handleAgentCurrentChange(val) {
// this.agentPageNum = val
// this.initBranchList()
this.agentPageNum = val
this.pageNum = val
this.getData()
},
@@ -1565,6 +1591,8 @@ p {
.page-vr {
width: 24%;
margin-left: 1%;
display: flex;
flex-direction: column;
}
.page-title {
@@ -1602,7 +1630,11 @@ p {
}
.page-vr-middle {
margin: 22px 0;
margin-bottom: 22px;
}
.page-vr-top {
margin-bottom: 22px;
}
.page-vl-top {
@@ -1873,7 +1905,7 @@ p {
.page-vr-btm {
height: 350px;
overflow-y: scroll;
margin-bottom: 30px;
margin-bottom: 22px;
}
::v-deep .el-badge__content {
@@ -1943,8 +1975,10 @@ p {
}
.url-box {
max-height: 210px;
flex: 1;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
.yjxxLi {

View File

@@ -6,24 +6,46 @@
v-model="selectedTab"
@input="handleChange"
>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button
>
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button
>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button
>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button
>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button
>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button
>
</template>
<template v-else>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button
>
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button
>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button
>
</template>
</el-radio-group>
<div class="searchForm">
<el-form
@@ -403,6 +425,9 @@ export default {
},
computed: {
...mapGetters(["roles", "userName"]),
deptId() {
return this.$store.state.user.deptId
},
//总行
isHeadAdmin() {
return this.roles.includes("headAdmin");
@@ -441,12 +466,17 @@ export default {
if (selectedTab) {
this.selectedTab = selectedTab;
} else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
const defaultTab = deptIdStr.startsWith('875') ? '0' : '2'
if (this.isPublic) {
this.selectedTab = '2'
} else if (this.isPrivate) {
this.selectedTab = '0'
this.selectedTab = defaultTab
} else {
this.selectedTab = '2'
this.selectedTab = defaultTab
}
}
this.initVisitingTaskList();

View File

@@ -64,9 +64,16 @@
</div>
<div class="content">
<el-radio-group class="header-radio" v-model="selectedTab" @input="handleChange">
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
</template>
<template v-else>
<el-radio-button label="2" :disabled="isPrivate" :class="{ 'btn-disabled': isPrivate }">企业</el-radio-button>
<el-radio-button label="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">商户</el-radio-button>
</template>
</el-radio-group>
<!-- <div class="taskTop">
<div class="taskTop_left">
@@ -292,6 +299,9 @@ export default {
},
computed: {
...mapGetters(['roles']),
deptId() {
return this.$store.state.user.deptId
},
//总行
isHeadAdmin() {
return this.roles.includes('headAdmin')
@@ -334,12 +344,17 @@ export default {
delete this.$route.query[key];
}
} else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
const defaultTab = deptIdStr.startsWith('875') ? '0' : '2'
if (this.isPublic) {
this.selectedTab = '2'
} else if (this.isPrivate) {
this.selectedTab = '0'
this.selectedTab = defaultTab
} else {
this.selectedTab = '2'
this.selectedTab = defaultTab
}
}
if (this.isBranchAdmin) {

View File

@@ -6,21 +6,40 @@
class="header-radio"
@input="handleChange"
>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button>
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button>
<template v-if="String(deptId).substring(0, 3) === '875'">
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button>
</template>
<template v-else>
<el-radio-button
label="2"
:disabled="isPrivate"
:class="{ 'btn-disabled': isPrivate }"
>企业</el-radio-button>
<el-radio-button
label="0"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>个人</el-radio-button>
<el-radio-button
label="1"
:disabled="isPublic"
:class="{ 'btn-disabled': isPublic }"
>商户</el-radio-button>
</template>
</el-radio-group>
<div class="taskTop">
<div class="taskTop_left">
@@ -1180,6 +1199,9 @@ export default {
},
computed: {
...mapGetters(['roles']),
deptId() {
return this.$store.state.user.deptId
},
// 总行
isHeadAdmin() {
return this.roles.includes('headAdmin')
@@ -1215,17 +1237,22 @@ export default {
created() {
// getGroupInfoByGroupId({})
this.isUserType()
// 根据deptId动态设置默认tab默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
const defaultTab = deptIdStr.startsWith('875') ? '0' : '2'
if (this.isPublic) {
this.selectedTab = '2'
this.custTypeList = [{ label: '企业', value: '2' }]
} else if (this.isPrivate) {
this.selectedTab = '0'
this.selectedTab = defaultTab
this.custTypeList = [
{ label: '个人', value: '0' },
{ label: '商户', value: '1' }
]
} else {
this.selectedTab = '2'
this.selectedTab = defaultTab
this.custTypeList = [
{ label: '个人', value: '0' },
{ label: '商户', value: '1' },

View File

@@ -1,29 +1,25 @@
<template>
<div class="app-container" style="padding: 0px !important;">
<el-date-picker
v-model="reportTime"
placeholder="请选择日期"
@change="initCardList"
class="time-picker"
/>
<el-row :gutter="24" style="padding: 0 30px;margin-left: -15px;">
<el-row :gutter="24" style="padding: 0 30px;">
<el-col :span="6">
<el-card class="box-card">
<div class="my-span-checklist-title">
总预警推送次数
<el-card class="stat-card stat-card-blue">
<div class="stat-icon">
<i class="el-icon-bell"></i>
</div>
<div class="my-span-checklist-main">
<span>{{ cardInfo.alterCount }}</span>
<div class="stat-content">
<div class="stat-title">总预警推送次数</div>
<div class="stat-value">{{ cardInfo.alterCount }}</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="box-card1">
<div class="my-span-checklist-title">
反馈完成率
<el-card class="stat-card stat-card-blue2">
<div class="stat-icon">
<i class="el-icon-data-analysis"></i>
</div>
<div class="my-span-checklist-main">
<span>{{ cardInfo.completeRate + '%' }}</span>
<div class="stat-content">
<div class="stat-title">反馈完成率</div>
<div class="stat-value">{{ cardInfo.completeRate + '%' }}</div>
</div>
</el-card>
</el-col>
@@ -35,6 +31,14 @@
:inline="true"
style="margin-top:20px;"
>
<el-form-item label="日期">
<el-date-picker
v-model="reportTime"
placeholder="请选择日期"
@change="initCardList"
style="width:100%"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="searchArray.status" placeholder="请选择状态" style="width: 100%" clearable>
<el-option
@@ -46,20 +50,26 @@
</el-select>
</el-form-item>
<el-form-item label="预警类型" prop="alterType">
<el-input
<el-select
v-model="searchArray.alterType"
placeholder="请输入预警类型"
placeholder="请选择预警类型"
clearable
style="width:100%"
>
</el-input>
<el-option
v-for="item in alterTypeOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="searchFn">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetFn">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="tableData" :height="dyHeight" :key="tableKey">
<el-table v-loading="loading" :data="tableData">
<template>
<el-table-column label="序号" prop="xh" width="80">
<template slot-scope="scope">
@@ -91,17 +101,15 @@
</el-table-column> -->
</template>
</el-table>
<div class="pagination_end">
<el-pagination
layout="total, prev, pager, next, jumper"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
@current-change="currentChangeFn"
:total="total"
@size-change="sizeChangeFn"
></el-pagination>
</div>
<el-pagination
:page-sizes="[10, 20, 30, 50]"
:page-size="pageSize"
layout="->,total,prev,pager,next,sizes"
:total="total"
:current-page="pageNum"
@current-change="currentChangeFn"
@size-change="sizeChangeFn"
></el-pagination>
<el-dialog
:title="dialogTitle"
@@ -199,7 +207,8 @@
import {
warningworkRecordList,
warningworkRecordSubmit,
warningCardNum
warningCardNum,
getAlterTypes
} from "@/api/system/home";
import _ from "lodash";
import dayjs from "dayjs";
@@ -243,6 +252,8 @@ export default {
}
],
alterTypeOptions: [],
searchArray: {
status: "",
alterType: ""
@@ -252,8 +263,6 @@ export default {
tableData: [],
total: 0,
loading: false,
dyHeight: {},
tableKey: false,
dialogTitle: '',
dialogForm: {
custName: '',
@@ -277,12 +286,9 @@ export default {
};
},
created() {
window.addEventListener("resize", () => {
this.dyHeight = Number(window.innerHeight) - 330;
});
this.dyHeight = Number(window.innerHeight) - 330;
this.resetFn();
this.initCardList()
this.initCardList();
this.getAlterTypeList();
},
filters: {
formatFilter(v, type, list) {
@@ -293,6 +299,13 @@ export default {
}
},
methods: {
getAlterTypeList() {
getAlterTypes().then(res => {
if (res.code === 200) {
this.alterTypeOptions = res.data || [];
}
});
},
initCardList() {
warningCardNum({ reportTime: this.reportTime ? dayjs(this.reportTime).format('YYYY-MM-DD') + ' 23:59:59' : '' }).then(res => {
this.cardInfo = res
@@ -326,7 +339,6 @@ export default {
} else {
this.$message.error(response.msg || "操作失败");
}
this.tableKey = !this.tableKey;
});
},
currentChangeFn(val) {
@@ -369,45 +381,79 @@ export default {
</script>
<style lang="scss" scoped>
.pagination_end {
margin-top: 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
::v-deep .el-pagination {
margin-top: 15px;
}
.time-picker {
margin: 20px 0 0 0px;
}
.box-card {
// 统计卡片样式
.stat-card {
height: 100px;
border-radius: 8px;
margin-top: 25px;
margin-left: -30px;
border: 3px solid #a8c2f5;
}
border: none;
display: flex;
align-items: center;
padding: 0 20px;
position: relative;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.box-card1 {
height: 100px;
border-radius: 8px;
margin-top: 25px;
border: 3px solid #a8c2f5;
}
// 左侧装饰条
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: rgba(255, 255, 255, 0.5);
}
.my-span-checklist-title {
font-size: 14px;
color: #666;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
margin-right: 12px;
flex-shrink: 0;
.my-span-checklist-main {
line-height: 40px;
font-size: 30px;
color: #222;
font-weight: bold;
i {
font-size: 22px;
color: #fff;
}
}
.stat-content {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
.stat-title {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
.stat-value {
font-size: 20px;
color: #fff;
font-weight: bold;
}
}
// 蓝色渐变卡片1左上深→右下浅
&.stat-card-blue {
background: linear-gradient(135deg, #1e7ee6 0%, #a0cfff 100%);
}
// 蓝色渐变卡片2左上浅→右下深
&.stat-card-blue2 {
background: linear-gradient(135deg, #a0cfff 0%, #1e7ee6 100%);
}
}
.header-radio {
display: flex;