Compare commits

...

8 Commits

93 changed files with 5555 additions and 726 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. 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 ### Backend Module Structure
# Maven 打包(跳过测试)
mvn clean package -Dmaven.test.skip=true
# 运行已打包的 JAR The backend is a multi-module Maven project with the following modules:
cd ruoyi-admin/target
java -jar -Xms256m -Xmx1024m ruoyi-admin.jar
# 后端服务地址 ```
http://localhost:8080 ruoyi/
├── ruoyi-admin/ # Web entry point, main application (RuoYiApplication)
# Swagger API 文档 ├── ruoyi-framework/ # Core framework (security, config, interceptors)
http://localhost:8080/swagger-ui/index.html ├── ruoyi-system/ # System management (users, roles, menus, depts)
├── ruoyi-common/ # Common utilities, annotations, domain classes
# 测试登录接口获取 token ├── ruoyi-quartz/ # Scheduled task management
POST /login/test?username=admin&password=admin123 ├── 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 ```bash
cd ruoyi-ui cd ruoyi-ui
# 安装依赖 # Development server (runs on port 80)
npm install
# 开发环境运行(端口 80,代理到 localhost:8080)
npm run dev npm run dev
# 生产环境构建 # For older Node.js versions with OpenSSL issues
npm run dev_t
# Build for production
npm run build:prod npm run build:prod
# 代码检查 # Build for staging
npm run build:stage
# Build for pre-production
npm run build:pre
# Lint code
npm run lint npm run lint
``` ```
### 数据库连接 ## Development Configuration
```bash ### Application Profiles
# 通过 MCP MySQL 工具连接
# 地址: 116.62.17.81:3306
# 数据库: ibs
# 用户名: root
```
## 核心架构 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`).
``` ### Frontend API Proxy
ruoyi-admin/ # 主入口模块,包含启动类和配置文件
ruoyi-framework/ # 框架核心:安全配置、缓存、数据源等
ruoyi-system/ # 系统管理:用户、角色、菜单、字典等
ruoyi-common/ # 通用工具:工具类、注解、常量等
ruoyi-quartz/ # 定时任务模块
ruoyi-generator/ # 代码生成器
ibs/ # ★ 业务模块:数字支行核心业务 ★
```
### 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
| 包名 | 功能 | 说明 | 1. **Controller-Service-Mapper Layering**
|------|------|------| - Controllers handle HTTP requests/responses, use `@RestController`
| `grid` | 网格管理 | 支行网格划分、分配、统计 | - Services contain business logic, use `@Service`
| `cmpm` | 客户经理管理 | 客户经理信息维护 | - Mappers use MyBatis annotations or XML files in `resources/mapper/`
| `list` | 客户列表管理 | 零售/商户/企业客户管理 |
| `visit` | 走访管理 | 走访任务、记录、轨迹 |
| `task` | 任务管理 | 营销任务分配和跟踪 |
| `draw` | 绘图/网格绘制 | 基于百度地图的网格绘制 |
| `custmap` | 客户地图 | 客户地理分布可视化 |
| `dashboard` | 仪表盘 | 数据统计和展示 |
| `datavisual` | 数据可视化 | 报表和图表 |
| `rules` | 规则配置 | 业务规则配置 |
| `qxhy` | 青县惠银接口 | 外部系统对接 |
| `websocket` | WebSocket通信 | 实时通信支持 |
**业务模块命名规范:** 2. **DTO/VO Pattern**
- 新建模块命名: `ibs` + 主要功能(如 `ibs-grid`, `ibs-customer`) - DTOs (Data Transfer Objects) for incoming requests
- Controller 放在新建模块中,不与若依框架混合 - VOs (View Objects) for outgoing responses
- Entity 使用 `@Data` 注解 - Entities map directly to database tables
- Service 使用 `@Resource` 注解,不继承 `ServiceImpl`
- DAO 使用 MyBatis Plus,复杂操作在 XML 中编写 SQL
### 前端结构 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()`
``` 4. **Pagination**
ruoyi-ui/src/ - Uses `PageHelper` for database pagination
├── api/ # API 接口定义 - Controllers return `TableDataInfo` with `total` and `rows`
├── views/ # 页面视图
│ ├── grid/ # 网格管理相关页面
│ ├── customer/ # 客户管理
│ ├── taskManage/ # 任务管理
│ ├── dashboard/ # 仪表盘
│ └── ...
├── components/ # 公共组件
├── map/ # 地图相关(百度地图集成)
├── store/ # Vuex 状态管理
└── router/ # 路由配置
```
### 数据库表命名规范 5. **Caching**
- Uses `RedisCache` for Redis operations
- Cache keys often follow pattern: `{module}:{feature}:{key}`
- 新建表需加项目前缀: `ibs_` + 表名 ### Frontend Code Patterns
- 示例: `ibs_grid`, `ibs_customer`, `ibs_visit_record`
## 关键技术点 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
- 走访轨迹记录
- 客户分布热力图
**相关配置:** ## Common Business Concepts
- 百度地图 AK 在前端配置
- 使用 JTS 库进行地理空间计算
### 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
设计批量导入功能时: ## MyBatis Configuration
- 使用批量操作提高响应速度
- 导入结果只展示失败数据,不展示成功数据
- 使用 EasyExcel 处理 Excel
### 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 端: 主要管理和配置功能 ## Notes
- PAD 端: 走访记录功能(移动端)
### 4. 外部系统对接 - 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
| 青县惠银 | http://158.234.96.76:5002 | 业务数据对接 |
| BI 系统 | http://158.220.52.42:9388/bi | 数据分析和报表 |
| 阿里云 OSS | - | 文件存储 |
## 开发规范
### 代码分层
- **Entity**: 实体类,不继承 BaseEntity,单独添加审计字段
- **DTO**: 接口传参专用类
- **VO**: 返回数据专用类
- **Service**: 业务逻辑,使用 `@Resource` 注入
- **Mapper**: MyBatis Plus + XML 混合使用
### 审计字段
通过注解实现自动填充:
```java
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
```
### 分页
使用 MyBatis Plus 分页插件自动处理,传入 `Page` 对象即可。
### 前端菜单配置
添加新页面后,需要在数据库 `sys_menu` 表中配置菜单权限。
## 重要配置文件
| 文件 | 位置 | 说明 |
|------|------|------|
| 后端配置 | `ruoyi-admin/src/main/resources/application-dev.yml` | 数据库、Redis、第三方服务配置 |
| 前端配置 | `ruoyi-ui/vue.config.js` | 开发服务器、代理配置 |
| 菜单数据 | 数据库 `sys_menu` 表 | 菜单权限配置 |
## 测试账号
- 用户名: `admin`
- 密码: `admin123`
## Swagger 接口文档
启动后端后访问: `http://localhost:8080/swagger-ui/index.html`
## 开发流程建议
1. **数据库设计**: 新建表需加 `ibs_` 前缀
2. **后端实体类**: 使用 `@Data` 注解,添加审计字段注解
3. **后端业务层**: 在 `ibs` 模块下开发,简单 CRUD 用 MyBatis Plus,复杂操作用 XML
4. **后端测试**: 使用 `/login/test` 获取 token 后测试接口
5. **前端开发**: 在 `ruoyi-ui/views/` 下对应业务模块开发
6. **菜单配置**: 在 `sys_menu` 表添加菜单项
7. **API 文档**: 生成后保存在 `doc/api/` 目录

View File

@@ -50,14 +50,14 @@ public class CustGroupController extends BaseController {
} }
/** /**
* 获取客群详情 * 根据ID查询客群详情
*/ */
@ApiOperation("获取客群详情") @ApiOperation("根据ID查询客群详情")
@Log(title = "客群管理-获取客群详情") @Log(title = "客群管理-查询客群详情")
@GetMapping("/{id}") @GetMapping("/{id}")
public AjaxResult getCustGroup(@PathVariable Long id) { public AjaxResult getCustGroup(@PathVariable Long id) {
CustGroupVO vo = custGroupService.getCustGroup(id); CustGroupVO custGroup = custGroupService.getCustGroup(id);
return AjaxResult.success(vo); return AjaxResult.success(custGroup);
} }
/** /**
@@ -83,17 +83,6 @@ public class CustGroupController extends BaseController {
return AjaxResult.success(custGroupService.createCustGroupByTemplate(custGroup, file)); 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); 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

@@ -39,6 +39,12 @@ public class CustGroupQueryDTO implements Serializable {
@ApiModelProperty(value = "客群状态", name = "groupStatus") @ApiModelProperty(value = "客群状态", name = "groupStatus")
private String groupStatus; private String groupStatus;
/**
* 视图类型mine=我创建的sharedToMe=下发给我的
*/
@ApiModelProperty(value = "视图类型", name = "viewType")
private String viewType;
/** /**
* 页码 * 页码
*/ */
@@ -50,4 +56,4 @@ public class CustGroupQueryDTO implements Serializable {
*/ */
@ApiModelProperty(value = "每页大小", name = "pageSize") @ApiModelProperty(value = "每页大小", name = "pageSize")
private Integer pageSize = 10; private Integer pageSize = 10;
} }

View File

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

View File

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

View File

@@ -65,10 +65,10 @@ public class CustGroupVO {
private Integer shareEnabled; private Integer shareEnabled;
/** /**
* 可见部门ID列表 * 可见部门ID列表(逗号分隔)
*/ */
@ApiModelProperty(value = "可见部门ID列表", name = "shareDeptIds") @ApiModelProperty(value = "可见部门ID列表(逗号分隔)", name = "shareDeptIds")
private List<Long> shareDeptIds; private String shareDeptIds;
/** /**
* 客群状态0=正常, 1=已禁用 * 客群状态0=正常, 1=已禁用
@@ -114,6 +114,49 @@ public class CustGroupVO {
@ApiModelProperty(value = "备注", name = "remark") @ApiModelProperty(value = "备注", name = "remark")
private String 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.dto.CustGroupQueryDTO;
import com.ruoyi.group.domain.entity.CustGroup; import com.ruoyi.group.domain.entity.CustGroup;
import com.ruoyi.group.domain.vo.CustGroupVO; import com.ruoyi.group.domain.vo.CustGroupVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
@@ -13,6 +14,7 @@ import java.util.List;
* *
* @author ruoyi * @author ruoyi
*/ */
@Mapper
public interface CustGroupMapper extends BaseMapper<CustGroup> { public interface CustGroupMapper extends BaseMapper<CustGroup> {
/** /**
@@ -30,5 +32,29 @@ public interface CustGroupMapper extends BaseMapper<CustGroup> {
* @param dto 查询条件 * @param dto 查询条件
* @return 客群VO列表 * @return 客群VO列表
*/ */
List<CustGroupVO> selectCustGroupList(@Param("dto") CustGroupQueryDTO dto); List<CustGroupVO> selectCustGroupList(@Param("dto") CustGroupQueryDTO dto,
} @Param("userName") String userName,
@Param("deptId") String deptId);
/**
* 根据ID查询客群详情
*
* @param id 客群ID
* @return 客群VO
*/
CustGroupVO selectCustGroupById(@Param("id") Long id,
@Param("userName") String userName,
@Param("deptId") String deptId);
/**
* 校验当前用户是否有客群查看权限
*
* @param id 客群ID
* @param userName 当前用户名
* @param deptId 当前部门ID
* @return 可查看数量
*/
Long countVisibleCustGroup(@Param("id") Long id,
@Param("userName") String userName,
@Param("deptId") String deptId);
}

View File

@@ -1,21 +1,36 @@
package com.ruoyi.group.mapper; package com.ruoyi.group.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; 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.entity.CustGroupMember;
import com.ruoyi.group.domain.vo.CustGroupMemberVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List;
/** /**
* 客群客户关联Mapper接口 * 客群客户关联Mapper接口
* *
* @author ruoyi * @author ruoyi
*/ */
@Mapper
public interface CustGroupMemberMapper extends BaseMapper<CustGroupMember> { public interface CustGroupMemberMapper extends BaseMapper<CustGroupMember> {
/** /**
* 查询客群客户数量 * 分页查询客群客户列表
* *
* @param groupId 客群ID * @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,21 @@ public interface ICustGroupService {
*/ */
List<CustGroupVO> listCustGroup(CustGroupQueryDTO dto); List<CustGroupVO> listCustGroup(CustGroupQueryDTO dto);
/**
* 根据ID查询客群详情
*
* @param id 客群ID
* @return 客群VO
*/
CustGroupVO getCustGroup(Long id);
/**
* 校验当前用户是否有客群查看权限
*
* @param id 客群ID
*/
void checkCustGroupViewPermission(Long id);
/** /**
* 异步创建客群(模板导入) * 异步创建客群(模板导入)
* *
@@ -57,14 +72,6 @@ public interface ICustGroupService {
*/ */
String updateCustGroupByTemplate(CustGroup custGroup, MultipartFile file); String updateCustGroupByTemplate(CustGroup custGroup, MultipartFile file);
/**
* 更新客群
*
* @param custGroup 客群实体
* @return 结果消息
*/
String updateCustGroup(CustGroup custGroup);
/** /**
* 删除客群 * 删除客群
* *
@@ -73,14 +80,6 @@ public interface ICustGroupService {
*/ */
String deleteCustGroup(List<Long> idList); String deleteCustGroup(List<Long> idList);
/**
* 获取客群详情
*
* @param id 客群ID
* @return 客群VO
*/
CustGroupVO getCustGroup(Long id);
/** /**
* 检查客群名称是否存在 * 检查客群名称是否存在
* *
@@ -97,15 +96,6 @@ public interface ICustGroupService {
*/ */
String getCreateStatus(Long id); String getCreateStatus(Long id);
/**
* 手动移除客群客户
*
* @param groupId 客群ID
* @param memberIds 客群成员ID列表
* @return 结果消息
*/
String removeMembers(Long groupId, List<Long> memberIds);
/** /**
* 更新动态客群(定时任务调用) * 更新动态客群(定时任务调用)
* 根据原始导入条件重新查询并更新客户列表 * 根据原始导入条件重新查询并更新客户列表
@@ -117,4 +107,4 @@ public interface ICustGroupService {
*/ */
void checkAndDisableExpiredGroups(); void checkAndDisableExpiredGroups();
} }

View File

@@ -0,0 +1,62 @@
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.ICustGroupService;
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;
@Resource
private ICustGroupService custGroupService;
@Override
public List<CustGroupMemberVO> listCustGroupMembers(Long groupId, CustGroupMemberQueryDTO dto) {
custGroupService.checkCustGroupViewPermission(groupId);
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.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysDeptMapper; import com.ruoyi.system.mapper.SysDeptMapper;
import com.ruoyi.ibs.cmpm.domain.entity.GridCmpm; import com.ruoyi.ibs.cmpm.domain.vo.GridCmpmVO;
import com.ruoyi.ibs.cmpm.mapper.GridCmpmMapper; import com.ruoyi.ibs.cmpm.service.GridCmpmService;
import com.ruoyi.ibs.draw.domain.entity.DrawGridShapeRelate; import com.ruoyi.ibs.draw.mapper.DrawGridCustUserUnbindMapper;
import com.ruoyi.ibs.draw.domain.entity.DrawShapeCust;
import com.ruoyi.ibs.draw.mapper.DrawGridShapeRelateMapper;
import com.ruoyi.ibs.draw.mapper.DrawShapeCustMapper;
import com.ruoyi.ibs.grid.domain.vo.CustVO;
import com.ruoyi.ibs.grid.service.RegionGridListService; import com.ruoyi.ibs.grid.service.RegionGridListService;
import com.ruoyi.group.domain.dto.CustGroupMemberTemplate; import com.ruoyi.group.domain.dto.CustGroupMemberTemplate;
import com.ruoyi.group.domain.dto.CustGroupQueryDTO; import com.ruoyi.group.domain.dto.CustGroupQueryDTO;
import com.ruoyi.group.domain.dto.GridImportDTO; import com.ruoyi.group.domain.dto.GridImportDTO;
import com.ruoyi.group.domain.entity.CustGroup; import com.ruoyi.group.domain.entity.CustGroup;
import com.ruoyi.group.domain.entity.CustGroupMember; 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.domain.vo.CustGroupVO;
import com.ruoyi.group.mapper.CustGroupMapper; import com.ruoyi.group.mapper.CustGroupMapper;
import com.ruoyi.group.mapper.CustGroupMemberMapper; import com.ruoyi.group.mapper.CustGroupMemberMapper;
import com.ruoyi.group.service.ICustGroupService; import com.ruoyi.group.service.ICustGroupService;
import com.ruoyi.ibs.grid.domain.entity.RegionCustUser; import com.ruoyi.ibs.grid.domain.entity.RegionCustUser;
import com.ruoyi.ibs.grid.domain.entity.RegionGrid; import com.ruoyi.ibs.handler.DynamicTableNameHelper;
import com.ruoyi.ibs.grid.mapper.RegionCustUserMapper;
import com.ruoyi.ibs.grid.mapper.RegionGridMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import lombok.val; import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -61,29 +53,37 @@ public class CustGroupServiceImpl implements ICustGroupService {
private ExecutorService executorService; private ExecutorService executorService;
@Resource @Resource
private GridCmpmMapper gridCmpmMapper; private GridCmpmService gridCmpmService;
@Resource
private RegionCustUserMapper regionCustUserMapper;
@Resource
private RegionGridMapper regionGridMapper;
@Resource @Resource
private RegionGridListService regionGridListService; private RegionGridListService regionGridListService;
@Resource @Resource
private DrawShapeCustMapper drawShapeCustMapper; private DrawGridCustUserUnbindMapper drawGridCustUserUnbindMapper;
@Resource @Resource
private DrawGridShapeRelateMapper drawGridShapeRelateMapper; private TransactionTemplate transactionTemplate;
@Resource
private SysDeptMapper sysDeptMapper;
@Override @Override
public List<CustGroupVO> listCustGroup(CustGroupQueryDTO dto) { public List<CustGroupVO> listCustGroup(CustGroupQueryDTO dto) {
return custGroupMapper.selectCustGroupList(dto); return custGroupMapper.selectCustGroupList(dto, SecurityUtils.getUsername(), String.valueOf(SecurityUtils.getDeptId()));
}
@Override
public CustGroupVO getCustGroup(Long id) {
CustGroupVO custGroup = custGroupMapper.selectCustGroupById(id, SecurityUtils.getUsername(), String.valueOf(SecurityUtils.getDeptId()));
if (custGroup == null) {
throw new ServiceException("客群不存在");
}
return custGroup;
}
@Override
public void checkCustGroupViewPermission(Long id) {
Long count = custGroupMapper.countVisibleCustGroup(id, SecurityUtils.getUsername(), String.valueOf(SecurityUtils.getDeptId()));
if (count == null || count <= 0) {
throw new ServiceException("客群不存在或无查看权限");
}
} }
@Override @Override
@@ -176,21 +176,6 @@ public class CustGroupServiceImpl implements ICustGroupService {
return String.valueOf(custGroup.getId()); 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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public String updateCustGroupByGrid(GridImportDTO gridImportDTO) { public String updateCustGroupByGrid(GridImportDTO gridImportDTO) {
@@ -200,10 +185,17 @@ public class CustGroupServiceImpl implements ICustGroupService {
if (existGroup == null) { if (existGroup == null) {
throw new ServiceException("客群不存在"); throw new ServiceException("客群不存在");
} }
// 检查客群是否正在创建或更新
if ("0".equals(existGroup.getCreateStatus())) {
throw new ServiceException("客群正在处理中,请稍后再试");
}
// 更新客群基本信息 // 更新客群基本信息
if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) { if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在"); throw new ServiceException("客群名称已存在");
} }
// 检查网格条件是否发生变化
boolean gridConditionChanged = isGridConditionChanged(existGroup, gridImportDTO);
// 重新查询数据库,获取最新状态 // 重新查询数据库,获取最新状态
CustGroup latestGroup = custGroupMapper.selectById(custGroup.getId()); CustGroup latestGroup = custGroupMapper.selectById(custGroup.getId());
latestGroup.setGroupName(custGroup.getGroupName()); latestGroup.setGroupName(custGroup.getGroupName());
@@ -212,7 +204,48 @@ public class CustGroupServiceImpl implements ICustGroupService {
latestGroup.setValidTime(custGroup.getValidTime()); latestGroup.setValidTime(custGroup.getValidTime());
latestGroup.setShareEnabled(custGroup.getShareEnabled()); latestGroup.setShareEnabled(custGroup.getShareEnabled());
latestGroup.setShareDeptIds(custGroup.getShareDeptIds()); 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); custGroupMapper.updateById(latestGroup);
// 重新设置回DTO确保异步线程能获取到正确的ID和状态 // 重新设置回DTO确保异步线程能获取到正确的ID和状态
gridImportDTO.setCustGroup(latestGroup); gridImportDTO.setCustGroup(latestGroup);
@@ -223,6 +256,80 @@ public class CustGroupServiceImpl implements ICustGroupService {
return "客群更新中"; 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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public String updateCustGroupByTemplate(CustGroup custGroup, MultipartFile file) { public String updateCustGroupByTemplate(CustGroup custGroup, MultipartFile file) {
@@ -231,6 +338,10 @@ public class CustGroupServiceImpl implements ICustGroupService {
if (existGroup == null) { if (existGroup == null) {
throw new ServiceException("客群不存在"); throw new ServiceException("客群不存在");
} }
// 检查客群是否正在创建或更新
if ("0".equals(existGroup.getCreateStatus())) {
throw new ServiceException("客群正在处理中,请稍后再试");
}
// 更新客群基本信息 // 更新客群基本信息
if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) { if (!existGroup.getGroupName().equals(custGroup.getGroupName()) && checkGroupNameExist(custGroup.getGroupName())) {
throw new ServiceException("客群名称已存在"); throw new ServiceException("客群名称已存在");
@@ -243,7 +354,11 @@ public class CustGroupServiceImpl implements ICustGroupService {
latestGroup.setValidTime(custGroup.getValidTime()); latestGroup.setValidTime(custGroup.getValidTime());
latestGroup.setShareEnabled(custGroup.getShareEnabled()); latestGroup.setShareEnabled(custGroup.getShareEnabled());
latestGroup.setShareDeptIds(custGroup.getShareDeptIds()); latestGroup.setShareDeptIds(custGroup.getShareDeptIds());
// 设置更新状态为"更新中"
latestGroup.setCreateStatus("0");
// 更新数据库 // 更新数据库
latestGroup.setUpdateBy(SecurityUtils.getUsername());
latestGroup.setUpdateTime(new Date());
custGroupMapper.updateById(latestGroup); custGroupMapper.updateById(latestGroup);
// 获取当前用户部门编码(异步线程中无法获取) // 获取当前用户部门编码(异步线程中无法获取)
String headId = SecurityUtils.getHeadId(); String headId = SecurityUtils.getHeadId();
@@ -269,82 +384,6 @@ public class CustGroupServiceImpl implements ICustGroupService {
return "客群删除成功"; 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 @Override
public boolean checkGroupNameExist(String groupName) { public boolean checkGroupNameExist(String groupName) {
LambdaQueryWrapper<CustGroup> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CustGroup> wrapper = new LambdaQueryWrapper<>();
@@ -486,14 +525,14 @@ public class CustGroupServiceImpl implements ICustGroupService {
gridImportDTO.setRegionGridIds(Arrays.stream(custGroup.getRegionGridIds().split(",")) gridImportDTO.setRegionGridIds(Arrays.stream(custGroup.getRegionGridIds().split(","))
.map(Long::valueOf).collect(Collectors.toList())); .map(Long::valueOf).collect(Collectors.toList()));
} }
newMemberList.addAll(importFromRegionGrid(custGroup, gridImportDTO)); newMemberList.addAll(importFromRegionGrid(custGroup, gridImportDTO, headId));
} else if ("2".equals(gridType)) { } else if ("2".equals(gridType)) {
// 绘制网格 // 绘制网格
if (StringUtils.isNotEmpty(custGroup.getDrawGridIds())) { if (StringUtils.isNotEmpty(custGroup.getDrawGridIds())) {
gridImportDTO.setDrawGridIds(Arrays.stream(custGroup.getDrawGridIds().split(",")) gridImportDTO.setDrawGridIds(Arrays.stream(custGroup.getDrawGridIds().split(","))
.map(Long::valueOf).collect(Collectors.toList())); .map(Long::valueOf).collect(Collectors.toList()));
} }
newMemberList.addAll(importFromDrawGrid(custGroup, gridImportDTO)); newMemberList.addAll(importFromDrawGrid(custGroup, gridImportDTO, headId));
} }
// 计算差异 // 计算差异
@@ -601,51 +640,71 @@ public class CustGroupServiceImpl implements ICustGroupService {
} }
memberList.add(member); memberList.add(member);
} }
// 批量插入
int batchSize = 1000; // 使用编程式事务:先删除旧客户,再插入新客户
int successCount = 0; transactionTemplate.executeWithoutResult(status -> {
int skippedCount = 0; // 删除该客群的所有旧客户
int restoredCount = 0; log.info("开始删除客群旧客户模板导入客群ID{}", custGroup.getId());
for (int i = 0; i < memberList.size(); i += batchSize) { LambdaQueryWrapper<CustGroupMember> memberWrapper = new LambdaQueryWrapper<>();
int endIndex = Math.min(i + batchSize, memberList.size()); memberWrapper.eq(CustGroupMember::getGroupId, custGroup.getId());
List<CustGroupMember> batchList = memberList.subList(i, endIndex); custGroupMemberMapper.delete(memberWrapper);
for (CustGroupMember member : batchList) { log.info("客群旧客户删除完成模板导入客群ID{}", custGroup.getId());
try {
custGroupMemberMapper.insert(member); // 批量插入新客户
successCount++; log.info("开始批量插入客户模板导入客群ID{},客户总数:{}", custGroup.getId(), memberList.size());
} catch (DuplicateKeyException e) { int batchSize = 1000;
// 客户已存在,检查是否是被手动移除的 int successCount = 0;
LambdaQueryWrapper<CustGroupMember> queryWrapper = new LambdaQueryWrapper<>(); int skippedCount = 0;
queryWrapper.eq(CustGroupMember::getGroupId, member.getGroupId()) int restoredCount = 0;
.eq(CustGroupMember::getCustId, member.getCustId()) for (int i = 0; i < memberList.size(); i += batchSize) {
.eq(CustGroupMember::getCustType, member.getCustType()); int endIndex = Math.min(i + batchSize, memberList.size());
CustGroupMember existMember = custGroupMemberMapper.selectOne(queryWrapper); List<CustGroupMember> batchList = memberList.subList(i, endIndex);
if (existMember != null && existMember.getManualRemove() != null && existMember.getManualRemove() == 1) { log.info("处理批次 [{}/{}],本批大小:{}", i / batchSize + 1, (memberList.size() + batchSize - 1) / batchSize, batchList.size());
// 是被手动移除的客户,清除标记并恢复 for (CustGroupMember member : batchList) {
existMember.setManualRemove(0); try {
existMember.setDelFlag(0); custGroupMemberMapper.insert(member);
existMember.setCustName(member.getCustName()); successCount++;
custGroupMemberMapper.updateById(existMember); } catch (DuplicateKeyException e) {
restoredCount++; // 客户已存在,检查是否是被手动移除的
log.debug("恢复手动移除的客户groupId={}, custId={}", member.getGroupId(), member.getCustId()); LambdaQueryWrapper<CustGroupMember> queryWrapper = new LambdaQueryWrapper<>();
} else { queryWrapper.eq(CustGroupMember::getGroupId, member.getGroupId())
// 正常存在的客户,跳过 .eq(CustGroupMember::getCustId, member.getCustId())
skippedCount++; .eq(CustGroupMember::getCustType, member.getCustType());
log.debug("客户已存在跳过groupId={}, custId={}", member.getGroupId(), member.getCustId()); 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{},成功:{},跳过重复:{},恢复:{}",
log.info("客群客户导入完成模板客群ID{},成功:{},跳过重复:{},恢复:{}", custGroup.getId(), successCount, skippedCount, restoredCount);
custGroup.getId(), successCount, skippedCount, restoredCount); // 更新创建状态为成功
// 更新创建状态为成功 custGroup.setCreateStatus("1");
custGroup.setCreateStatus("1"); custGroup.setUpdateBy(custGroup.getCreateBy());
custGroupMapper.updateById(custGroup); custGroup.setUpdateTime(new Date());
custGroupMapper.updateById(custGroup);
});
} catch (Exception e) { } catch (Exception e) {
log.error("客群客户导入失败客群ID{},异常:{}", custGroup.getId(), e.getMessage(), e);
// 注意:由于删除和插入在同一事务中,插入失败会自动回滚删除操作,无需手动清理
// 更新创建状态为失败 // 更新创建状态为失败
custGroup.setCreateStatus("2"); custGroup.setCreateStatus("2");
custGroup.setUpdateBy(custGroup.getCreateBy());
custGroup.setUpdateTime(new Date());
custGroupMapper.updateById(custGroup); custGroupMapper.updateById(custGroup);
log.error("客群客户导入失败客群ID{}", custGroup.getId(), e);
throw new ServiceException("客群客户导入失败: " + e.getMessage()); throw new ServiceException("客群客户导入失败: " + e.getMessage());
} }
} }
@@ -664,63 +723,122 @@ public class CustGroupServiceImpl implements ICustGroupService {
memberList.addAll(importFromCmpmGrid(custGroup, gridImportDTO, headId)); memberList.addAll(importFromCmpmGrid(custGroup, gridImportDTO, headId));
} else if ("1".equals(gridType)) { } else if ("1".equals(gridType)) {
// 地理网格 // 地理网格
memberList.addAll(importFromRegionGrid(custGroup, gridImportDTO)); memberList.addAll(importFromRegionGrid(custGroup, gridImportDTO, headId));
} else if ("2".equals(gridType)) { } else if ("2".equals(gridType)) {
// 绘制网格 // 绘制网格
memberList.addAll(importFromDrawGrid(custGroup, gridImportDTO)); memberList.addAll(importFromDrawGrid(custGroup, gridImportDTO, headId));
} }
if (memberList.isEmpty()) { if (memberList.isEmpty()) {
throw new ServiceException("未查询到任何客户"); throw new ServiceException("未查询到任何客户");
} }
// 批量插入
int batchSize = 1000; // 使用编程式事务:先删除旧客户,再插入新客户
int successCount = 0; transactionTemplate.executeWithoutResult(status -> {
int skippedCount = 0; // 删除该客群的所有旧客户
int restoredCount = 0; log.info("开始删除客群旧客户客群ID{}", custGroup.getId());
for (int i = 0; i < memberList.size(); i += batchSize) { LambdaQueryWrapper<CustGroupMember> memberWrapper = new LambdaQueryWrapper<>();
int endIndex = Math.min(i + batchSize, memberList.size()); memberWrapper.eq(CustGroupMember::getGroupId, custGroup.getId());
List<CustGroupMember> batchList = memberList.subList(i, endIndex); custGroupMemberMapper.delete(memberWrapper);
for (CustGroupMember member : batchList) { log.info("客群旧客户删除完成客群ID{}", custGroup.getId());
try {
custGroupMemberMapper.insert(member); // 批量插入新客户
successCount++; log.info("开始批量插入客户客群ID{},客户总数:{}", custGroup.getId(), memberList.size());
} catch (DuplicateKeyException e) {
// 客户已存在,检查是否是被手动移除的 // 分批批量插入每批1000条
LambdaQueryWrapper<CustGroupMember> queryWrapper = new LambdaQueryWrapper<>(); int batchSize = 1000;
queryWrapper.eq(CustGroupMember::getGroupId, member.getGroupId()) int totalInserted = 0;
.eq(CustGroupMember::getCustId, member.getCustId()) int totalRestored = 0;
.eq(CustGroupMember::getCustType, member.getCustType()); int totalSkipped = 0;
CustGroupMember existMember = custGroupMemberMapper.selectOne(queryWrapper);
if (existMember != null && existMember.getManualRemove() != null && existMember.getManualRemove() == 1) { for (int i = 0; i < memberList.size(); i += batchSize) {
// 是被手动移除的客户,清除标记并恢复 int endIndex = Math.min(i + batchSize, memberList.size());
existMember.setManualRemove(0); List<CustGroupMember> batchList = memberList.subList(i, endIndex);
existMember.setDelFlag(0); log.info("处理批次 [{}/{}],本批大小:{}", i / batchSize + 1,
existMember.setCustName(member.getCustName()); (memberList.size() + batchSize - 1) / batchSize, batchList.size());
custGroupMemberMapper.updateById(existMember);
restoredCount++; // SQL层面的批量插入
log.debug("恢复手动移除的客户groupId={}, custId={}", member.getGroupId(), member.getCustId()); custGroupMemberMapper.batchInsertMembers(batchList);
} else {
// 正常存在的客户,跳过 // 查询本批中被手动移除的客户,需要恢复
skippedCount++; List<CustGroupMember> toRestore = findManualRemovedToRestore(custGroup.getId(), batchList);
log.debug("客户已存在跳过groupId={}, custId={}", member.getGroupId(), member.getCustId()); 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{}成功{}跳过重复:{}恢复{}", log.info("客群客户导入完成网格客群ID{}插入{}复:{}跳过{}",
custGroup.getId(), successCount, skippedCount, restoredCount); custGroup.getId(), totalInserted, totalRestored, totalSkipped);
// 更新创建状态为成功
custGroup.setCreateStatus("1"); // 更新创建状态为成功
custGroupMapper.updateById(custGroup); 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) { } catch (Exception e) {
// 先记录原始异常(必须第一时间记录,避免后续异常覆盖)
log.error("==========客群客户导入异常========== 客群ID{},异常类型:{},异常消息:{}",
custGroup.getId(), e.getClass().getName(), e.getMessage(), e);
// 注意:由于删除和插入在同一事务中,插入失败会自动回滚删除操作,无需手动清理
// 更新创建状态为失败 // 更新创建状态为失败
custGroup.setCreateStatus("2"); try {
custGroupMapper.updateById(custGroup); custGroup.setCreateStatus("2");
log.error("客群客户导入失败客群ID{}", custGroup.getId(), e); 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()); 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 +853,13 @@ public class CustGroupServiceImpl implements ICustGroupService {
gridTypes.add("corporate"); gridTypes.add("corporate");
gridTypes.add("corporate_account"); gridTypes.add("corporate_account");
} else { } else {
throw new ServiceException("请选择绩效网格业务类型(零售/公司"); throw new ServiceException("请选择绩效网格业务类型(零售/对公/对公账户");
} }
// 查询客户 // 查询客户
for (String userName : gridImportDTO.getUserNames()) { for (String userName : gridImportDTO.getUserNames()) {
for (String gridType : gridTypes) { for (String gridType : gridTypes) {
List<GridCmpm> cmpmList = gridCmpmMapper.getGridCmpmByUserName(userName, headId, gridType); List<GridCmpmVO> cmpmList = gridCmpmService.selectManageListForImport(gridType, userName, headId);
for (GridCmpm cmpm : cmpmList) { for (GridCmpmVO cmpm : cmpmList) {
CustGroupMember member = new CustGroupMember(); CustGroupMember member = new CustGroupMember();
member.setGroupId(custGroup.getId()); member.setGroupId(custGroup.getId());
member.setCustId(cmpm.getCustId()); member.setCustId(cmpm.getCustId());
@@ -760,25 +878,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<>(); List<CustGroupMember> memberList = new ArrayList<>();
// 查询地理网格获取编码
if (gridImportDTO.getRegionGridIds() == null || gridImportDTO.getRegionGridIds().isEmpty()) { if (gridImportDTO.getRegionGridIds() == null || gridImportDTO.getRegionGridIds().isEmpty()) {
throw new ServiceException("请选择地理网格"); throw new ServiceException("请选择地理网格");
} }
List<RegionGrid> regionGrids = regionGridMapper.selectBatchIds(gridImportDTO.getRegionGridIds()); // 直接根据网格ID列表批量查询所有客户SQL中直接拼接headId绕过MyBatis-Plus拦截器
// 使用 selectAllCustFromGrid 方法查询所有客户(不限制客户类型) List<RegionCustUser> custUsers = regionGridListService.selectAllCustByGridIds(gridImportDTO.getRegionGridIds(), headId);
for (RegionGrid regionGrid : regionGrids) { for (RegionCustUser custUser : custUsers) {
List<RegionCustUser> custUsers = regionGridListService.selectAllCustFromGrid(regionGrid); CustGroupMember member = new CustGroupMember();
for (RegionCustUser custUser : custUsers) { member.setGroupId(custGroup.getId());
CustGroupMember member = new CustGroupMember(); member.setCustId(custUser.getCustId());
member.setGroupId(custGroup.getId()); member.setCustName(custUser.getCustName());
member.setCustId(custUser.getCustId()); member.setCustType(custUser.getCustType());
member.setCustName(custUser.getCustName()); member.setCreateTime(new Date());
member.setCustType(custUser.getCustType()); memberList.add(member);
member.setCreateTime(new Date());
memberList.add(member);
}
} }
return memberList; return memberList;
} }
@@ -786,27 +900,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<>(); List<CustGroupMember> memberList = new ArrayList<>();
if (gridImportDTO.getDrawGridIds() == null || gridImportDTO.getDrawGridIds().isEmpty()) { if (gridImportDTO.getDrawGridIds() == null || gridImportDTO.getDrawGridIds().isEmpty()) {
throw new ServiceException("请选择绘制网格"); throw new ServiceException("请选择绘制网格");
} }
// 查询绘制网格关联的图形ID // 使用 selectCustByDrawGridId 方法直接在SQL中拼接headId绕过拦截器
for (Long gridId : gridImportDTO.getDrawGridIds()) { for (Long gridId : gridImportDTO.getDrawGridIds()) {
LambdaQueryWrapper<DrawGridShapeRelate> relateWrapper = new LambdaQueryWrapper<>(); List<RegionCustUser> custUsers = drawGridCustUserUnbindMapper.selectCustByDrawGridId(gridId, headId);
relateWrapper.eq(DrawGridShapeRelate::getGridId, gridId); if (custUsers != null && !custUsers.isEmpty()) {
List<DrawGridShapeRelate> relates = drawGridShapeRelateMapper.selectList(relateWrapper); for (RegionCustUser custUser : custUsers) {
for (DrawGridShapeRelate relate : relates) {
// 根据图形ID查询客户
LambdaQueryWrapper<DrawShapeCust> custWrapper = new LambdaQueryWrapper<>();
custWrapper.eq(DrawShapeCust::getShapeId, relate.getShapeId());
List<DrawShapeCust> shapeCusts = drawShapeCustMapper.selectList(custWrapper);
for (DrawShapeCust shapeCust : shapeCusts) {
CustGroupMember member = new CustGroupMember(); CustGroupMember member = new CustGroupMember();
member.setGroupId(custGroup.getId()); member.setGroupId(custGroup.getId());
member.setCustId(shapeCust.getCustId()); member.setCustId(custUser.getCustId());
member.setCustName(shapeCust.getCustName()); member.setCustName(custUser.getCustName());
member.setCustType(shapeCust.getCustType()); member.setCustType(custUser.getCustType());
member.setCreateTime(new Date()); member.setCreateTime(new Date());
memberList.add(member); memberList.add(member);
} }
@@ -814,4 +922,4 @@ public class CustGroupServiceImpl implements ICustGroupService {
} }
return memberList; return memberList;
} }
} }

View File

@@ -4,6 +4,37 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.group.mapper.CustGroupMapper"> <mapper namespace="com.ruoyi.group.mapper.CustGroupMapper">
<sql id="custGroupVisibleBaseCondition">
AND (
cg.user_name = #{userName}
OR (
cg.share_enabled = 1
AND cg.group_status = '0'
AND cg.share_dept_ids IS NOT NULL
AND cg.share_dept_ids != ''
AND find_in_set(#{deptId}, cg.share_dept_ids)
)
)
</sql>
<sql id="custGroupVisibleCondition">
<choose>
<when test="dto != null and dto.viewType == 'mine'">
AND cg.user_name = #{userName}
</when>
<when test="dto != null and dto.viewType == 'sharedToMe'">
AND cg.share_enabled = 1
AND cg.group_status = '0'
AND cg.share_dept_ids IS NOT NULL
AND cg.share_dept_ids != ''
AND find_in_set(#{deptId}, cg.share_dept_ids)
</when>
<otherwise>
<include refid="custGroupVisibleBaseCondition"/>
</otherwise>
</choose>
</sql>
<select id="selectCustGroupList" resultType="CustGroupVO"> <select id="selectCustGroupList" resultType="CustGroupVO">
SELECT SELECT
cg.id, cg.id,
@@ -26,7 +57,8 @@
FROM ibs_cust_group cg FROM ibs_cust_group cg
<where> <where>
cg.del_flag = '0' cg.del_flag = '0'
and create_status = '1' AND cg.create_status = '1'
<include refid="custGroupVisibleCondition"/>
<if test="dto.groupName != null and dto.groupName != ''"> <if test="dto.groupName != null and dto.groupName != ''">
AND cg.group_name LIKE CONCAT('%', #{dto.groupName}, '%') AND cg.group_name LIKE CONCAT('%', #{dto.groupName}, '%')
</if> </if>
@@ -43,4 +75,45 @@
ORDER BY cg.create_time DESC ORDER BY cg.create_time DESC
</select> </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'
AND cg.create_status = '1'
<include refid="custGroupVisibleBaseCondition"/>
</select>
<select id="countVisibleCustGroup" resultType="java.lang.Long">
SELECT COUNT(1)
FROM ibs_cust_group cg
WHERE cg.id = #{id}
AND cg.del_flag = '0'
AND cg.create_status = '1'
<include refid="custGroupVisibleBaseCondition"/>
</select>
</mapper> </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) { public AjaxResult selectCustBaseInfoList(@RequestBody CustBaseInfo custBaseInfo) {
return AjaxResult.success( gridCmpmCustService.selectCustInfoList (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.DwbRetailCustLevelManagerDetailVO;
import com.ruoyi.ibs.cmpm.domain.vo.DwbRetailResultVO; import com.ruoyi.ibs.cmpm.domain.vo.DwbRetailResultVO;
import com.ruoyi.ibs.cmpm.domain.vo.GridCmpmClaimVO; import com.ruoyi.ibs.cmpm.domain.vo.GridCmpmClaimVO;
import com.ruoyi.ibs.cmpm.domain.vo.GridCmpmVO;
import com.ruoyi.ibs.customerselect.domain.CustBaseInfo; import com.ruoyi.ibs.customerselect.domain.CustBaseInfo;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; 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.List;
import java.util.Map;
/** /**
* @Author 吴凯程 * @Author 吴凯程
@@ -60,6 +64,32 @@ public interface GridCmpmMapper {
List<String> selectManagerList(); 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); // List<CustBaseInfo> selectCustInfoRetailFromGridCmpm(CustBaseInfo custBaseInfo);
// //

View File

@@ -302,4 +302,26 @@ public class GridCmpmService {
return changesMap; 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

@@ -18,8 +18,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.rmi.ServerException; import java.rmi.ServerException;
@@ -220,4 +222,18 @@ public class MyCustomerController extends BaseController {
} }
return AjaxResult.success(iSysCampaignGroupCustomerService.appointCustCamp( custId, custName, custIdc, custPhone, custIsn,socialCreditCode,lpName, campaignId, custType)); return AjaxResult.success(iSysCampaignGroupCustomerService.appointCustCamp( custId, custName, custIdc, custPhone, custIsn,socialCreditCode,lpName, campaignId, custType));
} }
@Log(title = "我的客户-异步导入企业客户价值分层", businessType = com.ruoyi.common.enums.BusinessType.IMPORT)
@PostMapping(value = "/importBusinessCustLevelAsync", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ApiOperation("异步导入企业客户价值分层")
public AjaxResult importBusinessCustLevelAsync(@RequestPart("file") MultipartFile file) {
return AjaxResult.success("导入任务已提交,后台正在处理", myCustomerService.importBusinessCustLevelAsync(file));
}
@Log(title = "我的客户-查询企业客户价值分层导入状态")
@GetMapping("/importBusinessCustLevelStatus/{taskId}")
@ApiOperation("查询企业客户价值分层导入状态")
public AjaxResult importBusinessCustLevelStatus(@PathVariable String taskId) {
return AjaxResult.success(myCustomerService.getBusinessCustLevelImportStatus(taskId));
}
} }

View File

@@ -0,0 +1,14 @@
package com.ruoyi.ibs.customerselect.domain;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
@Data
public class BusinessCustLevelImportExcelDTO {
@ExcelProperty("统信码")
private String socialCreditCode;
@ExcelProperty("价值分层")
private String custLevel;
}

View File

@@ -0,0 +1,30 @@
package com.ruoyi.ibs.customerselect.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class BusinessCustLevelImportTaskVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 0-处理中 1-成功 2-失败
*/
private String status;
private String message;
private Integer totalCount;
private Integer successCount;
private Integer ignoredCount;
private String userName;
private String createTime;
private String finishTime;
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.ibs.customerselect.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.common.core.domain.entity.CustInfoBusiness; import com.ruoyi.common.core.domain.entity.CustInfoBusiness;
import com.ruoyi.ibs.customerselect.domain.BusinessCustLevelImportExcelDTO;
import com.ruoyi.ibs.customerselect.domain.CustBaseInfo; import com.ruoyi.ibs.customerselect.domain.CustBaseInfo;
import com.ruoyi.ibs.customerselect.domain.CustInfoDeleteFromAnchor; import com.ruoyi.ibs.customerselect.domain.CustInfoDeleteFromAnchor;
import com.ruoyi.ibs.customerselect.domain.CustInfoUpdateFromAnchor; import com.ruoyi.ibs.customerselect.domain.CustInfoUpdateFromAnchor;
@@ -242,4 +243,10 @@ public interface CustInfoBusinessMapper extends BaseMapper<CustInfoBusiness>
public int insertCustomersToBusinessByScCode(List<SysGroupCustomer> sysGroupCustomers); public int insertCustomersToBusinessByScCode(List<SysGroupCustomer> sysGroupCustomers);
List<CustInfoBusiness> selectRecord(String socialCreditCode); List<CustInfoBusiness> selectRecord(String socialCreditCode);
List<String> selectExistingSocialCreditCodes(@Param("socialCreditCodes") List<String> socialCreditCodes,
@Param("deptCode") String deptCode);
int batchUpdateCustLevelBySocialCreditCode(@Param("list") List<BusinessCustLevelImportExcelDTO> list,
@Param("deptCode") String deptCode);
} }

View File

@@ -1,6 +1,7 @@
package com.ruoyi.ibs.customerselect.service; package com.ruoyi.ibs.customerselect.service;
import com.ruoyi.ibs.customerselect.domain.*; import com.ruoyi.ibs.customerselect.domain.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
@@ -41,4 +42,8 @@ public interface IMyCustomerService {
public CustListSearchVo selectCustomListSearchVo(CustBaseInfo sysCustomerBasedata); public CustListSearchVo selectCustomListSearchVo(CustBaseInfo sysCustomerBasedata);
MerchantMcspInfo selectmerchantMessage(String custId); MerchantMcspInfo selectmerchantMessage(String custId);
String importBusinessCustLevelAsync(MultipartFile file);
BusinessCustLevelImportTaskVO getBusinessCustLevelImportStatus(String taskId);
} }

View File

@@ -1,13 +1,19 @@
package com.ruoyi.ibs.customerselect.service.Impl; package com.ruoyi.ibs.customerselect.service.Impl;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.annotation.DataScope; import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.CustInfoBusiness; import com.ruoyi.common.core.domain.entity.CustInfoBusiness;
import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.ibs.dashboard.service.NotificationService;
import com.ruoyi.ibs.customerselect.domain.BusinessCustLevelImportExcelDTO;
import com.ruoyi.ibs.customerselect.domain.BusinessCustLevelImportTaskVO;
import com.ruoyi.ibs.customerselect.domain.*; import com.ruoyi.ibs.customerselect.domain.*;
import com.ruoyi.ibs.customerselect.domain.vo.GridRelateVO; import com.ruoyi.ibs.customerselect.domain.vo.GridRelateVO;
import com.ruoyi.ibs.customerselect.mapper.CustInfoBusinessMapper; import com.ruoyi.ibs.customerselect.mapper.CustInfoBusinessMapper;
@@ -32,9 +38,16 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -45,6 +58,10 @@ import java.util.stream.Collectors;
@Service @Service
public class MyCustomerServiceImpl implements IMyCustomerService { public class MyCustomerServiceImpl implements IMyCustomerService {
private static final String BUSINESS_CUST_LEVEL_IMPORT_TASK_KEY = "BUSINESS_CUST_LEVEL_IMPORT_TASK_";
private static final int BUSINESS_CUST_LEVEL_IMPORT_BATCH_SIZE = 1000;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired @Autowired
private CustInfoBusinessMapper custInfoBusinessMapper; private CustInfoBusinessMapper custInfoBusinessMapper;
@@ -80,6 +97,12 @@ public class MyCustomerServiceImpl implements IMyCustomerService {
@Resource @Resource
private RedisCache redisCache; private RedisCache redisCache;
@Resource
private NotificationService notificationService;
@Resource(name = "excelImportExecutor")
private ExecutorService excelImportExecutor;
private static Logger logger = LoggerFactory.getLogger(MyCustomerServiceImpl.class); private static Logger logger = LoggerFactory.getLogger(MyCustomerServiceImpl.class);
/** /**
@@ -467,5 +490,154 @@ public class MyCustomerServiceImpl implements IMyCustomerService {
return merchantMcspInfoMapper.selectOne(new LambdaQueryWrapper<MerchantMcspInfo>().eq(MerchantMcspInfo::getCustId,custId)); return merchantMcspInfoMapper.selectOne(new LambdaQueryWrapper<MerchantMcspInfo>().eq(MerchantMcspInfo::getCustId,custId));
} }
@Override
public String importBusinessCustLevelAsync(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new ServiceException("导入文件不能为空");
}
byte[] fileBytes;
try {
fileBytes = file.getBytes();
} catch (IOException e) {
throw new ServiceException("读取导入文件失败");
}
String taskId = IdUtils.fastSimpleUUID();
String userName = SecurityUtils.getUsername();
String deptCode = SecurityUtils.getHeadId();
BusinessCustLevelImportTaskVO taskVO = new BusinessCustLevelImportTaskVO();
taskVO.setStatus("0");
taskVO.setMessage("导入任务已提交,后台正在处理");
taskVO.setTotalCount(0);
taskVO.setSuccessCount(0);
taskVO.setIgnoredCount(0);
taskVO.setUserName(userName);
taskVO.setCreateTime(formatNow());
cacheBusinessCustLevelImportTask(taskId, taskVO);
excelImportExecutor.submit(() -> doImportBusinessCustLevel(taskId, userName, deptCode, fileBytes));
return taskId;
}
@Override
public BusinessCustLevelImportTaskVO getBusinessCustLevelImportStatus(String taskId) {
BusinessCustLevelImportTaskVO taskVO = redisCache.getCacheObject(getBusinessCustLevelImportTaskKey(taskId));
if (taskVO == null) {
throw new ServiceException("导入任务不存在或已过期");
}
return taskVO;
}
private void doImportBusinessCustLevel(String taskId, String userName, String deptCode, byte[] fileBytes) {
BusinessCustLevelImportTaskVO taskVO = redisCache.getCacheObject(getBusinessCustLevelImportTaskKey(taskId));
if (taskVO == null) {
taskVO = new BusinessCustLevelImportTaskVO();
taskVO.setUserName(userName);
taskVO.setCreateTime(formatNow());
}
try {
List<BusinessCustLevelImportExcelDTO> importRows = EasyExcel.read(new ByteArrayInputStream(fileBytes))
.head(BusinessCustLevelImportExcelDTO.class)
.sheet()
.doReadSync();
Map<String, String> levelMap = new LinkedHashMap<>();
if (importRows != null) {
for (BusinessCustLevelImportExcelDTO row : importRows) {
String socialCreditCode = normalizeImportCell(row.getSocialCreditCode());
String custLevel = normalizeImportCell(row.getCustLevel());
if (StringUtils.isEmpty(socialCreditCode)) {
continue;
}
levelMap.put(socialCreditCode, custLevel);
}
}
if (levelMap.isEmpty()) {
throw new ServiceException("Excel中未识别到有效的统信码和价值分层数据");
}
List<String> existingSocialCreditCodes = getExistingBusinessSocialCreditCodes(new ArrayList<>(levelMap.keySet()), deptCode);
Set<String> existingCodeSet = new HashSet<>(existingSocialCreditCodes);
List<BusinessCustLevelImportExcelDTO> updateList = new ArrayList<>();
for (Map.Entry<String, String> entry : levelMap.entrySet()) {
if (!existingCodeSet.contains(entry.getKey())) {
continue;
}
BusinessCustLevelImportExcelDTO dto = new BusinessCustLevelImportExcelDTO();
dto.setSocialCreditCode(entry.getKey());
dto.setCustLevel(entry.getValue());
updateList.add(dto);
}
batchUpdateBusinessCustLevel(updateList, deptCode);
int totalCount = levelMap.size();
int successCount = updateList.size();
int ignoredCount = totalCount - successCount;
String message = String.format("公司客户视图分层分类数据导入完成,成功更新%d条忽略%d条", successCount, ignoredCount);
taskVO.setStatus("1");
taskVO.setMessage(message);
taskVO.setTotalCount(totalCount);
taskVO.setSuccessCount(successCount);
taskVO.setIgnoredCount(ignoredCount);
taskVO.setFinishTime(formatNow());
cacheBusinessCustLevelImportTask(taskId, taskVO);
notificationService.sendNotification(userName, message);
} catch (Exception e) {
String errorMsg = StringUtils.isNotEmpty(e.getMessage()) ? e.getMessage() : "导入失败,请检查文件内容";
taskVO.setStatus("2");
taskVO.setMessage(errorMsg);
taskVO.setFinishTime(formatNow());
cacheBusinessCustLevelImportTask(taskId, taskVO);
notificationService.sendNotification(userName, "公司客户视图分层分类数据导入失败:" + errorMsg);
logger.error("公司客户视图分层分类数据导入失败taskId={}", taskId, e);
}
}
private void batchUpdateBusinessCustLevel(List<BusinessCustLevelImportExcelDTO> updateList, String deptCode) {
if (updateList == null || updateList.isEmpty()) {
return;
}
for (int i = 0; i < updateList.size(); i += BUSINESS_CUST_LEVEL_IMPORT_BATCH_SIZE) {
int endIndex = Math.min(i + BUSINESS_CUST_LEVEL_IMPORT_BATCH_SIZE, updateList.size());
custInfoBusinessMapper.batchUpdateCustLevelBySocialCreditCode(updateList.subList(i, endIndex), deptCode);
}
}
private List<String> getExistingBusinessSocialCreditCodes(List<String> socialCreditCodes, String deptCode) {
if (socialCreditCodes == null || socialCreditCodes.isEmpty()) {
return Collections.emptyList();
}
List<String> existingSocialCreditCodes = new ArrayList<>();
for (int i = 0; i < socialCreditCodes.size(); i += BUSINESS_CUST_LEVEL_IMPORT_BATCH_SIZE) {
int endIndex = Math.min(i + BUSINESS_CUST_LEVEL_IMPORT_BATCH_SIZE, socialCreditCodes.size());
List<String> batchCodes = socialCreditCodes.subList(i, endIndex);
List<String> batchResult = custInfoBusinessMapper.selectExistingSocialCreditCodes(batchCodes, deptCode);
if (batchResult != null && !batchResult.isEmpty()) {
existingSocialCreditCodes.addAll(batchResult);
}
}
return existingSocialCreditCodes;
}
private void cacheBusinessCustLevelImportTask(String taskId, BusinessCustLevelImportTaskVO taskVO) {
redisCache.setCacheObject(getBusinessCustLevelImportTaskKey(taskId), taskVO, 24, TimeUnit.HOURS);
}
private String getBusinessCustLevelImportTaskKey(String taskId) {
return BUSINESS_CUST_LEVEL_IMPORT_TASK_KEY + taskId;
}
private String normalizeImportCell(String value) {
if (value == null) {
return null;
}
String trimmedValue = value.trim();
return trimmedValue.isEmpty() ? null : trimmedValue;
}
private String formatNow() {
return LocalDateTime.now().format(DATE_TIME_FORMATTER);
}
} }

View File

@@ -173,6 +173,7 @@ public class DashboardController extends BaseController {
public TableDataInfo list(SysNotice notice) public TableDataInfo list(SysNotice notice)
{ {
startPage(); startPage();
notice.setCurrentHeadDeptId(SecurityUtils.getHeadId() + "000");
List<SysNotice> list = noticeService.selectNoticeList(notice); List<SysNotice> list = noticeService.selectNoticeList(notice);
return getDataTable(list); return getDataTable(list);
} }

View File

@@ -80,6 +80,16 @@ public class DrawGridController extends BaseController {
return getDataTable(gridList, page); 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("分页获取网格内客户列表") @ApiOperation("分页获取网格内客户列表")
@Log(title = "自定义绘制网格--分页获取网格内客户列表") @Log(title = "自定义绘制网格--分页获取网格内客户列表")
@GetMapping("/cust/list") @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.dto.grid.DrawGridCustListDTO;
import com.ruoyi.ibs.draw.domain.entity.DrawGridCustUserUnbind; import com.ruoyi.ibs.draw.domain.entity.DrawGridCustUserUnbind;
import com.ruoyi.ibs.draw.domain.vo.DrawGridCustVO; 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.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
@@ -18,5 +20,13 @@ public interface DrawGridCustUserUnbindMapper extends BaseMapper<DrawGridCustUse
List<DrawGridCustVO> getCustListByManager(DrawGridCustListDTO drawGridCustListDTO); 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.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ibs.draw.domain.entity.DrawGridShapeRelate; import com.ruoyi.ibs.draw.domain.entity.DrawGridShapeRelate;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/** /**
* @Author 吴凯程 * @Author 吴凯程
@@ -10,4 +14,19 @@ import org.apache.ibatis.annotations.Mapper;
**/ **/
@Mapper @Mapper
public interface DrawGridShapeRelateMapper extends BaseMapper<DrawGridShapeRelate> { 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); 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; 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) { private CustCountDTO getCustCountByGrid(Long gridId) {
CustCountDTO custCountDTO = new CustCountDTO(); CustCountDTO custCountDTO = new CustCountDTO();
LambdaQueryWrapper<DrawGridShapeRelate> queryWrapper = new LambdaQueryWrapper<>(); 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.dto.RegionGridListDTO;
import com.ruoyi.ibs.grid.domain.entity.RegionGrid; import com.ruoyi.ibs.grid.domain.entity.RegionGrid;
import com.ruoyi.ibs.grid.domain.vo.RegionCustUserVO; 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.domain.vo.RegionGridListVO;
import com.ruoyi.ibs.grid.mapper.RegionGridMapper; import com.ruoyi.ibs.grid.mapper.RegionGridMapper;
import com.ruoyi.ibs.grid.service.RegionGridListService; import com.ruoyi.ibs.grid.service.RegionGridListService;
@@ -108,6 +109,14 @@ public class RegionGridListController extends BaseController {
return AjaxResult.success("开始更新行社所有网格"); 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); 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.dto.RegionGridListDTO;
import com.ruoyi.ibs.grid.domain.entity.RegionGrid; import com.ruoyi.ibs.grid.domain.entity.RegionGrid;
import com.ruoyi.ibs.grid.domain.vo.RegionGridCustVO; 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.RegionGridListVO;
import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo; import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -69,4 +70,10 @@ public interface RegionGridMapper extends BaseMapper<RegionGrid> {
List<Long> getSecGridIdByTopGridId(@Param("gridId") Long gridId); 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.entity.RegionGrid;
import com.ruoyi.ibs.grid.domain.vo.RegionCustUserVO; import com.ruoyi.ibs.grid.domain.vo.RegionCustUserVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridCustVO; 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.RegionGridListVO;
import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo; import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo;
@@ -36,11 +37,20 @@ public interface RegionGridListService {
List<RegionGridListVO> getSecGridListByManager(RegionGridListDTO regionGridListDTO); List<RegionGridListVO> getSecGridListByManager(RegionGridListDTO regionGridListDTO);
/** /**
* 查询网格内所有客户(不限制客户类型,用于导入客群) * 根据网格ID列表批量查询所有客户用于导入客群,优化版
* @param regionGrid 地理网格对象 * @param gridIds 网格ID列表
* @return 网格内所有客户列表 * @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.entity.*;
import com.ruoyi.ibs.grid.domain.vo.RegionCustUserVO; import com.ruoyi.ibs.grid.domain.vo.RegionCustUserVO;
import com.ruoyi.ibs.grid.domain.vo.RegionGridCustVO; 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.RegionGridListVO;
import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo; import com.ruoyi.ibs.grid.domain.vo.RegionUnbindVo;
import com.ruoyi.ibs.grid.mapper.*; import com.ruoyi.ibs.grid.mapper.*;
@@ -432,23 +433,33 @@ public class RegionGridListServiceImpl implements RegionGridListService {
} }
/** /**
* 查询网格内所有客户(不限制客户类型,用于导入客群) * 根据网格ID列表批量查询所有客户用于导入客群,优化版
* @param regionGrid 地理网格对象 * @param gridIds 网格ID列表
* @return 网格内所有客户列表 * @return 客户列表(去重)
*/ */
@Override @Override
public List<RegionCustUser> selectAllCustFromGrid(RegionGrid regionGrid) { public List<RegionCustUser> selectAllCustByGridIds(List<Long> gridIds, String headId) {
LambdaQueryWrapper<RegionCustUser> queryWrapper = new LambdaQueryWrapper<>(); return regionCustUserMapper.selectAllCustByGridIds(gridIds, headId);
// 根据网格等级判断使用一级还是二级网格ID查询 }
if (regionGrid.getGridLevel().equals("1")) {
queryWrapper.eq(RegionCustUser::getTopGridId, regionGrid.getGridId()); /**
} else if (regionGrid.getGridLevel().equals("2")) { * 查询地理网格列表(简化版)- 专用于客群创建
queryWrapper.eq(RegionCustUser::getSecGridId, regionGrid.getGridId()); * 只返回gridId和gridName避免N+1查询问题
} else { * @param regionGridListDTO 查询条件
throw new ServiceException("无效的网格等级: " + regionGrid.getGridLevel()); * @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

@@ -312,6 +312,13 @@ public class SysCampaignController extends BaseController {
.stream().collect(HashMap::new, (m, v) -> m.put(v.getUuid(),v.getModelName()), HashMap::putAll))); .stream().collect(HashMap::new, (m, v) -> m.put(v.getUuid(),v.getModelName()), HashMap::putAll)));
} }
@PostMapping("/updateVisitInfoFeedback")
@ApiOperation("更新PAD走访反馈")
@Log(title ="pad走访记录-更新走访反馈", businessType = BusinessType.UPDATE)
public AjaxResult updateVisitInfoFeedback(@RequestBody VisitInfoFeedbackUpdateDTO updateDTO) {
return toAjax(sysCampaignService.updateVisitInfoFeedback(updateDTO));
}
@PostMapping("/delete") @PostMapping("/delete")
@ApiOperation("根据campaignId删除任务") @ApiOperation("根据campaignId删除任务")
@Log(title = "走访-根据campaignId删除任务") @Log(title = "走访-根据campaignId删除任务")

View File

@@ -41,6 +41,14 @@ public class CustInfoBusinessVo {
@ApiModelProperty(value = "手动打标") @ApiModelProperty(value = "手动打标")
private List<TreeNode> tagManual; private List<TreeNode> tagManual;
/** 九维画像分数 */
@ApiModelProperty(value = "九维画像分数")
private Ent9vPortraitOrc ent9vPortrait;
/** 九维画像详细信息 */
@ApiModelProperty(value = "九维画像详细信息")
private NineVFinalInfoOrc nineVFinalInfo;
public List<TreeNode> getTagManual() { public List<TreeNode> getTagManual() {
return tagManual; return tagManual;
} }
@@ -187,4 +195,20 @@ public class CustInfoBusinessVo {
public void setTabEnmuVos(List<TreeNode> tabEnmuVos) { public void setTabEnmuVos(List<TreeNode> tabEnmuVos) {
this.tabEnmuVos = 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,80 @@
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;
/** 客户内码 */
@TableField("cst_id")
private String cstId;
/** 机构号 */
@TableField("org_no")
private String orgNo;
/** score_1合规经营 */
@TableField("score_1")
private BigDecimal score1;
/** score_2风险准入 */
@TableField("score_2")
private BigDecimal score2;
/** score_3高管信用评价 */
@TableField("score_3")
private String score3;
/** score_4股东信用评价 */
@TableField("score_4")
private BigDecimal score4;
/** score_5社会贡献度 */
@TableField("score_5")
private BigDecimal score5;
/** score_6稳定经营 */
@TableField("score_6")
private BigDecimal score6;
/** score_7经营能力 */
@TableField("score_7")
private BigDecimal score7;
/** score_8偿债能力 */
@TableField("score_8")
private BigDecimal score8;
/** score_9潜在代偿资源 */
@TableField("score_9")
private BigDecimal score9;
/** 九维总分 */
@TableField("score_all")
private BigDecimal scoreAll;
/** 九维总分排名 */
@TableField("score_all_rank")
private BigDecimal scoreAllRank;
/** 会计日期 */
@TableField("dat_dt")
private String datDt;
}

View File

@@ -0,0 +1,315 @@
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;
/**
* 企业九维画像详细信息对象 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;
/** 机构号 */
@TableField("org_no")
private String orgNo;
/** 一、企业合规经营模块 */
/** 是否存在经营异常名录信息 */
@TableField("score_1_1")
private String score11;
/** 是否存在严重违法失信企业名单信息 */
@TableField("score_1_2")
private String score12;
/** 企业环境行为信仰登记是否为"E"或D */
@TableField("score_1_3")
private String score13;
/** 是否存在税务重大税收违法黑名单信息 */
@TableField("score_1_4")
private String score14;
/** 是否存在拖欠工资黑名单 */
@TableField("score_1_5")
private String score15;
/** 是否存在工商吊销企业信息 */
@TableField("score_1_6")
private String score16;
/** 二、企业风险准入模块 */
/** 是否存在注销企业信息 */
@TableField("score_2_1")
private String score21;
/** 是否存在执行案件信息 */
@TableField("score_2_2")
private String score22;
/** 是否存在查封信息 */
@TableField("score_2_3")
private String score23;
/** 是否存在单位未履行生效裁判信息 */
@TableField("score_2_4")
private String score24;
/** 是否存在企业破产清算信息 */
@TableField("score_2_5")
private String score25;
/** 是否失信被执行人 */
@TableField("score_2_6")
private String score26;
/** 是否为诉讼案件被告 */
@TableField("score_2_7")
private String score27;
/** 三、高管信用评价模块 */
/** 是否存在查封信息(score_3) */
@TableField("score_3_1")
private String score31;
/** 是否存在执行案件信息(score_3) */
@TableField("score_3_2")
private String score32;
/** 是否存在个人未履行生效裁判信息 */
@TableField("score_3_3")
private String score33;
/** 是否存在拖欠工资黑名单(score_3) */
@TableField("score_3_4")
private String score34;
/** 是否失信被执行人(score_3) */
@TableField("score_3_5")
private String score35;
/** 是否存在刑事案件被告人生效判决信息 */
@TableField("score_3_6")
private String score36;
/** 是否为诉讼案件被告(score_3) */
@TableField("score_3_7")
private String score37;
/** 四、股东信用评价模块 */
/** 是否存在查封信息(score_4) */
@TableField("score_4_1")
private String score41;
/** 是否存在执行案件信息(score_4) */
@TableField("score_4_2")
private String score42;
/** 是否存在个人未履行生效裁判信息(score_4) */
@TableField("score_4_3")
private String score43;
/** 是否存在拖欠工资黑名单(score_4) */
@TableField("score_4_4")
private String score44;
/** 是否失信被执行人(score_4) */
@TableField("score_4_5")
private String score45;
/** 是否存在刑事案件被告人生效判决信息(score_4) */
@TableField("score_4_6")
private String score46;
/** 是否为诉讼案件被告(score_4) */
@TableField("score_4_7")
private String score47;
/** 是否存在企业未履行生效裁判信息 */
@TableField("score_4_8")
private String score48;
/** 五、企业社会贡献度模块 */
/** 前12个月纳税金额 */
@TableField("score_5_1")
private String score51;
/** 纳税等级 */
@TableField("score_5_2")
private String score52;
/** 缴纳社保人数 */
@TableField("score_5_3")
private String score53;
/** 公积金缴纳人数 */
@TableField("score_5_4")
private String score54;
/** 是否为出口退税生产清单企业 */
@TableField("score_5_5")
private String score55;
/** 六、企业稳定经营模块 */
/** 市场主体经营年限 */
@TableField("score_6_1")
private String score61;
/** 股东(或发起人)或投资人信息认缴出资人数 */
@TableField("score_6_2")
private String score62;
/** 最大股东持股占比 */
@TableField("score_6_3")
private String score63;
/** 近三年法定代表人变更次数 */
@TableField("score_6_4")
private String score64;
/** 近三年股东变更次数 */
@TableField("score_6_5")
private String score65;
/** 近三年经营范围变更次数 */
@TableField("score_6_6")
private String score66;
/** 近三年经营地址变更次数 */
@TableField("score_6_7")
private String score67;
/** 近三年缴税年数 */
@TableField("score_6_8")
private String score68;
/** 法人户籍 */
@TableField("score_6_9")
private String score69;
/** 法人婚姻状况 */
@TableField("score_6_10")
private String score610;
/** 七、企业经营能力模块 */
/** 上年增值税金额 */
@TableField("score_7_1")
private String score71;
/** 今年增值税同比变动 */
@TableField("score_7_2")
private String score72;
/** 上年企业所得税金额 */
@TableField("score_7_3")
private String score73;
/** 今年所得税同比变动 */
@TableField("score_7_4")
private String score74;
/** 缴纳社保人数同比变动 */
@TableField("score_7_5")
private String score75;
/** 公积金缴纳人数同比变动 */
@TableField("score_7_6")
private String score76;
/** 当年纳税金额同比变动 */
@TableField("score_7_7")
private String score77;
/** 上年出口退税金额 */
@TableField("score_7_8")
private String score78;
/** 八、企业偿债能力模块 */
/** 房产套数 */
@TableField("score_8_1")
private String score81;
/** 房产面积 */
@TableField("score_8_2")
private String score82;
/** 未抵押房产套数 */
@TableField("score_8_3")
private String score83;
/** 未抵押房产面积 */
@TableField("score_8_4")
private String score84;
/** 已抵押房产被担保债权数额 */
@TableField("score_8_5")
private String score85;
/** 企业车产金额 */
@TableField("score_8_6")
private String score86;
/** 九、潜在代偿资源模块 */
/** 法人及股东房产面积合计 */
@TableField("score_9_1")
private String score91;
/** 法人及股东房产套数合计 */
@TableField("score_9_2")
private String score92;
/** 法人及股东未抵押房产套数合计 */
@TableField("score_9_3")
private String score93;
/** 法人及股东未抵押房产面积 */
@TableField("score_9_4")
private String score94;
/** 法人及股东已抵押房产被担保债权数额合计 */
@TableField("score_9_5")
private String score95;
/** 法人代表车产金额 */
@TableField("score_9_6")
private String score96;
/** 十、企业环境信用模块 */
/** 生态信用扣分 */
@TableField("score_b015")
private String scoreB015;
/** 列入环境失信黑名单时间 */
@TableField("score_b016_time")
private String scoreB016Time;
/** 列入环境失信黑名单原因 */
@TableField("score_b016_reason")
private String scoreB016Reason;
/** 环境行为信用评价等级 */
@TableField("score_a020")
private String scoreA020;
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.ibs.list.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class VisitFeedbackItemDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "反馈类型")
private String feedbackType;
@ApiModelProperty(value = "反馈产品列表")
private List<String> products;
}

View File

@@ -117,4 +117,40 @@ public class VisitInfo {
@ApiModelProperty(value = "走访备注") @ApiModelProperty(value = "走访备注")
private String remark; private String remark;
@ApiModelProperty(value = "互动地址")
private String interAddr;
@ApiModelProperty(value = "协同员工名称")
private String colStafName;
@ApiModelProperty(value = "后续备注")
private String laterNote;
@ApiModelProperty(value = "意向产品值")
private String intentionProductValue;
@ApiModelProperty(value = "反馈状态")
private String feedbackStatus;
@ApiModelProperty(value = "走访来源")
private String sourceOfInterview;
@ApiModelProperty(value = "文件名")
private String filename;
@ApiModelProperty(value = "外呼状态")
private String outCallStatus;
@ApiModelProperty(value = "外呼意向")
private String outCallIntention;
@ApiModelProperty(value = "来源")
private String source;
@ApiModelProperty(value = "分析值")
private String analysisValue;
@ApiModelProperty(value = "设备")
private String facility;
} }

View File

@@ -13,6 +13,10 @@ public class VisitInfoDTO {
@ApiModelProperty(value = "柜员名称") @ApiModelProperty(value = "柜员名称")
private String visName; private String visName;
/** 柜员号 */
@ApiModelProperty(value = "柜员号")
private String visId;
/** 走访时间 */ /** 走访时间 */
@ApiModelProperty(value = "走访时间") @ApiModelProperty(value = "走访时间")
private String visTime; private String visTime;

View File

@@ -0,0 +1,31 @@
package com.ruoyi.ibs.list.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class VisitInfoFeedbackUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "走访记录ID")
private Long id;
@ApiModelProperty(value = "走访渠道")
private String source;
@ApiModelProperty(value = "客户意愿结构化数据")
private List<VisitFeedbackItemDTO> feedbackItems;
@ApiModelProperty(value = "客户意愿拼接值")
private String intentionProductValue;
private String userName;
private String userRole;
private String deptId;
}

View File

@@ -128,4 +128,40 @@ public class VisitInfoVO {
@ApiModelProperty(value = "走访结果") @ApiModelProperty(value = "走访结果")
private String interRes; private String interRes;
@ApiModelProperty(value = "实地拜访地址")
private String interAddr;
@ApiModelProperty(value = "协同走访客户经理")
private String colStafName;
@ApiModelProperty(value = "事后备注")
private String laterNote;
@ApiModelProperty(value = "走访反馈")
private String intentionProductValue;
@ApiModelProperty(value = "反馈状态")
private String feedbackStatus;
@ApiModelProperty(value = "走访来源")
private String sourceOfInterview;
@ApiModelProperty(value = "批量导入文件名")
private String filename;
@ApiModelProperty(value = "外呼状态")
private String outCallStatus;
@ApiModelProperty(value = "客户意愿")
private String outCallIntention;
@ApiModelProperty(value = "走访来源1nullpad 2企业微信 3pc")
private String source;
@ApiModelProperty(value = "nlp模型提取")
private String analysisValue;
@ApiModelProperty(value = "预授信额度")
private String facility;
} }

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

@@ -156,6 +156,8 @@ public interface SysCampaignMapper extends BaseMapper<SysCampaign> {
List<VisitInfoVO> selectVisitInfoList(VisitInfoDTO visitInfoDTO); List<VisitInfoVO> selectVisitInfoList(VisitInfoDTO visitInfoDTO);
int updateVisitInfoFeedback(VisitInfoFeedbackUpdateDTO updateDTO);
@Update("UPDATE sys_campaign SET del_flag = '2' where campaign_id = #{campaignId}") @Update("UPDATE sys_campaign SET del_flag = '2' where campaign_id = #{campaignId}")
int deleteSysCampaignByCampaignId(@Param("campaignId") String campaignId); int deleteSysCampaignByCampaignId(@Param("campaignId") String campaignId);

View File

@@ -115,5 +115,7 @@ public interface ISysCampaignService {
public List<VisitInfoVO> selectVisitInfoVoList(VisitInfoDTO visitInfoDTO); public List<VisitInfoVO> selectVisitInfoVoList(VisitInfoDTO visitInfoDTO);
int updateVisitInfoFeedback(VisitInfoFeedbackUpdateDTO updateDTO);
public int deleteSysCampaign(String campaignId); public int deleteSysCampaign(String campaignId);
} }

View File

@@ -24,7 +24,9 @@ import com.ruoyi.ibs.grid.util.CustExcelUtil;
import com.ruoyi.ibs.list.domain.*; import com.ruoyi.ibs.list.domain.*;
import com.ruoyi.ibs.list.mapper.CorporateShareholderMapper; import com.ruoyi.ibs.list.mapper.CorporateShareholderMapper;
import com.ruoyi.ibs.list.mapper.CustInfoRetailMapper; 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.FamilyMembersMapper;
import com.ruoyi.ibs.list.mapper.NineVFinalInfoOrcMapper;
import com.ruoyi.ibs.list.mapper.SignedProductsMapper; import com.ruoyi.ibs.list.mapper.SignedProductsMapper;
import com.ruoyi.ibs.list.service.ICustInfoBusinessService; import com.ruoyi.ibs.list.service.ICustInfoBusinessService;
import com.ruoyi.system.mapper.SysIndustryMapper; import com.ruoyi.system.mapper.SysIndustryMapper;
@@ -75,6 +77,12 @@ public class CustInfoBusinessServiceImpl implements ICustInfoBusinessService
@Autowired @Autowired
private FamilyMembersMapper familyMembersMapper; private FamilyMembersMapper familyMembersMapper;
@Autowired
private Ent9vPortraitOrcMapper ent9vPortraitOrcMapper;
@Autowired
private NineVFinalInfoOrcMapper nineVFinalInfoOrcMapper;
@Autowired @Autowired
private CustMapMapper custMapMapper; private CustMapMapper custMapMapper;
@@ -138,6 +146,17 @@ public class CustInfoBusinessServiceImpl implements ICustInfoBusinessService
//查询手动标签列表 //查询手动标签列表
List<CustManualTagVO> tagCreateVos = familyMembersMapper.selectManualTag(params); List<CustManualTagVO> tagCreateVos = familyMembersMapper.selectManualTag(params);
custInfoBusinessVo.setTagManual(TreeNode.convertToTreeByParentId(tagCreateVos)); 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; return custInfoBusinessVo;
} }

View File

@@ -2178,6 +2178,23 @@ public class SysCampaignServiceImpl implements ISysCampaignService
return sysCampaignMapper.selectVisitInfoList(visitInfoDTO); return sysCampaignMapper.selectVisitInfoList(visitInfoDTO);
} }
@Override
public int updateVisitInfoFeedback(VisitInfoFeedbackUpdateDTO updateDTO) {
if (updateDTO == null || updateDTO.getId() == null) {
throw new ServiceException("走访记录ID不能为空");
}
updateDTO.setUserName(SecurityUtils.getUsername());
updateDTO.setUserRole(SecurityUtils.userRole());
updateDTO.setDeptId(String.valueOf(SecurityUtils.getDeptId()));
updateDTO.setSource(StringUtils.trimToNull(updateDTO.getSource()));
updateDTO.setIntentionProductValue(buildIntentionProductValue(updateDTO.getFeedbackItems()));
int rows = sysCampaignMapper.updateVisitInfoFeedback(updateDTO);
if (rows <= 0) {
throw new ServiceException("走访记录不存在或无权限修改");
}
return rows;
}
@Override @Override
public int deleteSysCampaign(String campaignId) { public int deleteSysCampaign(String campaignId) {
SysCampaign sysCampaign = sysCampaignMapper.selectSysCampaignByCampaignId(campaignId); SysCampaign sysCampaign = sysCampaignMapper.selectSysCampaignByCampaignId(campaignId);
@@ -2186,4 +2203,28 @@ public class SysCampaignServiceImpl implements ISysCampaignService
} }
return sysCampaignMapper.deleteSysCampaignByCampaignId(campaignId); return sysCampaignMapper.deleteSysCampaignByCampaignId(campaignId);
} }
private String buildIntentionProductValue(List<VisitFeedbackItemDTO> feedbackItems) {
if (feedbackItems == null || feedbackItems.isEmpty()) {
return null;
}
String joinedValue = feedbackItems.stream()
.filter(Objects::nonNull)
.map(item -> {
String feedbackType = StringUtils.trimToNull(item.getFeedbackType());
List<String> products = Optional.ofNullable(item.getProducts())
.orElse(Collections.emptyList())
.stream()
.map(StringUtils::trimToNull)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (feedbackType == null || products.isEmpty()) {
return null;
}
return feedbackType + ":" + String.join(",", products);
})
.filter(Objects::nonNull)
.collect(Collectors.joining(";"));
return StringUtils.trimToNull(joinedValue);
}
} }

View File

@@ -131,4 +131,15 @@ public class WorkRecordController extends BaseController {
return toAjax(workRecordService.updateReadTime(id)); 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); List<AlterConfigVO> selectAlterConfigList(String alterType);
int updateAlterConfig(AlterConfig alterConfig); 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); int updateReadTime(Long id);
AlterCountVO getAlterCount(Date reportTime); 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.github.pagehelper.Page;
import com.ruoyi.common.core.page.TableDataPageInfo; 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.PageUtils;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.ibs.task.domain.dto.WorkRecordDTO; import com.ruoyi.ibs.task.domain.dto.WorkRecordDTO;
@@ -39,6 +40,8 @@ import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class WorkRecordServiceImpl implements WorkRecordService { public class WorkRecordServiceImpl implements WorkRecordService {
private final static String alterTypesRedisKey = "work:record:alter:types";
@Autowired @Autowired
private WorkRecordMapper workRecordMapper; private WorkRecordMapper workRecordMapper;
@@ -48,6 +51,9 @@ public class WorkRecordServiceImpl implements WorkRecordService {
@Autowired @Autowired
private ISysDeptService sysDeptService; private ISysDeptService sysDeptService;
@Autowired
private RedisCache redisCache;
/** /**
* 查询我的工作清单 * 查询我的工作清单
* *
@@ -253,6 +259,19 @@ public class WorkRecordServiceImpl implements WorkRecordService {
return alterCountVO; 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 * 获取当前登录用户权限范围内所有post_ids
* @return * @return

View File

@@ -857,6 +857,28 @@
select id,cust_name from cust_info_business where social_credit_code = #{socialCreditCode} select id,cust_name from cust_info_business where social_credit_code = #{socialCreditCode}
</select> </select>
<select id="selectExistingSocialCreditCodes" resultType="java.lang.String">
select social_credit_code
from cust_info_business_${deptCode}
where social_credit_code in
<foreach item="socialCreditCode" collection="socialCreditCodes" open="(" separator="," close=")">
#{socialCreditCode}
</foreach>
</select>
<update id="batchUpdateCustLevelBySocialCreditCode">
update cust_info_business_${deptCode}
set cust_level = case social_credit_code
<foreach item="item" collection="list">
when #{item.socialCreditCode} then #{item.custLevel}
</foreach>
end
where social_credit_code in
<foreach item="item" collection="list" open="(" separator="," close=")">
#{item.socialCreditCode}
</foreach>
</update>
<insert id="insertCustomersToBusinessByScCode" parameterType="java.util.List"> <insert id="insertCustomersToBusinessByScCode" parameterType="java.util.List">
INSERT INTO cust_info_business INSERT INTO cust_info_business
(cust_id,cust_name, social_credit_code, update_by, update_time, cust_phone, industry, asset, credit) (cust_id,cust_name, social_credit_code, update_by, update_time, cust_phone, industry, asset, credit)
@@ -866,4 +888,4 @@
</foreach> </foreach>
</insert> </insert>
</mapper> </mapper>

View File

@@ -193,7 +193,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and status = #{status} and status = #{status}
</if> </if>
<if test="alterType != null and alterType != ''"> <if test="alterType != null and alterType != ''">
and alter_type LIKE CONCAT('%', #{alterType}, '%') and alter_type = #{alterType}
</if> </if>
</where> </where>
order by create_time desc, status asc order by create_time desc, status asc
@@ -232,7 +232,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and wr.status = #{status} and wr.status = #{status}
</if> </if>
<if test="alterType != null and alterType != ''"> <if test="alterType != null and alterType != ''">
and wr.alter_type LIKE CONCAT('%', #{alterType}, '%') and wr.alter_type = #{alterType}
</if> </if>
<!-- "走访异常提醒"类型直接通过用户名匹配,其他类型按角色权限处理 --> <!-- "走访异常提醒"类型直接通过用户名匹配,其他类型按角色权限处理 -->
and ((wr.alter_type = '走访异常提醒' and wr.user_name = #{username}) or 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} where id = #{id}
</update> </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> </mapper>

View File

@@ -505,4 +505,26 @@
where manager_id is not null where manager_id is not null
</select> </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> </mapper>

View File

@@ -55,8 +55,17 @@
<if test="custName != null and custName != ''">AND b.cust_name like concat('%', #{custName}, '%')</if> <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> <if test="custType != null and custType != ''">AND b.cust_type = #{custType}</if>
</select> </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> </select>
</mapper> </mapper>

View File

@@ -121,4 +121,30 @@
WHERE sec_grid_id = #{gridId} WHERE sec_grid_id = #{gridId}
AND cust_type = #{custType} AND cust_type = #{custType}
</select> </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> </mapper>

View File

@@ -339,4 +339,26 @@
<select id="getSecGridIdByTopGridId" resultType="Long"> <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 grid_id from grid_region_grid where parent_grid_id = #{gridId} and grid_level = '2' and delete_flag = '0'
</select> </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> </mapper>

View File

@@ -127,6 +127,18 @@
<result property="remark" column="remark" /> <result property="remark" column="remark" />
<result property="custType" column="cust_type" /> <result property="custType" column="cust_type" />
<result property="deptName" column="dept_name" /> <result property="deptName" column="dept_name" />
<result property="interAddr" column="inter_addr" />
<result property="colStafName" column="col_staf_name" />
<result property="laterNote" column="later_note" />
<result property="intentionProductValue" column="intention_product_value" />
<result property="feedbackStatus" column="feedback_status" />
<result property="sourceOfInterview" column="source_of_interview" />
<result property="filename" column="filename" />
<result property="outCallStatus" column="out_call_status" />
<result property="outCallIntention" column="out_call_intention" />
<result property="source" column="source" />
<result property="analysisValue" column="analysis_value" />
<result property="facility" column="facility" />
</resultMap> </resultMap>
<sql id="selectSysCampaignVo"> <sql id="selectSysCampaignVo">
@@ -1384,12 +1396,15 @@
<select id="selectVisitInfoList" parameterType="VisitInfoDTO" resultType="VisitInfoVO"> <select id="selectVisitInfoList" parameterType="VisitInfoDTO" resultType="VisitInfoVO">
select vi.id,vi.campaign_id,vi.campaign_name,vi.vis_name,vi.vis_id,vi.dept_id,d.dept_name,vi.vis_time,vi.cust_name,vi.cust_type,vi.cust_idc, select vi.id,vi.campaign_id,vi.campaign_name,vi.vis_name,vi.vis_id,vi.dept_id,d.dept_name,vi.vis_time,vi.cust_name,vi.cust_type,vi.cust_idc,
vi.social_credit_code,vi.create_by,vi.create_time,vi.update_by,vi.update_time,vi.remark,vi.sign_in_time,vi.sign_out_time,vi.sign_in_address, vi.social_credit_code,vi.create_by,vi.create_time,vi.update_by,vi.update_time,vi.remark,vi.sign_in_time,vi.sign_out_time,vi.sign_in_address,
vi.sign_out_address,vi.abnormal_visit_tag,vi.abnormal_visit_info,vi.sign_in_coordinate,vi.sign_out_coordinate,vi.is_valid_cust,vi.marketing_way,vi.inter_res vi.sign_out_address,vi.abnormal_visit_tag,vi.abnormal_visit_info,vi.sign_in_coordinate,vi.sign_out_coordinate,vi.is_valid_cust,vi.marketing_way,vi.inter_res,
vi.inter_addr,vi.col_staf_name,vi.later_note,vi.intention_product_value,vi.feedback_status,vi.source_of_interview,vi.filename,
vi.out_call_status,vi.out_call_intention,vi.source,vi.analysis_value,vi.facility
from visit_info vi from visit_info vi
left join sys_dept d on vi.dept_id = d.dept_id left join sys_dept d on vi.dept_id = d.dept_id
<where> <where>
<if test="custType != null and custType != ''"> and vi.cust_type = #{custType}</if> <if test="custType != null and custType != ''"> and vi.cust_type = #{custType}</if>
<if test="visName != null and visName != ''"> and vi.vis_name like concat('%', #{visName}, '%')</if> <if test="visName != null and visName != ''"> and vi.vis_name like concat('%', #{visName}, '%')</if>
<if test="visId != null and visId != ''"> and vi.vis_id = #{visId}</if>
<if test="custIdc != null and custIdc != ''"> and vi.cust_idc = #{custIdc}</if> <if test="custIdc != null and custIdc != ''"> and vi.cust_idc = #{custIdc}</if>
<if test="socialCreditCode != null and socialCreditCode != ''"> and vi.social_credit_code = #{socialCreditCode}</if> <if test="socialCreditCode != null and socialCreditCode != ''"> and vi.social_credit_code = #{socialCreditCode}</if>
<if test="abnormalVisitTag != null and abnormalVisitTag != ''"> and vi.abnormal_visit_tag = #{abnormalVisitTag}</if> <if test="abnormalVisitTag != null and abnormalVisitTag != ''"> and vi.abnormal_visit_tag = #{abnormalVisitTag}</if>
@@ -1410,4 +1425,22 @@
order by vi.sign_in_time desc order by vi.sign_in_time desc
</select> </select>
</mapper> <update id="updateVisitInfoFeedback" parameterType="VisitInfoFeedbackUpdateDTO">
update visit_info vi
left join sys_dept d on vi.dept_id = d.dept_id
set vi.source = #{source},
vi.intention_product_value = #{intentionProductValue},
vi.update_by = #{userName},
vi.update_time = sysdate()
where vi.id = #{id}
<if test="userRole != null">
<choose>
<when test="userRole == 'manager'"> and vi.vis_id = #{userName} </when>
<when test="userRole == 'outlet'"> and d.dept_id = #{deptId} </when>
<when test="userRole == 'branch'"> and (d.dept_id = #{deptId} or find_in_set(#{deptId},d.ancestors)) </when>
<when test="userRole in {'head', 'ops', 'public', 'private'}"> and left(d.dept_id,3) = left(#{deptId},3) </when>
</choose>
</if>
</update>
</mapper>

View File

@@ -215,6 +215,13 @@
<version>${ruoyi.version}</version> <version>${ruoyi.version}</version>
</dependency> </dependency>
<!-- 数字支行-客群模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ibs-group</artifactId>
<version>3.8.8</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

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

View File

@@ -38,6 +38,10 @@
<groupId>com.ruoyi</groupId> <groupId>com.ruoyi</groupId>
<artifactId>ibs</artifactId> <artifactId>ibs</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ibs-group</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@@ -1,6 +1,7 @@
package com.ruoyi.quartz.task; package com.ruoyi.quartz.task;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.group.service.ICustGroupService;
import com.ruoyi.ibs.cmpm.service.GridCmpmService; import com.ruoyi.ibs.cmpm.service.GridCmpmService;
import com.ruoyi.ibs.dashboard.service.FileOptService; import com.ruoyi.ibs.dashboard.service.FileOptService;
import com.ruoyi.ibs.draw.service.DrawGridCustService; import com.ruoyi.ibs.draw.service.DrawGridCustService;
@@ -47,6 +48,9 @@ public class RyTask
@Resource @Resource
private AddressAnalyseService addressAnalyseService; private AddressAnalyseService addressAnalyseService;
@Resource
private ICustGroupService custGroupService;
public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i)
{ {
@@ -111,4 +115,12 @@ public class RyTask
addressAnalyseService.pointInGeometryScheduled(); addressAnalyseService.pointInGeometryScheduled();
} }
public void updateDynamicCustGroups() {
custGroupService.updateDynamicCustGroups();
}
public void checkAndDisableExpiredGroups() {
custGroupService.checkAndDisableExpiredGroups();
}
} }

View File

@@ -31,6 +31,12 @@ public class SysNotice extends BaseEntity
/** 公告状态0正常 1关闭 */ /** 公告状态0正常 1关闭 */
private String status; private String status;
/** 可见总行部门ID多选逗号分隔 */
private String deptIds;
/** 当前登录用户所属总行部门ID仅用于查询过滤 */
private String currentHeadDeptId;
public Long getNoticeId() public Long getNoticeId()
{ {
return noticeId; return noticeId;
@@ -84,6 +90,26 @@ public class SysNotice extends BaseEntity
return status; return status;
} }
public String getDeptIds()
{
return deptIds;
}
public void setDeptIds(String deptIds)
{
this.deptIds = deptIds;
}
public String getCurrentHeadDeptId()
{
return currentHeadDeptId;
}
public void setCurrentHeadDeptId(String currentHeadDeptId)
{
this.currentHeadDeptId = currentHeadDeptId;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@@ -92,6 +118,7 @@ public class SysNotice extends BaseEntity
.append("noticeType", getNoticeType()) .append("noticeType", getNoticeType())
.append("noticeContent", getNoticeContent()) .append("noticeContent", getNoticeContent())
.append("status", getStatus()) .append("status", getStatus())
.append("deptIds", getDeptIds())
.append("createBy", getCreateBy()) .append("createBy", getCreateBy())
.append("createTime", getCreateTime()) .append("createTime", getCreateTime())
.append("updateBy", getUpdateBy()) .append("updateBy", getUpdateBy())

View File

@@ -133,8 +133,13 @@ public class SysDeptServiceImpl implements ISysDeptService
@Override @Override
public List<TreeSelect> selectDeptTreeListForTopGrid(SysDept dept) { public List<TreeSelect> selectDeptTreeListForTopGrid(SysDept dept) {
List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept); List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept);
List<SysDept> branchs = depts.stream().filter(sysDept -> !sysDept.getDeptType().equals("outlet")) if (depts == null || depts.isEmpty()) {
.filter(sysDept -> !sysDept.getDeptType().equals("head")) return Collections.emptyList();
}
List<SysDept> branchs = depts.stream()
.filter(Objects::nonNull)
.filter(sysDept -> !"outlet".equals(sysDept.getDeptType()))
.filter(sysDept -> !"head".equals(sysDept.getDeptType()))
.collect(Collectors.toList()); .collect(Collectors.toList());
return buildDeptTreeSelect(branchs); return buildDeptTreeSelect(branchs);
} }

View File

@@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="noticeType" column="notice_type" /> <result property="noticeType" column="notice_type" />
<result property="noticeContent" column="notice_content" /> <result property="noticeContent" column="notice_content" />
<result property="status" column="status" /> <result property="status" column="status" />
<result property="deptIds" column="dept_ids" />
<result property="createBy" column="create_by" /> <result property="createBy" column="create_by" />
<result property="createTime" column="create_time" /> <result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" /> <result property="updateBy" column="update_by" />
@@ -18,7 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectNoticeVo"> <sql id="selectNoticeVo">
select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, dept_ids, create_by, create_time, update_by, update_time, remark
from sys_notice from sys_notice
</sql> </sql>
@@ -39,6 +40,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createBy != null and createBy != ''"> <if test="createBy != null and createBy != ''">
AND create_by like concat('%', #{createBy}, '%') AND create_by like concat('%', #{createBy}, '%')
</if> </if>
<if test="currentHeadDeptId != null and currentHeadDeptId != ''">
AND (dept_ids is null or dept_ids = '' or find_in_set(#{currentHeadDeptId}, dept_ids))
</if>
</where> </where>
</select> </select>
@@ -48,6 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="noticeType != null and noticeType != '' ">notice_type, </if> <if test="noticeType != null and noticeType != '' ">notice_type, </if>
<if test="noticeContent != null and noticeContent != '' ">notice_content, </if> <if test="noticeContent != null and noticeContent != '' ">notice_content, </if>
<if test="status != null and status != '' ">status, </if> <if test="status != null and status != '' ">status, </if>
<if test="deptIds != null">dept_ids, </if>
<if test="remark != null and remark != ''">remark,</if> <if test="remark != null and remark != ''">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if> <if test="createBy != null and createBy != ''">create_by,</if>
create_time create_time
@@ -56,6 +61,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="noticeType != null and noticeType != ''">#{noticeType}, </if> <if test="noticeType != null and noticeType != ''">#{noticeType}, </if>
<if test="noticeContent != null and noticeContent != ''">#{noticeContent}, </if> <if test="noticeContent != null and noticeContent != ''">#{noticeContent}, </if>
<if test="status != null and status != ''">#{status}, </if> <if test="status != null and status != ''">#{status}, </if>
<if test="deptIds != null">#{deptIds}, </if>
<if test="remark != null and remark != ''">#{remark},</if> <if test="remark != null and remark != ''">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if> <if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate() sysdate()
@@ -69,6 +75,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="noticeType != null and noticeType != ''">notice_type = #{noticeType}, </if> <if test="noticeType != null and noticeType != ''">notice_type = #{noticeType}, </if>
<if test="noticeContent != null">notice_content = #{noticeContent}, </if> <if test="noticeContent != null">notice_content = #{noticeContent}, </if>
<if test="status != null and status != ''">status = #{status}, </if> <if test="status != null and status != ''">status = #{status}, </if>
<if test="deptIds != null">dept_ids = #{deptIds}, </if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if> <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
update_time = sysdate() update_time = sysdate()
</set> </set>
@@ -86,4 +93,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach> </foreach>
</delete> </delete>
</mapper> </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) { export function featureGridDelete(data) {
return request({ return request({

View File

@@ -91,4 +91,20 @@ export function uploadTag(data) {
data: data, data: data,
isUpload: true isUpload: true
}) })
} }
export function importBusinessCustLevelAsync(data) {
return request({
url: '/system/custBaseInfo/importBusinessCustLevelAsync',
method: 'post',
data,
isUpload: true
})
}
export function getBusinessCustLevelImportStatus(taskId) {
return request({
url: `/system/custBaseInfo/importBusinessCustLevelStatus/${taskId}`,
method: 'get'
})
}

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', method: 'get',
params: query params: query
}) })
}
// 查询所有预警类型
export function getAlterTypes() {
return request({
url: '/work/record/alter/types',
method: 'get'
})
} }

View File

@@ -41,4 +41,12 @@ export function delNotice(noticeId) {
url: '/system/notice/' + noticeId, url: '/system/notice/' + noticeId,
method: 'delete' method: 'delete'
}) })
} }
// 查询总行列表
export function getHeadList() {
return request({
url: '/system/dept/headList',
method: 'get'
})
}

View File

@@ -6,4 +6,12 @@ export function getPADVisitRecord(query) {
method: 'get', method: 'get',
params: query params: query
}) })
} }
export function updatePADVisitFeedback(data) {
return request({
url: `/system/campaign/updateVisitInfoFeedback`,
method: 'post',
data: data
})
}

View File

@@ -192,6 +192,10 @@ export default {
}, },
created() { created() {
this.getCenterList(); this.getCenterList();
window.addEventListener('notice-center-refresh', this.getCenterList);
},
beforeDestroy() {
window.removeEventListener('notice-center-refresh', this.getCenterList);
}, },
methods: { methods: {
openModal() { openModal() {

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', path: '/checklist/customerlist',
component: Layout, component: Layout,

View File

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

View File

@@ -1024,6 +1024,77 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-collapse-item> </el-collapse-item>
<el-collapse-item v-if="is825" name="11">
<template slot="title">
<p class="common-title">企业九维数据</p>
</template>
<div v-if="ent9vPortrait || nineVFinalInfo" class="nine-dimension-data">
<!-- 企业九维画像 -->
<div v-if="ent9vPortrait" class="nine-dimension-section">
<h4 class="section-title">1.企业九维画像</h4>
<div class="nine-dimension-chart-container">
<div class="radar-chart-wrapper">
<div ref="nineDimensionRadar" class="nine-dimension-radar"></div>
</div>
<div class="nine-dimension-summary">
<div class="summary-item">
<span class="summary-label">综合评分</span>
<div class="summary-content">
<span class="summary-value">{{ formatPortraitDisplay(ent9vPortrait.scoreAll) }}</span>
<span class="summary-unit">分</span>
</div>
</div>
<div class="summary-item">
<span class="summary-label">总分排名</span>
<div class="summary-content">
<span class="summary-value is-rank">{{ formatPortraitDisplay(ent9vPortrait.scoreAllRank) }}</span>
<span class="summary-unit">名</span>
</div>
</div>
</div>
</div>
</div>
<!-- 九维细项指标 -->
<div v-if="nineVFinalInfo" class="nine-dimension-section">
<h4 class="section-title">2.九维细项指标</h4>
<div class="nine-dimension-detail-table">
<table class="detail-table">
<tbody>
<template v-for="group in nineDimensionDetailGroups">
<tr
v-for="(row, rowIndex) in group.rows"
:key="`${group.title}-${rowIndex}`"
>
<td
v-if="rowIndex === 0"
class="category-cell"
:rowspan="group.rows.length"
>
{{ group.title }}
</td>
<template v-for="metric in row">
<td :key="`${group.title}-${metric.key}-label`" class="metric-label-cell">
{{ metric.label }}
</td>
<td :key="`${group.title}-${metric.key}-value`" class="metric-value-cell">
{{ formatNineDimensionValue(metric) }}
</td>
</template>
<template v-if="row.length === 1">
<td class="metric-label-cell is-empty"></td>
<td class="metric-value-cell is-empty"></td>
</template>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<el-empty v-else description="暂无企业九维数据"></el-empty>
</el-collapse-item>
</el-collapse> </el-collapse>
<el-dialog <el-dialog
title="该客户尚未建档,请先进行营销建档" title="该客户尚未建档,请先进行营销建档"
@@ -1215,7 +1286,120 @@ import { downloadFiles } from '@/utils'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { cloneDeep, isEmpty } from 'lodash' import { cloneDeep, isEmpty } from 'lodash'
import _ from 'lodash' import _ from 'lodash'
import * as echarts from 'echarts'
import Custom from '../custom.vue' import Custom from '../custom.vue'
const NINE_DIMENSION_DETAIL_CONFIG = [
{
title: '企业合规经营模块',
metrics: [
{ key: 'score11', label: '是否存在经营异常名录信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score12', label: '是否存在严重违法失信企业名单信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score13', label: '企业环境行为信用等级是否为“E”或“D”', valueMap: { '1': '是', '2': '否' } },
{ key: 'score14', label: '是否存在税务重大税收违法黑名单信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score15', label: '是否存在拖欠工资黑名单', valueMap: { '1': '是', '2': '否' } },
{ key: 'score16', label: '是否存在工商吊销企业信息', valueMap: { '1': '是', '2': '否' } }
]
},
{
title: '企业风险准入模块',
metrics: [
{ key: 'score21', label: '是否存在注销企业信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score22', label: '是否存在执行案件信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score23', label: '是否存在查封信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score24', label: '是否存在单位未履行生效裁判信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score25', label: '是否存在企业破产清算信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score26', label: '是否失信被执行人', valueMap: { '1': '是', '2': '否' } },
{ key: 'score27', label: '是否为诉讼案件被告', valueMap: { '1': '是', '2': '否' } }
]
},
{
title: '高管信用评价模块',
metrics: [
{ key: 'score31', label: '是否存在查封信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score32', label: '是否存在执行案件信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score33', label: '是否存在个人未履行生效裁判信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score34', label: '是否存在拖欠工资黑名单', valueMap: { '1': '是', '2': '否' } },
{ key: 'score35', label: '是否失信被执行人', valueMap: { '1': '是', '2': '否' } },
{ key: 'score36', label: '是否存在刑事案件被告人生效判决信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score37', label: '是否为诉讼案件被告', valueMap: { '1': '是', '2': '否' } }
]
},
{
title: '股东信用评价模块',
metrics: [
{ key: 'score41', label: '是否存在查封信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score42', label: '是否存在执行案件信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score43', label: '是否存在个人未履行生效裁判信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score44', label: '是否存在拖欠工资黑名单', valueMap: { '1': '是', '2': '否' } },
{ key: 'score45', label: '是否失信被执行人', valueMap: { '1': '是', '2': '否' } },
{ key: 'score46', label: '是否存在刑事案件被告人生效判决信息', valueMap: { '1': '是', '2': '否' } },
{ key: 'score47', label: '是否为诉讼案件被告', valueMap: { '1': '是', '2': '否' } },
{ key: 'score48', label: '是否存在企业未履行生效裁判信息', valueMap: { '1': '是', '2': '否' } }
]
},
{
title: '企业社会贡献模块',
metrics: [
{ key: 'score51', label: '前12个月纳税金额', valueMap: { '1': '0', '2': '05万元', '3': '5-10万元', '4': '10-50万元', '5': '50-100万元', '6': '100-500万元', '7': '500-1000万元', '8': '1000万元以上' } },
{ key: 'score52', label: '纳税等级', valueMap: { '1': 'A', '2': 'B', '3': 'M', '4': 'C、D', '5': '未评' } },
{ key: 'score53', label: '缴纳社保人数', valueMap: { '1': '0', '2': '3人以内', '3': '3-10人', '4': '10-30人', '5': '30-50人', '6': '50-100人', '7': '100-500人', '8': '500人以上' } },
{ key: 'score54', label: '公积金缴纳人数', valueMap: { '1': '0', '2': '3人以内', '3': '3-10人', '4': '10-30人', '5': '30-50人', '6': '50-100人', '7': '100-500人', '8': '500人以上' } },
{ key: 'score55', label: '是否为出口退税生产清单企业', valueMap: { '1': '是', '2': '否' } }
]
},
{
title: '企业稳定经营模块',
metrics: [
{ key: 'score61', label: '市场主体经营年限', valueMap: { '1': '1年以内', '2': '1-3年', '3': '3-5年', '4': '5-10年', '5': '10年以上' } },
{ key: 'score62', label: '股东(或发起人)或投资人信息认缴出资人数', valueMap: { '1': '1人独资', '2': '2-5人', '3': '5人以上' } },
{ key: 'score63', label: '股东(或发起人)或投资人信息最大股东持股占比', valueMap: { '1': '10%以内(含)', '2': '10%-30%(含)', '3': '30%-50%(含)', '4': '50%-66.6%(含)', '5': '66.6%以上' } },
{ key: 'score64', label: '近三年法定代表人变更次数', valueMap: { '1': '0次', '2': '1次', '3': '2次', '4': '3次及以上' } },
{ key: 'score65', label: '近三年股东变更次数', valueMap: { '1': '0次', '2': '1次', '3': '2次', '4': '3次及以上' } },
{ key: 'score66', label: '近三年经营范围变更次数', valueMap: { '1': '0次', '2': '1次', '3': '2次', '4': '3次及以上' } },
{ key: 'score67', label: '近三年经营地址变更次数', valueMap: { '1': '0次', '2': '1次', '3': '2次', '4': '3次及以上' } },
{ key: 'score68', label: '近三年缴税年数', valueMap: { '1': '0', '2': '1年', '3': '2年', '4': '3年' } },
{ key: 'score69', label: '法人户籍', valueMap: { '1': '户籍本地、原籍本地', '2': '户籍本地、原籍浙江其他地区', '3': '户籍本地、原籍未浙江', '4': '非本地户籍、浙江籍', '5': '非本地户籍、未浙江籍' } },
{ key: 'score610', label: '法人婚姻状况', valueMap: { '1': '未婚', '2': '已婚', '3': '丧偶', '4': '离婚' } }
]
},
{
title: '企业经营能力模块',
metrics: [
{ key: 'score71', label: '上年增值税金额', valueMap: { '1': '0', '2': '05万元', '3': '5-10万元', '4': '10-30万元', '5': '30-50万元', '6': '50-100万元', '7': '100-500万元', '8': '500-1000万元', '9': '1000万元以上' } },
{ key: 'score72', label: '今年增值税同比变动', valueMap: { '1': '0及以下', '2': '05%(含)', '3': '5%10%(含)', '4': '10%-20%(含)', '5': '20%以上' } },
{ key: 'score73', label: '上年企业所得税金额', valueMap: { '1': '0', '2': '05万元', '3': '5-10万元', '4': '10-30万元', '5': '30-50万元', '6': '50-100万元', '7': '100-500万元', '8': '500-1000万元', '9': '1000万元以上' } },
{ key: 'score74', label: '今年所得税同比变动', valueMap: { '1': '0及以下', '2': '05%(含)', '3': '5%10%(含)', '4': '10%-20%(含)', '5': '20%以上' } },
{ key: 'score75', label: '缴纳社保人数同比变动', valueMap: { '1': '0及以下', '2': '05%(含)', '3': '5%10%(含)', '4': '10%-20%(含)', '5': '20%以上' } },
{ key: 'score76', label: '公积金缴纳人数同比变动', valueMap: { '1': '0及以下', '2': '05%(含)', '3': '5%10%(含)', '4': '10%-20%(含)', '5': '20%以上' } },
{ key: 'score77', label: '当年纳税金额同比变动', valueMap: { '1': '0及以下', '2': '05%(含)', '3': '5%10%(含)', '4': '10%-20%(含)', '5': '20%以上' } },
{ key: 'score78', label: '上年出口退税金额', valueMap: { '1': '0', '2': '05万元', '3': '5-10万元', '4': '10-50万元', '5': '50-100万元', '6': '100万元以上' } }
]
},
{
title: '企业偿债能力模块',
metrics: [
{ key: 'score81', label: '房产套数', valueMap: { '1': '0', '2': '1套', '3': '2套', '4': '3套', '5': '4套及以上' } },
{ key: 'score82', label: '房产面积', valueMap: { '1': '0', '2': '0500平方米', '3': '5001000平方米以下', '4': '1000-3000平方米', '5': '3000-5000平方米', '6': '5000-10000平方米', '7': '10000平方米及以上' } },
{ key: 'score83', label: '未抵押房产套数', valueMap: { '1': '0', '2': '1套', '3': '2套', '4': '3套', '5': '4套及以上' } },
{ key: 'score84', label: '未抵押房产面积', valueMap: { '1': '0', '2': '0500平方米', '3': '5001000平方米以下', '4': '1000-3000平方米', '5': '3000-5000平方米', '6': '5000-10000平方米', '7': '10000平方米及以上' } },
{ key: 'score85', label: '已抵押房产被担保债权数额', valueMap: { '1': '0', '2': '0100万元', '3': '100500万元', '4': '5001000万元', '5': '10003000万元', '6': '30005000万元', '7': '500010000万元', '8': '10000万元以上' } },
{ key: 'score86', label: '企业车产金额', valueMap: { '1': '0', '2': '020万元', '3': '2040万元', '4': '4060万元', '5': '6080万元', '6': '80100万元', '7': '100万元以上' } }
]
},
{
title: '潜在代偿资源模块',
metrics: [
{ key: 'score91', label: '法人代表及股东房产面积合计', valueMap: { '1': '0', '2': '0500平方米', '3': '5001000平方米以下', '4': '1000-3000平方米', '5': '3000-5000平方米', '6': '5000-10000平方米', '7': '10000平方米及以上' } },
{ key: 'score92', label: '法人代表及股东房产套数合计', valueMap: { '1': '0', '2': '1套', '3': '2套', '4': '3套', '5': '4套及以上' } },
{ key: 'score93', label: '法人代表及股东未抵押房产套数合计', valueMap: { '1': '0', '2': '1套', '3': '2套', '4': '3套', '5': '4套及以上' } },
{ key: 'score94', label: '法人代表及股东未抵押房产面积', valueMap: { '1': '0', '2': '0500平方米', '3': '5001000平方米以下', '4': '1000-3000平方米', '5': '3000-5000平方米', '6': '5000-10000平方米', '7': '10000平方米及以上' } },
{ key: 'score95', label: '法人代表及股东已抵押房产被担保债权数额合计', valueMap: { '1': '0', '2': '0100万元', '3': '100500万元', '4': '5001000万元', '5': '10003000万元', '6': '30005000万元', '7': '500010000万元', '8': '10000万元以上' } },
{ key: 'score96', label: '法人代表车产金额', valueMap: { '1': '0', '2': '020万元', '3': '2040万元', '4': '4060万元', '5': '6080万元', '6': '80100万元', '7': '100万元以上' } }
]
}
]
export default { export default {
data() { data() {
var validatePhone = (rule, value, callback) => { var validatePhone = (rule, value, callback) => {
@@ -1347,6 +1531,12 @@ export default {
showTagsList:[], showTagsList:[],
addTagName:"", addTagName:"",
newCustTag:[], newCustTag:[],
// 企业九维数据
ent9vPortrait: null,
nineVFinalInfo: null,
nineDimensionRadarChart: null, // 九维画像雷达图实例
nineDimensionRadarResizeObserver: null,
nineDimensionRadarRenderTimer: null,
regAddress: { regAddress: {
lazy: true, lazy: true,
lazyLoad(node, resolve) { lazyLoad(node, resolve) {
@@ -1403,13 +1593,40 @@ export default {
this.managerOptions = [] this.managerOptions = []
this.authUser = '' this.authUser = ''
} }
},
ent9vPortrait: {
handler(newVal) {
if (!newVal) {
this.destroyNineDimensionRadar()
return
}
this.scheduleNineDimensionRadarRender()
},
deep: true
} }
}, },
components: { components: {
Custom Custom
}, },
computed: { computed: {
...mapGetters(['roles', 'userName']), ...mapGetters(['roles', 'userName', 'deptId']),
nineDimensionDetailGroups() {
return NINE_DIMENSION_DETAIL_CONFIG.map(group => ({
title: group.title,
rows: this.buildNineDimensionRows(group.metrics)
}))
},
nineDimensionScores() {
if (!this.ent9vPortrait) {
return []
}
return [1, 2, 3, 4, 5, 6, 7, 8, 9].map(index =>
this.normalizePortraitScore(this.ent9vPortrait[`score${index}`])
)
},
nineDimensionRadarScale() {
return this.getAdaptiveRadarScale(this.nineDimensionScores)
},
isHeadAdmin() { isHeadAdmin() {
return this.roles.includes('headAdmin') return this.roles.includes('headAdmin')
}, },
@@ -1443,6 +1660,9 @@ export default {
// 海宁 // 海宁
is875() { is875() {
return this.userName.slice(0, 3) === '875' return this.userName.slice(0, 3) === '875'
},
is825() {
return String(this.deptId || '').substring(0, 3) === '825'
} }
}, },
created() { created() {
@@ -1464,6 +1684,16 @@ export default {
// systemUserAllTreeUser().then(res => { // systemUserAllTreeUser().then(res => {
// this.secoureOption = res // this.secoureOption = res
// }) // })
window.addEventListener('resize', this.handleRadarResize);
},
activated() {
this.scheduleNineDimensionRadarRender(120)
},
beforeDestroy() {
this.clearNineDimensionRadarRenderTimer()
this.destroyNineDimensionRadarObserver()
this.destroyNineDimensionRadar()
window.removeEventListener('resize', this.handleRadarResize);
}, },
methods: { methods: {
handleAddTag(){ handleAddTag(){
@@ -1591,6 +1821,9 @@ export default {
this.showTagsList=this.tagsList[0].children[0].children||[]; this.showTagsList=this.tagsList[0].children[0].children||[];
} }
this.tagManualList = cloneDeep(res.data.tagManual) || [] this.tagManualList = cloneDeep(res.data.tagManual) || []
// 企业九维数据
this.ent9vPortrait = res.data.ent9vPortrait || null
this.nineVFinalInfo = res.data.nineVFinalInfo || null
} }
}) })
}, },
@@ -2014,6 +2247,381 @@ export default {
this.managerOptions = response.data; this.managerOptions = response.data;
}); });
}, },
normalizePortraitScore(value) {
const numericValue = Number(value)
return Number.isFinite(numericValue) ? numericValue : 0
},
getAdaptiveRadarScale(scores) {
const maxScore = Math.max(...scores, 0)
if (maxScore <= 0) {
return {
max: 10,
splitNumber: 5,
step: 2
}
}
const splitNumber = 5
const roughStep = maxScore / splitNumber
const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)))
const normalized = roughStep / magnitude
let niceFactor = 10
if (normalized <= 1) {
niceFactor = 1
} else if (normalized <= 2) {
niceFactor = 2
} else if (normalized <= 2.5) {
niceFactor = 2.5
} else if (normalized <= 5) {
niceFactor = 5
}
const step = niceFactor * magnitude
let max = Math.ceil(maxScore / step) * step
if (max <= maxScore) {
max += step
}
return {
max,
splitNumber,
step
}
},
formatPortraitDisplay(value) {
if (value === null || value === undefined || value === '') {
return '-'
}
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) {
return value
}
return Number.isInteger(numericValue) ? `${numericValue}` : numericValue.toFixed(2)
},
clearNineDimensionRadarRenderTimer() {
if (this.nineDimensionRadarRenderTimer) {
clearTimeout(this.nineDimensionRadarRenderTimer)
this.nineDimensionRadarRenderTimer = null
}
},
scheduleNineDimensionRadarRender(delay = 80) {
this.clearNineDimensionRadarRenderTimer()
this.nineDimensionRadarRenderTimer = setTimeout(() => {
this.initNineDimensionRadar()
}, delay)
},
destroyNineDimensionRadar() {
if (this.nineDimensionRadarChart) {
this.nineDimensionRadarChart.dispose();
this.nineDimensionRadarChart = null;
}
},
destroyNineDimensionRadarObserver() {
if (this.nineDimensionRadarResizeObserver) {
this.nineDimensionRadarResizeObserver.disconnect()
this.nineDimensionRadarResizeObserver = null
}
},
initNineDimensionRadarObserver(chartDom) {
if (!chartDom || typeof ResizeObserver === 'undefined' || this.nineDimensionRadarResizeObserver) {
return
}
this.nineDimensionRadarResizeObserver = new ResizeObserver((entries) => {
const targetRect = entries && entries[0] ? entries[0].contentRect : null
if (this.nineDimensionRadarChart) {
this.handleRadarResize()
return
}
if (targetRect && targetRect.width >= 360 && targetRect.height >= 260) {
this.scheduleNineDimensionRadarRender(0)
}
})
this.nineDimensionRadarResizeObserver.observe(chartDom)
if (chartDom.parentElement) {
this.nineDimensionRadarResizeObserver.observe(chartDom.parentElement)
}
},
formatRadarLabel(label, chunkSize = 7) {
if (!label || label.length <= chunkSize) {
return label
}
const segments = []
for (let index = 0; index < label.length; index += chunkSize) {
segments.push(label.slice(index, index + chunkSize))
}
return segments.join('\n')
},
// 九维画像分数字段标签映射
getPortraitLabel(key) {
const labelMap = {
uniscid: '统一社会信用代码',
cstId: '客户内码',
orgNo: '机构号',
score1: '企业合规经营模块',
score2: '企业风险准入模块',
score3: '高管信用评价模块',
score4: '股东信用评价模块',
score5: '企业社会贡献模块',
score6: '企业稳定经营模块',
score7: '企业经营能力模块',
score8: '企业偿债能力模块',
score9: '潜在代偿资源模块',
scoreAll: '九维总分',
scoreAllRank: '九维总分排名',
datDt: '会计日期'
};
return labelMap[key] || key;
},
// 获取九维画像维度名称列表
getNineDimensionLabels() {
return [
'企业合规经营模块',
'企业风险准入模块',
'高管信用评价模块',
'股东信用评价模块',
'企业社会贡献模块',
'企业稳定经营模块',
'企业经营能力模块',
'企业偿债能力模块',
'潜在代偿资源模块'
];
},
// 初始化九维画像雷达图
initNineDimensionRadar() {
if (!this.ent9vPortrait) {
this.destroyNineDimensionRadar()
return;
}
this.$nextTick(() => {
const chartDom = this.$refs.nineDimensionRadar;
if (!chartDom) return;
this.initNineDimensionRadarObserver(chartDom)
const chartWidth = chartDom.clientWidth || 0
const chartHeight = chartDom.clientHeight || 0
if (chartWidth < 360 || chartHeight < 260) {
this.scheduleNineDimensionRadarRender(140)
return
}
const scores = this.nineDimensionScores
const radarScale = this.nineDimensionRadarScale
const dimensionLabels = this.getNineDimensionLabels()
const isCompactChart = chartWidth > 0 && chartWidth < 760
const currentInstance = echarts.getInstanceByDom(chartDom)
if (currentInstance) {
this.nineDimensionRadarChart = currentInstance
} else {
this.destroyNineDimensionRadar()
this.nineDimensionRadarChart = echarts.init(chartDom);
}
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(17, 24, 39, 0.92)',
borderColor: 'rgba(99, 102, 241, 0.35)',
borderWidth: 1,
padding: [10, 14],
textStyle: {
color: '#f8fafc',
fontSize: 13
},
formatter: (params) => {
const lines = dimensionLabels.map((name, index) => `${name}: ${this.formatPortraitDisplay(params.value[index])}`)
return lines.join('<br/>')
}
},
radar: {
indicator: dimensionLabels.map(name => ({ name, max: radarScale.max })),
shape: 'polygon',
splitNumber: radarScale.splitNumber,
radius: isCompactChart ? '76%' : '82%',
center: ['50%', '54%'],
name: {
formatter: name => (isCompactChart ? this.formatRadarLabel(name, 6) : name),
textStyle: {
color: '#364152',
fontSize: isCompactChart ? 13 : 14,
fontWeight: 500
}
},
splitLine: {
lineStyle: {
color: 'rgba(122, 108, 255, 0.34)',
width: 1
}
},
splitArea: {
show: true,
areaStyle: {
color: [
'rgba(107, 114, 128, 0.12)',
'rgba(148, 163, 184, 0.1)',
'rgba(203, 213, 225, 0.08)',
'rgba(226, 232, 240, 0.06)',
'rgba(241, 245, 249, 0.04)'
]
}
},
axisLine: {
lineStyle: {
color: 'rgba(148, 163, 184, 0.4)',
width: 1
}
},
axisNameGap: isCompactChart ? 4 : 8
},
series: [
{
name: '企业九维画像',
type: 'radar',
data: [
{
value: scores,
name: '当前企业',
itemStyle: {
color: '#ffbf2f'
},
areaStyle: {
color: 'rgba(255, 191, 47, 0.68)'
},
lineStyle: {
color: '#f5b700',
width: 2.5
},
symbol: 'circle',
symbolSize: 6
}
]
}
],
animationDuration: 650
};
this.nineDimensionRadarChart.setOption(option);
this.handleRadarResize()
setTimeout(() => {
this.handleRadarResize()
}, 120)
});
},
// 处理雷达图窗口大小变化
handleRadarResize() {
if (this.nineDimensionRadarChart) {
this.$nextTick(() => {
this.nineDimensionRadarChart.resize();
})
}
},
buildNineDimensionRows(metrics) {
const rows = []
for (let index = 0; index < metrics.length; index += 2) {
const firstMetric = metrics[index]
const secondMetric = metrics[index + 1]
rows.push([
{
...firstMetric,
value: this.nineVFinalInfo ? this.nineVFinalInfo[firstMetric.key] : null
},
...(secondMetric
? [{
...secondMetric,
value: this.nineVFinalInfo ? this.nineVFinalInfo[secondMetric.key] : null
}]
: [])
])
}
return rows
},
formatNineDimensionValue(metric) {
const value = metric ? metric.value : null
if (value === null || value === undefined || value === '') {
return '-'
}
const rawValue = `${value}`.trim()
const normalizedValue = /^-?\d+(\.0+)?$/.test(rawValue) ? `${Number(rawValue)}` : rawValue
if (metric && metric.valueMap) {
if (Object.prototype.hasOwnProperty.call(metric.valueMap, normalizedValue)) {
return metric.valueMap[normalizedValue]
}
if (Object.prototype.hasOwnProperty.call(metric.valueMap, rawValue)) {
return metric.valueMap[rawValue]
}
}
if (metric && metric.label && metric.label.includes('是否')) {
if (['1', 1, true, 'true', 'TRUE', '是'].includes(value)) {
return '是'
}
if (['2', 2, false, 'false', 'FALSE', '否'].includes(value)) {
return '否'
}
}
return value
},
// 九维详细信息字段标签映射(部分常用字段)
getFinalInfoLabel(key) {
const labelMap = {
uniscid: '统一社会信用代码',
orgNo: '机构号',
// 维度1 - 合规经营
score11: '是否存在经营异常名录信息',
score12: '是否存在严重违法失信企业名单信息',
score13: '企业环境行为信用等级是否为E或D',
score14: '是否存在税务重大税收违法黑名单信息',
score15: '是否存在拖欠工资黑名单',
score16: '是否存在工商吊销企业信息',
// 维度2 - 风险准入
score21: '是否存在注销企业信息',
score22: '是否存在执行案件信息',
score23: '是否存在查封信息',
score24: '是否存在单位未履行生效裁判信息',
score25: '是否存在企业破产清算信息',
score26: '是否失信被执行人',
score27: '是否为诉讼案件被告',
// 维度5 - 社会贡献度
score51: '前12个月纳税金额',
score52: '纳税等级',
score53: '缴纳社保人数',
score54: '公积金缴纳人数',
score55: '是否为出口退税生产清单企业',
// 维度6 - 稳定经营
score61: '市场主体经营年限',
score62: '认缴出资人数',
score63: '最大股东持股占比',
score64: '近三年法定代表人变更次数',
score65: '近三年股东变更次数',
score69: '法人户籍',
score610: '法人婚姻状况',
// 维度7 - 经营能力
score71: '上年增值税金额',
score72: '今年增值税同比变动',
score73: '上年企业所得税金额',
score74: '今年所得税同比变动',
score75: '缴纳社保人数同比变动',
score77: '当年纳税金额同比变动',
score78: '上年出口退税金额',
// 维度8 - 偿债能力
score81: '房产套数',
score82: '房产面积',
score83: '未抵押房产套数',
score84: '未抵押房产面积',
score85: '已抵押房产被担保债权数额',
score86: '企业车产金额'
};
return labelMap[key] || key;
},
} }
} }
</script> </script>
@@ -2405,4 +3013,210 @@ export default {
width: 320px !important; width: 320px !important;
} }
} }
// 企业九维数据样式
.nine-dimension-data {
padding: 20px 0;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
border-radius: 14px;
.nine-dimension-section {
margin-bottom: 30px;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #202938;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid #edf1f7;
}
.nine-dimension-chart-container {
display: flex;
align-items: stretch;
gap: 30px;
padding: 26px 30px;
justify-content: space-between;
background: linear-gradient(135deg, #ffffff 0%, #fbfcff 100%);
border: 1px solid #e7edf7;
border-radius: 14px;
box-shadow: 0 8px 26px rgba(31, 55, 88, 0.06);
.radar-chart-wrapper {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
padding: 4px 18px 4px 0;
border-right: 1px solid #edf1f7;
.nine-dimension-radar {
width: 100%;
max-width: none;
height: 430px;
margin: 0 auto;
}
}
.nine-dimension-summary {
width: 236px;
flex-shrink: 0;
padding: 18px 20px;
background: #fff;
border: 1px solid #edf1f7;
border-radius: 12px;
box-shadow: 0 12px 28px rgba(34, 65, 120, 0.08);
align-self: center;
.summary-item {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px 0;
border-bottom: 1px solid #edf1f7;
&:last-child {
padding-bottom: 6px;
border-bottom: 0;
}
.summary-label {
display: block;
font-size: 15px;
color: #202938;
font-weight: 600;
}
.summary-content {
display: flex;
align-items: baseline;
justify-content: flex-end;
gap: 8px;
}
.summary-value {
display: block;
font-size: 40px;
line-height: 1;
font-weight: 700;
color: #1f6feb;
letter-spacing: -1px;
&.is-rank {
color: #22c55e;
}
}
.summary-unit {
font-size: 18px;
font-weight: 600;
color: #202938;
}
}
}
}
.nine-dimension-detail-table {
overflow-x: auto;
.detail-table {
width: 100%;
min-width: 980px;
border-collapse: collapse;
table-layout: fixed;
border: 1px solid #ebeef5;
td {
padding: 18px 16px;
border: 1px solid #ebeef5;
font-size: 13px;
line-height: 1.6;
color: #303133;
vertical-align: middle;
word-break: break-word;
}
.category-cell {
width: 184px;
background: #eef3f9;
color: #1f6feb;
font-size: 15px;
font-weight: 700;
text-align: center;
}
.metric-label-cell {
background: #f6f8fb;
font-weight: 600;
}
.metric-value-cell {
width: 120px;
background: #fff;
font-weight: 500;
text-align: center;
}
.is-empty {
background: #fff;
}
}
}
.data-item {
margin-bottom: 12px;
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
.data-label {
display: inline-block;
width: 120px;
font-size: 14px;
color: #606266;
font-weight: 500;
}
.data-value {
font-size: 14px;
color: #303133;
}
}
}
}
@media (max-width: 1400px) {
.nine-dimension-data {
.nine-dimension-section {
.nine-dimension-chart-container {
flex-direction: column;
align-items: stretch;
.radar-chart-wrapper {
padding-right: 0;
border-right: 0;
border-bottom: 1px solid #edf1f7;
padding-bottom: 18px;
margin-bottom: 4px;
.nine-dimension-radar {
max-width: 100%;
height: 390px;
}
}
.nine-dimension-summary {
width: 100%;
}
}
}
}
}
</style> </style>

View File

@@ -2,9 +2,16 @@
<div class="customer-wrap"> <div class="customer-wrap">
<div> <div>
<el-radio-group class="header-radio" v-model="selectedTab" @change="handleTabChange"> <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> <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="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="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> </el-radio-group>
<div class="customerMain"> <div class="customerMain">
<div :class="iscollapsed ? 'customerMain_left_sq' : 'customerMain_left_zk'"> <div :class="iscollapsed ? 'customerMain_left_sq' : 'customerMain_left_zk'">
@@ -135,6 +142,29 @@
<el-dropdown-item @click.native="handleExportAll">导出前1000条<i class="quesiton"></i></el-dropdown-item> <el-dropdown-item @click.native="handleExportAll">导出前1000条<i class="quesiton"></i></el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<template v-if="selectedTab === '2' && canSeeBusinessImport">
<div class="import-action">
<el-upload
ref="businessImportUploadRef"
class="business-import-upload"
action="#"
:show-file-list="false"
:http-request="requestBusinessCustLevelImport"
:before-upload="beforeBusinessCustLevelUpload"
:disabled="businessImportLoading"
>
<el-button
icon="el-icon-upload2"
type="primary"
style="margin-left: 10px"
:loading="businessImportLoading"
>导入</el-button>
</el-upload>
<el-tooltip placement="top" trigger="hover" content="导入更新公司客户视图分层分类数据" width="200">
<span class="import-question"><i class="el-icon-question"></i></span>
</el-tooltip>
</div>
</template>
<upload-tag style="margin-left: 10px" v-if="selectedTab === '0' && userName.indexOf('931') === 0"></upload-tag> <upload-tag style="margin-left: 10px" v-if="selectedTab === '0' && userName.indexOf('931') === 0"></upload-tag>
</div> </div>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="searchColoumns" <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="searchColoumns"
@@ -173,7 +203,16 @@
</div> </div>
</template> </template>
<script> <script>
import { getCustomerList, getCustAddressList, getPerIndcList, getGegionList, getDrawList, getVirtualList } from '@/api/grid/mycustomer.js' import {
getCustomerList,
getCustAddressList,
getPerIndcList,
getGegionList,
getDrawList,
getVirtualList,
importBusinessCustLevelAsync,
getBusinessCustLevelImportStatus
} from '@/api/grid/mycustomer.js'
import GroupCheck from './components/group-check' import GroupCheck from './components/group-check'
import GroupCheckMore from './components/group-check-more' import GroupCheckMore from './components/group-check-more'
import UploadTag from './components/uploadTag.vue' import UploadTag from './components/uploadTag.vue'
@@ -281,6 +320,8 @@ export default {
showMoreIndc: true, showMoreIndc: true,
contiKeys: [], contiKeys: [],
iscollapsed: false, iscollapsed: false,
businessImportLoading: false,
businessImportTimer: null,
} }
}, },
watch: { watch: {
@@ -317,12 +358,18 @@ export default {
}, },
computed: { computed: {
...mapGetters(['roles', 'userName']), ...mapGetters(['roles', 'userName']),
deptId() {
return this.$store.state.user.deptId
},
filtereDate() { filtereDate() {
if (this.searchQuery) { if (this.searchQuery) {
return this.tableData.filter((item) => item.companyName.includes(this.searchQuery) || item.legalName.includes(this.searchQuery)) return this.tableData.filter((item) => item.companyName.includes(this.searchQuery) || item.legalName.includes(this.searchQuery))
} }
return this.tableData return this.tableData
}, },
isHeadAdmin() {
return this.roles.includes('headAdmin')
},
isPublic() { isPublic() {
return this.roles.includes('headPublic') return this.roles.includes('headPublic')
}, },
@@ -332,6 +379,12 @@ export default {
isOps() { isOps() {
return this.roles.includes('headOps') return this.roles.includes('headOps')
}, },
is825() {
return String(this.deptId || '').substring(0, 3) === '825'
},
canSeeBusinessImport() {
return this.is825 && (this.isHeadAdmin || this.isPublic || this.isPrivate || this.isOps)
},
// 客户经理 // 客户经理
isCommonManager() { isCommonManager() {
return this.roles.includes('commonManager') return this.roles.includes('commonManager')
@@ -770,12 +823,75 @@ export default {
) )
} }
}, },
beforeBusinessCustLevelUpload(file) {
const fileName = file.name ? file.name.toLowerCase() : ''
const isExcel = fileName.endsWith('.xls') || fileName.endsWith('.xlsx')
if (!isExcel) {
this.$message.warning('请上传Excel文件')
return false
}
return true
},
async requestBusinessCustLevelImport(fileOut) {
const { file } = fileOut
const formData = new FormData()
formData.append('file', file)
this.businessImportLoading = true
this.clearBusinessImportTimer()
try {
const res = await importBusinessCustLevelAsync(formData)
this.$message.success(res.msg || '导入任务已提交,后台正在处理')
if (res.data) {
this.pollBusinessCustLevelImportStatus(res.data)
}
} finally {
this.businessImportLoading = false
if (this.$refs.businessImportUploadRef) {
this.$refs.businessImportUploadRef.clearFiles()
}
}
},
pollBusinessCustLevelImportStatus(taskId) {
this.clearBusinessImportTimer()
this.businessImportTimer = setInterval(async () => {
try {
const res = await getBusinessCustLevelImportStatus(taskId)
const task = res.data
if (!task || task.status === '0') {
return
}
this.clearBusinessImportTimer()
if (task.status === '1') {
window.dispatchEvent(new Event('notice-center-refresh'))
this.$message.success(task.message || '导入成功')
this.getList()
} else if (task.status === '2') {
window.dispatchEvent(new Event('notice-center-refresh'))
this.$message.error(task.message || '导入失败')
}
} catch (error) {
this.clearBusinessImportTimer()
}
}, 3000)
},
clearBusinessImportTimer() {
if (this.businessImportTimer) {
clearInterval(this.businessImportTimer)
this.businessImportTimer = null
}
}
}, },
created() { created() {
const { query } = this.$route; const { query } = this.$route;
if (query.backUrl) { if (query.backUrl) {
this.selectedTab = query.selectedTab this.selectedTab = query.selectedTab
this.queryParams.custPattern = 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() { mounted() {
@@ -784,6 +900,9 @@ export default {
this.initGetSecondRegionList() this.initGetSecondRegionList()
this.initgetDrawList() this.initgetDrawList()
this.initGetVirtualList() this.initGetVirtualList()
},
beforeDestroy() {
this.clearBusinessImportTimer()
} }
} }
</script> </script>
@@ -1046,6 +1165,26 @@ export default {
} }
} }
.import-action {
display: inline-block;
position: relative;
}
.business-import-upload {
display: inline-block;
}
.import-question {
position: absolute;
top: -2px;
right: -14px;
color: #b9b9b9;
font-size: 13px;
cursor: pointer;
line-height: 1;
z-index: 1;
}
.iframe-wrap { .iframe-wrap {
width: 100%; width: 100%;
height: 500px; height: 500px;

View File

@@ -1,5 +1,22 @@
const commonCol = function (isCom, type) { // 当headId=875时需要隐藏的字段列表
return [ 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', prop: 'regionTopGridName',
label: '总行行政网格名称', label: '总行行政网格名称',
@@ -101,9 +118,16 @@ const commonCol = function (isCom, type) {
showOverflowTooltip: true, 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': { '2': {
placeholder: '搜索企业名称/法人名字', placeholder: '搜索企业名称/法人名字',
custPattern: '2', custPattern: '2',
@@ -138,7 +162,7 @@ export const placeholderMap = (type) => ({
type: 'myCustLevel', type: 'myCustLevel',
// desc: '建档输入/取新华社数据', // desc: '建档输入/取新华社数据',
}, },
...commonCol('2', type), ...commonCol('2', type, headId),
{ {
prop: 'lpName', prop: 'lpName',
label: '法人姓名', label: '法人姓名',
@@ -393,7 +417,7 @@ export const placeholderMap = (type) => ({
type: 'myCustIdsn', type: 'myCustIdsn',
// desc: '建档输入/取新华社数据', // desc: '建档输入/取新华社数据',
}, },
...commonCol('1', type), ...commonCol('1', type, headId),
{ {
prop: 'lpName', prop: 'lpName',
label: '经营者姓名', label: '经营者姓名',
@@ -634,7 +658,7 @@ export const placeholderMap = (type) => ({
type: 'myCustLevel', type: 'myCustLevel',
// desc: '取大信贷数据', // desc: '取大信贷数据',
}, },
...commonCol('0', type), ...commonCol('0', type, headId),
{ {
prop: 'custPhone', prop: 'custPhone',
label: '联系方式', label: '联系方式',

View File

@@ -2,9 +2,16 @@
<div class="customer-wrap"> <div class="customer-wrap">
<div> <div>
<el-radio-group class="header-radio" v-model="selectedTab" @change="handleTabChange"> <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" <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="0" :disabled="isPublic" :class="{ 'btn-disabled': isPublic }">个人</el-radio-button>
<el-radio-button label="1" :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> </el-radio-group>
<div class="customerMain"> <div class="customerMain">
<div :class="iscollapsed ? 'customerMain_left_sq' : 'customerMain_left_zk'"> <div :class="iscollapsed ? 'customerMain_left_sq' : 'customerMain_left_zk'">
@@ -283,8 +290,9 @@ export default {
const type = this.roles.includes('headPublic') ? 'isPublic' : const type = this.roles.includes('headPublic') ? 'isPublic' :
this.roles.includes('headPrivate') ? 'isPrivate' : this.roles.includes('headPrivate') ? 'isPrivate' :
this.roles.includes('headOps') ? 'isOps' : "" this.roles.includes('headOps') ? 'isOps' : ""
this.tableColoumns = placeholderMap(type)[val].tableColoumns const headId = this.deptId
this.placeholder = placeholderMap(type)[val].placeholder this.tableColoumns = placeholderMap(type, headId)[val].tableColoumns
this.placeholder = placeholderMap(type, headId)[val].placeholder
this.queryParams.custPattern = val this.queryParams.custPattern = val
this.searchColoumns = this.getSearchColoumns() this.searchColoumns = this.getSearchColoumns()
} }
@@ -310,6 +318,9 @@ export default {
}, },
computed: { computed: {
...mapGetters(['roles']), ...mapGetters(['roles']),
deptId() {
return this.$store.state.user.deptId
},
filtereDate() { filtereDate() {
if (this.searchQuery) { if (this.searchQuery) {
return this.tableData.filter((item) => item.companyName.includes(this.searchQuery) || item.legalName.includes(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) { if (query.backUrl) {
this.selectedTab = query.selectedTab this.selectedTab = query.selectedTab
this.queryParams.custPattern = 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() { 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,447 @@
<template>
<div class="customer-wrap">
<el-radio-group v-model="activeTab" class="group-tab-radio" @input="handleTabChange">
<el-radio-button label="mine">我创建的</el-radio-button>
<el-radio-button label="sharedToMe">下发给我的</el-radio-button>
</el-radio-group>
<div v-show="showSearch" class="search-area">
<el-form
ref="queryForm"
:model="queryParams"
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">
<template v-if="isMineTab">
<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>
</template>
</div>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</section>
<div class="main_table">
<el-table
v-loading="loading"
:data="groupList"
style="width: 100%"
max-height="625"
@selection-change="handleSelectionChange"
>
<el-table-column
v-if="isMineTab"
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>
<template v-if="isMineTab">
<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>
</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,
activeTab: 'mine',
ids: [],
single: true,
multiple: true,
total: 0,
groupList: [],
dialogVisible: false,
isEdit: false,
form: {},
refreshTimer: null,
queryParams: {
pageNum: 1,
pageSize: 10,
groupName: null,
groupMode: null,
createMode: null,
groupStatus: null,
viewType: 'mine'
}
}
},
computed: {
isMineTab() {
return this.activeTab === 'mine'
}
},
created() {
this.getList()
},
beforeDestroy() {
this.clearRefreshTimer()
},
methods: {
clearRefreshTimer() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer)
this.refreshTimer = null
}
},
refreshList(delay = 0) {
this.clearRefreshTimer()
if (delay > 0) {
this.refreshTimer = setTimeout(() => {
this.getList()
this.refreshTimer = null
}, delay)
return
}
this.getList()
},
getList() {
this.loading = true
this.queryParams.viewType = this.activeTab
listCustGroup(this.queryParams).then(response => {
this.groupList = response.rows
this.total = response.total
this.loading = false
}).catch(() => {
this.loading = false
})
},
handleTabChange() {
this.ids = []
this.single = true
this.multiple = true
this.queryParams.pageNum = 1
this.queryParams.viewType = this.activeTab
this.getList()
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.queryParams.pageNum = 1
this.queryParams.pageSize = 10
this.queryParams.viewType = this.activeTab
this.handleQuery()
},
handleSelectionChange(selection) {
if (!this.isMineTab) {
this.ids = []
this.single = true
this.multiple = true
return
}
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.refreshList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
handleSubmit() {
this.dialogVisible = false
this.queryParams.pageNum = 1
this.refreshList(800)
},
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;
.group-tab-radio {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #ebebeb;
margin-bottom: 8px;
.el-radio-button {
flex: 1;
::v-deep .el-radio-button__inner {
width: 100%;
border: none;
font-weight: 400;
letter-spacing: 0.44px;
line-height: 25px;
font-size: 16px;
color: #666666;
padding: 11px 0 12px 0;
border-radius: 0;
box-shadow: none;
}
::v-deep .el-radio-button__orig-radio:checked + .el-radio-button__inner {
background-color: #4886f8;
font-weight: 400;
color: #ffffff;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
&:nth-child(2) {
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
height: 21px;
width: 1px;
background: #ebebeb;
z-index: 1;
}
&::after {
right: 1px;
}
}
&.is-active {
&::before,
&::after {
content: none;
}
}
}
}
.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;
min-height: 32px;
}
.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 style="font-size: 14px;">较上月变动</span>
<span v-if="String(item.inc).includes('-')" style=" font-size: 14px;color: #00B453">{{ changeData(item.inc) <span v-if="String(item.inc).includes('-')" style=" font-size: 14px;color: #00B453">{{ changeData(item.inc)
}}<i class="el-icon-caret-bottom"></i></span> }}<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 <span v-else style=" font-size: 14px;color: #EF3F35">{{ changeData(item.inc) }}<i
class="el-icon-caret-top"></i></span> class="el-icon-caret-top"></i></span>
</div> </div>
@@ -43,6 +44,11 @@
@click="goToCustManager(item.itemNm, 'fall')"> @click="goToCustManager(item.itemNm, 'fall')">
{{ item.curAmt }}<i class="el-icon-caret-bottom"></i> {{ item.curAmt }}<i class="el-icon-caret-bottom"></i>
</span> </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 <span v-else
style="font-size: 14px; color: #EF3F35; cursor: pointer;" style="font-size: 14px; color: #EF3F35; cursor: pointer;"
@click="goToCustManager(item.itemNm, 'rise')"> @click="goToCustManager(item.itemNm, 'rise')">
@@ -60,7 +66,7 @@
<el-radio-button label="4">预警任务</el-radio-button> <el-radio-button label="4">预警任务</el-radio-button>
<el-radio-button label="5">二次走访提醒</el-radio-button> <el-radio-button label="5">二次走访提醒</el-radio-button>
<el-radio-button label="6">走访资源提醒</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> </el-radio-group>
</div> </div>
<el-table v-if="selectedTab==='3'" key="3" :data="tableData" :loading="loading" style="width: 100%;margin-top: 20px;"> <el-table v-if="selectedTab==='3'" key="3" :data="tableData" :loading="loading" style="width: 100%;margin-top: 20px;">
@@ -152,7 +158,7 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </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="营销任务" 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="客户姓名" prop="custName" min-width="100" show-overflow-tooltip />
<el-table-column label="客户号" width="200" prop="custId" min-width="140" 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="结束时间" width="200" prop="endTime" min-width="100" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip /> <el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
</el-table> </el-table>
<el-pagination @size-change="handleAgentSizeChange" @current-change="handleAgentCurrentChange" <el-pagination @current-change="handleAgentCurrentChange"
class="warnPagination" :page-sizes="[5, 10, 20, 30]" :page-size="agentPageSize" class="warnPagination" :page-size="5"
layout="->,total,sizes,prev,pager,next" :total="agentTotal" :current-page="agentPageNum"></el-pagination> layout="->,total,prev,pager,next" :total="agentTotal" :current-page="agentPageNum"></el-pagination>
</div> </div>
</div> </div>
<div class="page-vr"> <div class="page-vr">
@@ -210,7 +216,7 @@
</span> </span>
</p> </p>
<ul class="operate-cnt"> <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)"> @mouseleave="onMouseOver(ind, false)">
<div class="noSetting" v-if="it.setShow"> <div class="noSetting" v-if="it.setShow">
<svg-icon :icon-class="getCurrentImg(ind)" :class="isSetting ? 'svg-icon-imgSetting' : 'svg-icon-img'" <svg-icon :icon-class="getCurrentImg(ind)" :class="isSetting ? 'svg-icon-imgSetting' : 'svg-icon-img'"
@@ -230,7 +236,7 @@
</li> </li>
</ul> </ul>
</div> </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> <p class="page-title page-btm">预警信息</p>
<ul class="common-ul"> <ul class="common-ul">
<li v-for="(it, ind) in warnArr" :key="ind" style="cursor: pointer;" @click="handleWarn(it)" class="yjxxLi"> <li v-for="(it, ind) in warnArr" :key="ind" style="cursor: pointer;" @click="handleWarn(it)" class="yjxxLi">
@@ -857,6 +863,23 @@ export default {
}, },
showposition() { showposition() {
return this.userName.slice(0, 3) === '875' return this.userName.slice(0, 3) === '875'
},
// headId为875时隐藏预警信息和营销任务
shouldHideFor875() {
return this.userName && (this.userName + '').substring(0, 3) === '875'
},
// headId为875时便捷操作只显示快速入门
filteredOptArr() {
return this.optArr
}
},
watch: {
selectedTab(newVal, oldVal) {
// 切换tab时重置分页为第1页
if (newVal !== oldVal) {
this.pageNum = 1
this.agentPageNum = 1
}
} }
}, },
mounted() { mounted() {
@@ -1135,8 +1158,7 @@ export default {
this.getData() this.getData()
}, },
handleAgentCurrentChange(val) { handleAgentCurrentChange(val) {
// this.agentPageNum = val this.agentPageNum = val
// this.initBranchList()
this.pageNum = val this.pageNum = val
this.getData() this.getData()
}, },
@@ -1398,10 +1420,9 @@ export default {
handleSaveSetting() { handleSaveSetting() {
let arr = this.optArr.map(item => { let arr = this.optArr.map(item => {
return item.name return item.name
}) }).filter(name => name && name.trim() !== '') // 过滤掉空值
console.log(arr, 'arrarrarr') console.log(arr, 'arrarrarr')
updateQuickSelect(arr).then(res => { updateQuickSelect(arr).then(res => {
if (res.code == 200) { if (res.code == 200) {
Message.success(res.data) Message.success(res.data)
this.handleSetting() this.handleSetting()
@@ -1565,6 +1586,8 @@ p {
.page-vr { .page-vr {
width: 24%; width: 24%;
margin-left: 1%; margin-left: 1%;
display: flex;
flex-direction: column;
} }
.page-title { .page-title {
@@ -1602,7 +1625,11 @@ p {
} }
.page-vr-middle { .page-vr-middle {
margin: 22px 0; margin-bottom: 22px;
}
.page-vr-top {
margin-bottom: 22px;
} }
.page-vl-top { .page-vl-top {
@@ -1873,7 +1900,7 @@ p {
.page-vr-btm { .page-vr-btm {
height: 350px; height: 350px;
overflow-y: scroll; overflow-y: scroll;
margin-bottom: 30px; margin-bottom: 22px;
} }
::v-deep .el-badge__content { ::v-deep .el-badge__content {
@@ -1943,8 +1970,10 @@ p {
} }
.url-box { .url-box {
max-height: 210px; flex: 1;
overflow-y: scroll; overflow-y: scroll;
display: flex;
flex-direction: column;
} }
.yjxxLi { .yjxxLi {

View File

@@ -83,6 +83,11 @@
<dict-tag :options="dict.type.sys_notice_type" :value="scope.row.noticeType"/> <dict-tag :options="dict.type.sys_notice_type" :value="scope.row.noticeType"/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="可见总行" align="center" min-width="180" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span>{{ formatHeadNames(scope.row.deptIds) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100"> <el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<dict-tag :options="dict.type.sys_notice_status" :value="scope.row.status"/> <dict-tag :options="dict.type.sys_notice_status" :value="scope.row.status"/>
@@ -154,6 +159,25 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24">
<el-form-item label="可见总行" prop="deptIds">
<el-select
v-model="headList"
placeholder="请选择可见总行,不选则全员可见"
multiple
filterable
clearable
style="width: 100%;"
>
<el-option
v-for="item in headOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="内容"> <el-form-item label="内容">
<editor v-model="form.noticeContent" :min-height="192"/> <editor v-model="form.noticeContent" :min-height="192"/>
@@ -170,7 +194,7 @@
</template> </template>
<script> <script>
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice"; import { listNotice, getNotice, delNotice, addNotice, updateNotice, getHeadList } from "@/api/system/notice";
export default { export default {
name: "Notice", name: "Notice",
@@ -191,6 +215,10 @@ export default {
total: 0, total: 0,
// 公告表格数据 // 公告表格数据
noticeList: [], noticeList: [],
// 总行选项
headOptions: [],
// 已选总行
headList: [],
// 弹出层标题 // 弹出层标题
title: "", title: "",
// 是否显示弹出层 // 是否显示弹出层
@@ -218,6 +246,7 @@ export default {
}, },
created() { created() {
this.getList(); this.getList();
this.getHeadOptions();
}, },
methods: { methods: {
/** 查询公告列表 */ /** 查询公告列表 */
@@ -229,6 +258,16 @@ export default {
this.loading = false; this.loading = false;
}); });
}, },
getHeadOptions() {
getHeadList().then(response => {
if (response.code === 200 && Array.isArray(response.data)) {
this.headOptions = response.data.map(item => ({
label: item.deptName,
value: String(item.deptId)
}));
}
});
},
// 取消按钮 // 取消按钮
cancel() { cancel() {
this.open = false; this.open = false;
@@ -241,8 +280,10 @@ export default {
noticeTitle: undefined, noticeTitle: undefined,
noticeType: undefined, noticeType: undefined,
noticeContent: undefined, noticeContent: undefined,
deptIds: undefined,
status: "0" status: "0"
}; };
this.headList = [];
this.resetForm("form"); this.resetForm("form");
}, },
/** 搜索按钮操作 */ /** 搜索按钮操作 */
@@ -273,6 +314,7 @@ export default {
const noticeId = row.noticeId || this.ids const noticeId = row.noticeId || this.ids
getNotice(noticeId).then(response => { getNotice(noticeId).then(response => {
this.form = response.data; this.form = response.data;
this.headList = response.data.deptIds ? response.data.deptIds.split(',').filter(Boolean) : [];
this.open = true; this.open = true;
this.title = "修改公告"; this.title = "修改公告";
}); });
@@ -281,6 +323,7 @@ export default {
submitForm: function() { submitForm: function() {
this.$refs["form"].validate(valid => { this.$refs["form"].validate(valid => {
if (valid) { if (valid) {
this.form.deptIds = Array.isArray(this.headList) && this.headList.length > 0 ? this.headList.join(',') : '';
if (this.form.noticeId != undefined) { if (this.form.noticeId != undefined) {
updateNotice(this.form).then(response => { updateNotice(this.form).then(response => {
this.$modal.msgSuccess("修改成功"); this.$modal.msgSuccess("修改成功");
@@ -306,6 +349,20 @@ export default {
this.getList(); this.getList();
this.$modal.msgSuccess("删除成功"); this.$modal.msgSuccess("删除成功");
}).catch(() => {}); }).catch(() => {});
},
formatHeadNames(deptIds) {
if (!deptIds) {
return '全员可见';
}
const selectedIds = String(deptIds).split(',').filter(Boolean);
if (!selectedIds.length) {
return '全员可见';
}
const nameMap = this.headOptions.reduce((map, item) => {
map[item.value] = item.label;
return map;
}, {});
return selectedIds.map(id => nameMap[id] || id).join('、');
} }
} }
}; };

View File

@@ -6,24 +6,46 @@
v-model="selectedTab" v-model="selectedTab"
@input="handleChange" @input="handleChange"
> >
<el-radio-button <template v-if="String(deptId).substring(0, 3) === '875'">
label="2" <el-radio-button
:disabled="isPrivate" label="0"
:class="{ 'btn-disabled': isPrivate }" :disabled="isPublic"
>企业</el-radio-button :class="{ 'btn-disabled': isPublic }"
> >个人</el-radio-button
<el-radio-button >
label="0" <el-radio-button
:disabled="isPublic" label="1"
:class="{ 'btn-disabled': isPublic }" :disabled="isPublic"
>个人</el-radio-button :class="{ 'btn-disabled': isPublic }"
> >商户</el-radio-button
<el-radio-button >
label="1" <el-radio-button
:disabled="isPublic" label="2"
:class="{ 'btn-disabled': isPublic }" :disabled="isPrivate"
>商户</el-radio-button :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> </el-radio-group>
<div class="searchForm"> <div class="searchForm">
<el-form <el-form
@@ -41,6 +63,15 @@
@clear="handleSearch" @clear="handleSearch"
/> />
</el-form-item> </el-form-item>
<el-form-item label="柜员号" class="staff-id-filter">
<el-input
v-model="searchForm.visId"
placeholder="请输入"
@blur="handleSearch"
clearable
@clear="handleSearch"
/>
</el-form-item>
<el-form-item label="走访时间"> <el-form-item label="走访时间">
<el-date-picker <el-date-picker
v-model="searchForm.visTime" v-model="searchForm.visTime"
@@ -340,6 +371,23 @@
width="150px" width="150px"
v-if="columns[16].visible" v-if="columns[16].visible"
></el-table-column> ></el-table-column>
<el-table-column align="left" prop="interAddr" label="实地拜访地址" show-overflow-tooltip width="180px" v-if="columns[17].visible"></el-table-column>
<el-table-column align="left" prop="colStafName" label="协同走访客户经理" show-overflow-tooltip width="160px" v-if="columns[18].visible"></el-table-column>
<el-table-column align="left" prop="laterNote" label="事后备注" show-overflow-tooltip width="180px" v-if="columns[19].visible"></el-table-column>
<el-table-column align="left" prop="intentionProductValue" label="走访反馈" show-overflow-tooltip width="160px" v-if="columns[20].visible"></el-table-column>
<el-table-column align="left" prop="feedbackStatus" label="反馈状态" show-overflow-tooltip width="120px" v-if="columns[21].visible"></el-table-column>
<el-table-column align="left" prop="sourceOfInterview" label="走访来源" show-overflow-tooltip width="140px" v-if="columns[22].visible"></el-table-column>
<el-table-column align="left" prop="filename" label="批量导入文件名" show-overflow-tooltip width="180px" v-if="columns[23].visible"></el-table-column>
<el-table-column align="left" prop="outCallStatus" label="外呼状态" show-overflow-tooltip width="120px" v-if="columns[24].visible"></el-table-column>
<el-table-column align="left" prop="outCallIntention" label="客户意愿" show-overflow-tooltip width="140px" v-if="columns[25].visible"></el-table-column>
<el-table-column align="left" prop="source" label="走访渠道" show-overflow-tooltip width="120px" v-if="columns[26].visible"></el-table-column>
<el-table-column align="left" prop="analysisValue" label="nlp模型提取" show-overflow-tooltip width="140px" v-if="columns[27].visible"></el-table-column>
<el-table-column align="left" prop="facility" label="预授信额度" show-overflow-tooltip width="140px" v-if="columns[28].visible"></el-table-column>
<el-table-column align="center" label="操作" fixed="right" width="100">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="handleEditFeedback(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
@size-change="handleSizeChange" @size-change="handleSizeChange"
@@ -350,24 +398,90 @@
:total="total" :total="total"
:current-page="pageNum" :current-page="pageNum"
></el-pagination> ></el-pagination>
<el-dialog
title="编辑走访反馈"
:visible.sync="feedbackDialogVisible"
width="960px"
custom-class="feedback-dialog"
append-to-body
@close="resetFeedbackForm"
>
<el-form ref="feedbackFormRef" :model="feedbackForm" label-width="100px" class="feedback-form">
<el-form-item label="走访渠道" required>
<el-radio-group v-model="feedbackForm.source">
<el-radio v-for="item in sourceOptions" :key="item" :label="item">{{ item }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="客户意愿" required>
<div class="feedback-groups">
<section v-for="type in feedbackTypeOptions" :key="type" class="feedback-group">
<div class="feedback-group__title">{{ type }}</div>
<el-checkbox-group v-model="feedbackForm.feedbackSelections[type]">
<el-checkbox
v-for="product in feedbackProductOptions"
:key="`${type}-${product}`"
:label="product"
>
{{ product }}
</el-checkbox>
</el-checkbox-group>
</section>
</div>
</el-form-item>
<el-form-item label="预览结果">
<div class="feedback-preview">{{ feedbackPreview || "-" }}</div>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="feedbackDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="feedbackSubmitting" @click="handleSubmitFeedback">确定</el-button>
</span>
</el-dialog>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapGetters } from "vuex";
import { getPADVisitRecord } from "@/api/task/PADvisitRecord.js"; import { getPADVisitRecord, updatePADVisitFeedback } from "@/api/task/PADvisitRecord.js";
import { Message } from "element-ui";
const SOURCE_OPTIONS = ["企业微信", "PAD"];
const FEEDBACK_TYPE_OPTIONS = ["拒绝", "考虑", "意愿", "其他", "愿意", "现场办理"];
const FEEDBACK_PRODUCT_OPTIONS = [
"丰收互联",
"贷款",
"电子社保卡签约",
"基金",
"贵金属",
"信用卡",
"医保电子凭证",
"社保卡",
"理财签约"
];
function createEmptyFeedbackSelections() {
return FEEDBACK_TYPE_OPTIONS.reduce((result, item) => {
result[item] = [];
return result;
}, {});
}
export default { export default {
data() { data() {
return { return {
selectedTab: "0", selectedTab: "0",
tableData: [], tableData: [],
Loading: false, Loading: false,
feedbackDialogVisible: false,
feedbackSubmitting: false,
total: 0, total: 0,
pageSize: 10, pageSize: 10,
pageNum: 1, pageNum: 1,
sourceOptions: SOURCE_OPTIONS,
feedbackTypeOptions: FEEDBACK_TYPE_OPTIONS,
feedbackProductOptions: FEEDBACK_PRODUCT_OPTIONS,
searchForm: { searchForm: {
visName: "", visName: "",
visId: "",
visTime: "", visTime: "",
custIdc: "", custIdc: "",
socialCreditCode: '', socialCreditCode: '',
@@ -393,16 +507,36 @@ export default {
{ key: 13, label: "签到坐标", visible: true }, { key: 13, label: "签到坐标", visible: true },
{ key: 14, label: "签退坐标", visible: true }, { key: 14, label: "签退坐标", visible: true },
{ key: 15, label: "是否为有效客户", visible: true }, { key: 15, label: "是否为有效客户", visible: true },
{ key: 16, label: "走访备注", visible: true } { key: 16, label: "走访备注", visible: true },
{ key: 17, label: "实地拜访地址", visible: true },
{ key: 18, label: "协同走访客户经理", visible: true },
{ key: 19, label: "事后备注", visible: true },
{ key: 20, label: "走访反馈", visible: true },
{ key: 21, label: "反馈状态", visible: true },
{ key: 22, label: "走访来源", visible: true },
{ key: 23, label: "批量导入文件名", visible: true },
{ key: 24, label: "外呼状态", visible: true },
{ key: 25, label: "客户意愿", visible: true },
{ key: 26, label: "走访渠道", visible: true },
{ key: 27, label: "nlp模型提取", visible: true },
{ key: 28, label: "预授信额度", visible: true }
], ],
columns875: [ columns875: [
{ key: 17, label: "异常走访标签", visible: true }, { key: 29, label: "异常走访标签", visible: true },
{ key: 18, label: "异常走访信息", visible: true }, { key: 30, label: "异常走访信息", visible: true },
] ],
feedbackForm: {
id: null,
source: "",
feedbackSelections: createEmptyFeedbackSelections()
}
}; };
}, },
computed: { computed: {
...mapGetters(["roles", "userName"]), ...mapGetters(["roles", "userName"]),
deptId() {
return this.$store.state.user.deptId
},
//总行 //总行
isHeadAdmin() { isHeadAdmin() {
return this.roles.includes("headAdmin"); return this.roles.includes("headAdmin");
@@ -434,6 +568,9 @@ export default {
// 海宁 // 海宁
is875() { is875() {
return this.userName.slice(0, 3) === '875' return this.userName.slice(0, 3) === '875'
},
feedbackPreview() {
return this.buildFeedbackValue(this.feedbackForm.feedbackSelections)
} }
}, },
created() { created() {
@@ -441,17 +578,93 @@ export default {
if (selectedTab) { if (selectedTab) {
this.selectedTab = selectedTab; this.selectedTab = selectedTab;
} else { } else {
// 默认选中第一个tab
const deptId = this.$store.state.user.deptId
const deptIdStr = deptId ? String(deptId) : ''
const defaultTab = deptIdStr.startsWith('875') ? '0' : '2'
if (this.isPublic) { if (this.isPublic) {
this.selectedTab = '2' this.selectedTab = '2'
} else if (this.isPrivate) { } else if (this.isPrivate) {
this.selectedTab = '0' this.selectedTab = defaultTab
} else { } else {
this.selectedTab = '2' this.selectedTab = defaultTab
} }
} }
this.initVisitingTaskList(); this.initVisitingTaskList();
}, },
methods: { methods: {
resetFeedbackForm() {
this.feedbackSubmitting = false;
this.feedbackForm = {
id: null,
source: "",
feedbackSelections: createEmptyFeedbackSelections()
};
},
parseFeedbackValue(value) {
const feedbackSelections = createEmptyFeedbackSelections();
if (!value) {
return feedbackSelections;
}
value.split(";").forEach((segment) => {
const [type, products] = segment.split(":");
if (!type || !feedbackSelections[type]) {
return;
}
feedbackSelections[type] = (products || "")
.split(",")
.map(item => item && item.trim())
.filter(Boolean);
});
return feedbackSelections;
},
buildFeedbackItems(feedbackSelections) {
return this.feedbackTypeOptions
.map((type) => ({
feedbackType: type,
products: (feedbackSelections[type] || []).filter(Boolean)
}))
.filter(item => item.products.length > 0);
},
buildFeedbackValue(feedbackSelections) {
return this.buildFeedbackItems(feedbackSelections)
.map(item => `${item.feedbackType}:${item.products.join(",")}`)
.join(";");
},
handleEditFeedback(row) {
this.feedbackForm = {
id: row.id,
source: row.source || "",
feedbackSelections: this.parseFeedbackValue(row.intentionProductValue)
};
this.feedbackDialogVisible = true;
},
handleSubmitFeedback() {
const feedbackItems = this.buildFeedbackItems(this.feedbackForm.feedbackSelections);
if (!this.feedbackForm.source) {
this.$message.warning("请选择走访渠道");
return;
}
if (!feedbackItems.length) {
this.$message.warning("请至少选择一组客户意愿和营销产品");
return;
}
const payload = {
id: this.feedbackForm.id,
source: this.feedbackForm.source || null,
feedbackItems
};
this.feedbackSubmitting = true;
updatePADVisitFeedback(payload).then(() => {
this.$message.success("保存成功");
this.feedbackDialogVisible = false;
this.resetFeedbackForm();
this.initVisitingTaskList();
}).finally(() => {
this.feedbackSubmitting = false;
});
},
handleChange(val) { handleChange(val) {
this.pageSize = 10; this.pageSize = 10;
this.pageNum = 1; this.pageNum = 1;
@@ -462,6 +675,7 @@ export default {
custType: this.selectedTab, custType: this.selectedTab,
visTime: this.searchForm.visTime, visTime: this.searchForm.visTime,
visName: this.searchForm.visName, visName: this.searchForm.visName,
visId: this.searchForm.visId,
custIdc: this.searchForm.custIdc, custIdc: this.searchForm.custIdc,
socialCreditCode: this.searchForm.socialCreditCode, socialCreditCode: this.searchForm.socialCreditCode,
abnormalVisitTag: this.searchForm.abnormalVisitTag, abnormalVisitTag: this.searchForm.abnormalVisitTag,
@@ -478,7 +692,16 @@ export default {
}); });
}, },
handleRefersh() { handleRefersh() {
this.searchForm = {}; this.searchForm = {
visName: "",
visId: "",
visTime: "",
custIdc: "",
socialCreditCode: '',
abnormalVisitTag: '',
marketingWay: '',
interRes: ''
};
this.initVisitingTaskList(); this.initVisitingTaskList();
}, },
handleSizeChange(newSize) { handleSizeChange(newSize) {
@@ -493,7 +716,16 @@ export default {
this.initVisitingTaskList(); this.initVisitingTaskList();
}, },
resetFilters() { resetFilters() {
this.searchForm = {}; this.searchForm = {
visName: "",
visId: "",
visTime: "",
custIdc: "",
socialCreditCode: '',
abnormalVisitTag: '',
marketingWay: '',
interRes: ''
};
this.initVisitingTaskList(); this.initVisitingTaskList();
}, },
}, },
@@ -535,67 +767,6 @@ export default {
line-height: 30px; line-height: 30px;
} }
.header-radio {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #ebebeb;
.btn-disabled {
::v-deep .el-radio-button__inner {
background-color: #e7e7e7;
}
}
.el-radio-button {
flex: 1;
::v-deep .el-radio-button__inner {
width: 100%;
border: none;
font-weight: 400;
letter-spacing: 0.44px;
line-height: 25px;
font-size: 16px;
color: #666666;
padding: 11px 0 12px 0;
}
::v-deep .el-radio-button__orig-radio:checked + .el-radio-button__inner {
background-color: #4886f8;
font-weight: 400;
color: #ffffff;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
&:nth-child(2) {
&::before,
&::after {
content: "";
position: absolute;
top: 50%;
transform: translateY(-50%);
height: 21px;
width: 1px;
background: #ebebeb;
z-index: 1;
}
&::after {
right: 1px;
}
}
&.is-active {
&::before,
&::after {
content: none;
}
}
}
}
.btn-disabled { .btn-disabled {
::v-deep .el-radio-button__inner { ::v-deep .el-radio-button__inner {
background-color: #e7e7e7; background-color: #e7e7e7;
@@ -617,7 +788,6 @@ export default {
.el-radio-button { .el-radio-button {
flex: 1; flex: 1;
border: 1px solid #ccc;
::v-deep .el-radio-button__inner { ::v-deep .el-radio-button__inner {
width: 100%; width: 100%;
@@ -695,6 +865,11 @@ export default {
.searchForm { .searchForm {
margin-top: 20px; margin-top: 20px;
.staff-id-filter {
clear: left;
margin-left: 0 !important;
}
} }
.operate-cnt { .operate-cnt {
@@ -726,6 +901,67 @@ export default {
} }
} }
.feedback-form {
::v-deep .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label::before {
color: #f56c6c;
margin-right: 4px;
}
.feedback-groups {
display: flex;
flex-direction: column;
gap: 12px;
}
.feedback-group {
padding: 10px 12px;
border: 1px solid #ebeef5;
border-radius: 8px;
background: #fafbfc;
}
.feedback-group__title {
margin-bottom: 10px;
color: #303133;
font-weight: 600;
}
::v-deep .el-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 6px 8px;
}
::v-deep .el-checkbox {
margin-right: 0;
min-width: auto;
white-space: nowrap;
}
.feedback-preview {
min-height: 20px;
color: #606266;
word-break: break-all;
}
}
::v-deep .feedback-dialog {
border-radius: 18px;
overflow: hidden;
.el-dialog__header {
padding: 20px 24px 16px;
}
.el-dialog__body {
padding: 12px 24px 20px;
}
.el-dialog__footer {
padding: 10px 24px 20px;
}
}
.quesiton { .quesiton {
color: #b9b9b9; color: #b9b9b9;
} }

View File

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

View File

@@ -6,21 +6,40 @@
class="header-radio" class="header-radio"
@input="handleChange" @input="handleChange"
> >
<el-radio-button <template v-if="String(deptId).substring(0, 3) === '875'">
label="2" <el-radio-button
:disabled="isPrivate" label="0"
:class="{ 'btn-disabled': isPrivate }" :disabled="isPublic"
>企业</el-radio-button> :class="{ 'btn-disabled': isPublic }"
<el-radio-button >个人</el-radio-button>
label="0" <el-radio-button
:disabled="isPublic" label="1"
:class="{ 'btn-disabled': isPublic }" :disabled="isPublic"
>个人</el-radio-button> :class="{ 'btn-disabled': isPublic }"
<el-radio-button >商户</el-radio-button>
label="1" <el-radio-button
:disabled="isPublic" label="2"
:class="{ 'btn-disabled': isPublic }" :disabled="isPrivate"
>商户</el-radio-button> :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> </el-radio-group>
<div class="taskTop"> <div class="taskTop">
<div class="taskTop_left"> <div class="taskTop_left">
@@ -1180,6 +1199,9 @@ export default {
}, },
computed: { computed: {
...mapGetters(['roles']), ...mapGetters(['roles']),
deptId() {
return this.$store.state.user.deptId
},
// 总行 // 总行
isHeadAdmin() { isHeadAdmin() {
return this.roles.includes('headAdmin') return this.roles.includes('headAdmin')
@@ -1215,17 +1237,22 @@ export default {
created() { created() {
// getGroupInfoByGroupId({}) // getGroupInfoByGroupId({})
this.isUserType() 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) { if (this.isPublic) {
this.selectedTab = '2' this.selectedTab = '2'
this.custTypeList = [{ label: '企业', value: '2' }] this.custTypeList = [{ label: '企业', value: '2' }]
} else if (this.isPrivate) { } else if (this.isPrivate) {
this.selectedTab = '0' this.selectedTab = defaultTab
this.custTypeList = [ this.custTypeList = [
{ label: '个人', value: '0' }, { label: '个人', value: '0' },
{ label: '商户', value: '1' } { label: '商户', value: '1' }
] ]
} else { } else {
this.selectedTab = '2' this.selectedTab = defaultTab
this.custTypeList = [ this.custTypeList = [
{ label: '个人', value: '0' }, { label: '个人', value: '0' },
{ label: '商户', value: '1' }, { label: '商户', value: '1' },

View File

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