工作时间

哪有项目经理,直接是老板(总经理)直接对接的
工作时间8小时?
工作日
8:30-12:00
13:30-18:00
周六(无偿)
9:00-12:00

薪资

2760+五险一金

2026-06-15

了解若依框架 RuoYi

项目管理
安装 TortoiseSVN
安装 TortoiseGit

yudao框架
熟悉项目,了解项目整体架构

2026-06-16

校园招聘项目

需求:新增接口
查询管理员,也就是查询users表中role_id = 0 and status = 1的数据)
原接口查询到的是所有数据,在页面做的过滤,现在是要修改这种方式,直接查询出管理员数据。
UserMapper.xml

1
2
3
4
5
6
<!--2026-6-15 查询管理员(role_id = 0 and status = 1) -->
<select id="selectAdmins" resultType="com.example.luan_recruitment_system.entity.User">
SELECT * FROM users
WHERE role_id = 0 AND status = 1
ORDER BY create_time DESC
</select>

UserMapper.java

1
List<User> selectAdmins();

UserServiceImpl.java

1
2
3
4
@Override
public List<User> getAdmins() {
return userMapper.selectAdmins();
}

UserService.java

1
2
3
4
/**
* 查询管理员(role_id = 0 and status = 1)
*/
List<User> getAdmins();

UserController.java

1
2
3
4
5
6
7
8
private final UserService userService;

@Operation(summary = "查询管理员", description = "查询users表中role_id = 0 and status = 1的数据")
@GetMapping("/admins")
public Result<List<User>> getAdmins() {
List<User> admins = userService.getAdmins();
return Result.success(admins);
}

2026-06-17

两非两资项目

主表 sasac_schedule_new 包含字段:

  • 记录类型(企业经营/项目实施/资产处置)
  • 标的企业信息(名称、股权结构、企业级次等)
  • 资产信息(土地、厂房相关)
  • 处置信息(处置方式、进展、难点等)
  • 财务数据(资产总额、负债总额、净资产等)

子表 sasac_schedule_month 包含:

  • 关联年度计划事项 ( schedule_id )
  • 处置对象名称、关键突破举措
  • 本月重点工作措施、处置完成金额等

子表 sasac_schedule_file 包含:

  • 关联两非两资 ( schedule_id )
  • 业务阶段、文件项目类别、文件URL
    Controller 路由定义为 /sasac/schedule-new ,前端路由使用 /sasac/schedulenew

流程关联详情

1. 前端 BPM 页面

2. 流程关联机制

1
businessKey 格式: sasac_schedule_new:${业务ID}

前端发起流程时:

  1. 先调用 ScheduleNewApi.createScheduleNew() 创建业务数据
  2. 再调用 ProcessInstanceApi.createProcessInstance() 发起 BPM 流程
  3. 流程变量包含 recordType, planYear, launchStatus, deptId

3. 后端流程监听器

SasacScheduleResultListener.java

监听流程结束事件,根据审批结果更新业务状态:

  • PROCESS_STATUS = 1 → 审批通过 → launchStatus = 2
  • PROCESS_STATUS = 3 → 审批不通过 → launchStatus = 4

⚠️ 注意事项

监听器代码中引用的是 ScheduleMapperScheduleDO(旧表 sasac_schedule
而不是 ScheduleNewMapperScheduleNewDO(新表 sasac_schedule_new)。

这可能需要确认:

  • 流程监听器是否需要更新为监听 sasac_schedule_new 表?
  • 或者 sasac_schedulesasac_schedule_new 是两个不同的业务模块?

完整方案设计

审核状态字段设计

状态 说明
0 未提交 草稿状态,未发起流程
1 审核中 流程正在进行
2 审核通过 流程审批通过
3 审核不通过 流程审批拒绝
4 已撤回 流程被撤回

完整实施方案总结

已完成的代码修改

文件 修改内容
ScheduleNewDO.java 添加 approvalStatusprocessInstanceId 字段
ScheduleNewRespVO.java 添加审核状态响应字段
ScheduleNewPageReqVO.java 添加审核状态查询条件
SasacScheduleNewResultListener.java 新建流程监听器
dict.ts 添加审核状态字典类型
index.vue 添加审核状态列和搜索条件

需要执行的数据库 SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 1. 添加审核状态字段到 sasac_schedule_new 表
ALTER TABLE `sasac_schedule_new`
ADD COLUMN `approval_status` SMALLINT DEFAULT 0 COMMENT '审核状态:0-未提交,1-审核中,2-审核通过,3-审核不通过,4-已撤回',
ADD COLUMN `process_instance_id` VARCHAR(64) DEFAULT NULL COMMENT '流程实例ID';

-- 2. 添加字典类型
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES ('两资两非审核状态', 'sasac_schedule_new_approval_status', 0, '两资两非审核状态', '1', NOW(), '1', NOW(), 0);

-- 3. 添加字典数据
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(0, '未提交', '0', 'sasac_schedule_new_approval_status', 0, 'info', '', '未提交', '1', NOW(), '1', NOW(), 0),
(1, '审核中', '1', 'sasac_schedule_new_approval_status', 0, 'warning', '', '审核中', '1', NOW(), '1', NOW(), 0),
(2, '审核通过', '2', 'sasac_schedule_new_approval_status', 0, 'success', '', '审核通过', '1', NOW(), '1', NOW(), 0),
(3, '审核不通过', '3', 'sasac_schedule_new_approval_status', 0, 'danger', '', '审核不通过', '1', NOW(), '1', NOW(), 0),
(4, '已撤回', '4', 'sasac_schedule_new_approval_status', 0, 'info', '', '已撤回', '1', NOW(), '1', NOW(), 0);

流程监听器配置

在 BPM 流程设计器中,需要在流程结束节点配置监听器:

  1. 打开流程设计器
  2. 在结束节点添加 ExecutionListener
  3. 监听器类型选择:end
  4. 监听器值填写:${sasacScheduleNewResultListener}

效果预览

页面将显示:

审核状态 标签颜色
未提交 灰色
审核中 橙色
审核通过 绿色
审核不通过 红色
已撤回 灰色

执行完数据库 SQL 后,重启后端服务即可生效。

2026-06-18

  1. 依据需求设计并添加了上报计划审批流程图
  2. 在 SasacDeptService 中添加了一个获取顶级父部门的方法
  3. 添加新表单对应的表单填写页面及表单详情页面
  4. 在流程模型设计器中配置了表单创建路径和表单查看路径

2026-06-19

  1. 完善上报计划审批流程
    项目后端添加查找部门的顶级部门下一级部门的方法并联动表达式完成上报计划审批的通用流程;
    修复上报计划审批流程的审批详情为空的错误;
  2. 调整前端流程实例详情页面下面的按钮
  3. 添加并测试部门独立的流程模型
  4. 解决/bpm/task/create跳转异常页面的问题

2026-06-20

  1. 表单中增加 审批状态(草稿/审批中/已上报/已驳回/已取消)字段
  2. 流程模型设置监听,根据审批情况进行状态更新. 监听到同意操作则更新审批状态为已上报
    监听到撤回操作则更新审批状态为已取消
    监听到驳回操作则更新审批状态为已驳回
  3. 表单中增加暂存按钮,点击暂存按钮时候保存表单数据 同时更新审批状态为草稿
  4. 发起流程的时候更新审批状态为审批中
    0 未提交:仅保存表单,未调用发起流程接口
    0 → 发起流程 → 1 审批中
    1 审批中:
    任一节点驳回 → 3 已驳回
    所有节点全部同意 → 2 审批通过
    发起人撤回单据 → 4 已取消
    3 已驳回:修改单据重新提交 → 回到 1 审批中
    0/1 状态均可取消为 4 已取消;2、3 终态不允许取消
  5. 集团账号查询,操作页面需要有 发起申请 按钮。对审批状态是 草稿 的 进行流程申请

2026-06-22

  1. 添加审核状态显示功能
  2. 补充监听器
  3. 优化提交流程,只需点击一次提交后自动发送审核
  4. 修复部门不显示名字的错误
  5. 修复首页报错的问题
  6. 优化两非两资用户的数据权限部门范围的层级展示

2026-06-23

  1. 新增月度计划审批流程

2026-06-24

  1. 解决正在推进处置,计划完成处置的scheduleId为空导致不显示数据的问题
  2. 优化页面展示,调整审核状态标签位置
  3. 给新增年度处置计划添加提交按钮
  4. 给新增月度计划添加提交按钮
  5. 修改年度处置计划流程表单以适配新表单
  6. 修复月度计划提交功能
  7. 添加未提交时更多里的提交按钮

2026-06-25

  1. 发起流程时,添加可能的字段
  2. 更新年度和月度的审批详情页面
  3. 月度审批的时候 审批状态没有存放到表中
  4. 修复年度展示的错误

2026-06-26

  1. 月度计划审核状态修复
  2. 月度计划暂存修复
  3. 字段转换

数易通

2026-06-29

前端报错问题

报错:
http://localhost/marketing/task-template 页面报错
permission.js:512 [Vue warn]: Error in created hook: “TypeError: Cannot read properties of undefined (reading ‘marketing’)”
found in
—> at src/views/marketing/template/index.vue
at src/layout/components/AppMain.vue
at src/layout/index.vue
at src/App.vue

permission.js:512 TypeError: Cannot read properties of undefined (reading ‘marketing’)

原因:没有后端实现
解决:添加后端部分
过程:

  1. 了解需求:
    移动端:客户经理移动工作台
    页面4:营销任务派发(自动+手动)
    4.1 我的任务列表
    • 任务类型:新客拓展、存款提升、贷款营销、到期转化、走访、整村授信摸排、整村授信审核(新增)
    • 任务状态:待执行、执行中、已完成、逾期
    • 任务来源:系统自动派发 / 支行下发 / 自己创建 / 网格管理员下发
    • 新增筛选:是否完成电话沟通、电话沟通时长区间、整村授信任务类型、授信批次(新增)
    4.2 任务详情页
    • 任务标题、下发机构、截止时间
    • 客户清单(支持多选,整村授信任务可按行政村批量选择客户,网格任务可按网格批量选择客户)
    • 产品建议(系统自动匹配,整村授信任务匹配“农户信用贷”“村集体担保贷”等产品)
    • 标准话术模板(一键复制,新增整村授信专属话术,如摸排话术、授信告知话术,新增清单任务、网格任务专属话术)
    • 走访计划:时间、地点、事项(整村授信任务可设置“行政村集中走访”,网格任务可设置“网格集中走访”)
    • 执行记录:填写内容、上传照片、定位打卡
    • 电话沟通记录:沟通客户、沟通时长、沟通类型、沟通摘要(完成任务时,可关联已有的电话沟通记录,无需重复填写)
    • 整村授信任务专属记录(新增):
    • 摸排任务:填写摸排信息、上传摸排照片、提交预授信建议
    • 审核任务:填写审核意见、选择审核结果(通过/驳回)、备注驳回原因、提交审核

4.3 外勤打卡
• 时间自动获取
• 定位获取(网点/客户地址/行政村集中走访点)
• 现场拍照
• 走访小结(整村授信走访可填写“行政村整体摸排情况”)

模块2:营销任务管理
• 任务模板创建(存款/贷款/走访/到期/整村授信摸排/整村授信审核,可设置“电话沟通时长要求”)
• 任务批量派发(按网点/客户经理/客户群/行政村)
• 任务进度监控(“电话沟通完成情况”“整村授信任务推进情况”监控)
• 任务审核与驳回(可校验电话沟通时长、整村授信摸排信息是否达标)
• 任务数据统计(新增任务关联的电话沟通总时长、平均时长、整村授信任务完成率)

四、核心接口清单
• 企微登录免登接口
• 客户列表查询接口(整村授信筛选)
• 客户详情查询接口(整村授信)
• 客户交易流水接口
• 客户标签 & 画像接口(授信类标签)
• 营销任务列表接口(整村授信任务类型)
• 任务下发 & 提交接口
• 风险预警接口
• 业绩统计接口
• 外勤打卡接口
• 上传图片 / 录音接口
• 电话沟通记录新增接口
• 电话沟通记录查询接口
• 电话沟通记录编辑接口
• 企微电话时长同步接口
• 沟通时长统计接口
• 沟通时长异常监控接口
• 整村授信摸排记录新增接口
• 整村授信审核提交接口
• 行政村与授信批次查询接口
• 整村授信客户详情查询接口
• 整村授信批量摸排任务创建接口
• 整村授信报表导出接口

五、权限体系
客户经理:查看本人网格、执行清单任务、电话沟通、摸排上报、贷后检查
网格管理员:查看所辖网格、统筹任务、督办进度、查看网格报表
支行管理员:创建清单模板、下发任务、网格分配、审核授信、查看全网点数据
总行管理员:全局网格视图、全机构报表、风险监控、规则配置

  1. 创建表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- ----------------------------
-- 营销任务模板表
-- ----------------------------
drop table if exists marketing_task_template;
create table marketing_task_template (
id bigint(20) not null auto_increment comment '模板ID',
template_name varchar(100) not null comment '模板名称',
task_type char(1) not null comment '任务类型(0新客拓展 1存款提升 2贷款营销 3到期转化 4走访 5整村授信摸排 6整村授信审核)',
require_duration int(11) default 0 comment '电话沟通时长要求(分钟)',
start_time datetime comment '任务开始时间',
end_time datetime comment '任务结束时间',
status char(1) default '0' comment '状态(0启用 1停用)',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (id)
) engine=innodb auto_increment=1 comment = '营销任务模板表';

create index idx_marketing_task_template_task_type on marketing_task_template(task_type);
create index idx_marketing_task_template_status on marketing_task_template(status);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-- ----------------------------
-- 营销任务主表
-- ----------------------------
drop table if exists marketing_task;
create table marketing_task (
id bigint(20) not null auto_increment comment '任务ID',
task_name varchar(200) not null comment '任务名称',
task_type char(1) default '0' comment '任务类型(0新客拓展 1存款提升 2贷款营销 3到期转化 4走访 5整村授信摸排 6整村授信审核)',
template_id bigint(20) default null comment '任务模板ID',
customer_id bigint(20) default null comment '客户ID',
customer_name varchar(100) default '' comment '客户姓名',
manager_id bigint(20) default null comment '客户经理ID',
manager_name varchar(100) default '' comment '客户经理姓名',
village_id varchar(64) default null comment '行政村ID',
village_name varchar(200) default '' comment '所属行政村',
require_duration int(11) default 0 comment '要求时长(分钟)',
real_duration int(11) default 0 comment '实际沟通时长(分钟)',
task_status char(1) default '0' comment '任务状态(0待执行 1进行中 2已完成 3已逾期)',
audit_status char(1) default '0' comment '审核状态(0待审核 1已通过 2已驳回)',
audit_result varchar(20) default null comment '审核结果(pass通过 reject驳回)',
reject_reason varchar(500) default null comment '驳回原因',
audit_by varchar(64) default '' comment '审核人',
audit_time datetime comment '审核时间',
deadline_time datetime comment '截止时间',
dispatch_type varchar(20) default null comment '派发维度(org按网点 manager按客户经理 tag按客户群标签 village按行政村)',
dispatch_target varchar(200) default null comment '派发对象值',
remark varchar(500) default null comment '备注',
del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
primary key (id)
) engine=innodb auto_increment=1000 comment = '营销任务主表';

-- 索引
create index idx_marketing_task_task_type on marketing_task(task_type);
create index idx_marketing_task_task_status on marketing_task(task_status);
create index idx_marketing_task_audit_status on marketing_task(audit_status);
create index idx_marketing_task_manager_id on marketing_task(manager_id);
create index idx_marketing_task_village_id on marketing_task(village_id);
create index idx_marketing_task_customer_id on marketing_task(customer_id);
  1. 修改代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 旧的
taskTypeOptions: [
{ label: '常规跟进', value: '0' },
{ label: '贷后检查提醒', value: '1' },
{ label: '逾期催收', value: '2' },
{ label: '产品营销', value: '3' },
{ label: '整村授信摸排', value: '4' },
{ label: '其他', value: '5' }
],
// 字典
taskTypeOptions:[
{ label: '新客拓展', value: '0' },
{ label: '存款提升', value: '1' },
{ label: '贷款营销', value: '2' },
{ label: '到期转化', value: '3' },
{ label: '走访', value: '4' },
{ label: '整村授信摸排', value: '5' },
{ label: '整村授信审核', value: '6' }
],
1
2
3
4
5
--- 获取所有表中的某个字段
SELECT DISTINCT table_name
FROM information_schema.columns
WHERE table_schema = 'cmmw'
AND column_name = 'task_type';

添加 batchDispatchsaveAudit 接口

建表时:修改createTable
createTable({ sql: this.content, tplWebType: “element-ui” }).then(res => {

2026-06-30

四次失败汇总
次数 派发方式 目标参数 查询结果 状态
1 按组织派发 orgId=2001 Total: 0 ❌ 失败
2 按标签派发 tagId=1 Total: 0 ❌ 失败
3 按客户经理派发 managerName=”王行长” Total: 0 ❌ 失败
4 按组织派发 orgId=2003 Total: 0 ❌ 失败

系统处于”有模板、无农户”的状态:
✅ 营销任务模板存在(templateId=1, 2 都能查到)
❌ 农户数据为空或无效
❌ 组织与农户的关联数据缺失
❌ 标签与农户的关联数据缺失
❌ 客户经理与农户的关联数据缺失

电话要求时长

http://localhost/marketing/task-audit页面下
为任务类型整村授信任务(整村授信摸排,整村授信审核)在操作栏添加推进情况
为任务类型整村授信摸排在审核时再添加一个是否达标的展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
-- ----------------------------
-- 用户端营销任务表
-- ----------------------------
drop table if exists user_marketing_task;
create table user_marketing_task (
id bigint(20) not null auto_increment comment '任务ID',
task_no varchar(32) not null comment '任务编号(便于展示和查询)',
task_name varchar(200) not null comment '任务名称',
task_type char(1) default '0' comment '任务类型(0新客拓展 1存款提升 2贷款营销 3到期转化 4走访 5整村授信摸排 6整村授信审核)',
task_type_name varchar(50) default '' comment '任务类型名称(冗余展示)',
customer_id bigint(20) default null comment '客户ID',
customer_name varchar(100) default '' comment '客户姓名',
customer_phone varchar(20) default '' comment '客户手机号(方便联系)',
customer_address varchar(200) default '' comment '客户地址(走访需要)',
village_id varchar(64) default null comment '行政村ID',
village_name varchar(200) default '' comment '所属行政村',
manager_id bigint(20) not null comment '接收任务的客户经理ID',
manager_name varchar(100) default '' comment '客户经理姓名',
require_duration int(11) default 0 comment '要求时长(分钟)',
real_duration int(11) default 0 comment '实际沟通时长(分钟)',
task_status char(1) default '0' comment '任务状态(0待执行 1进行中 2已完成 3已逾期)',
task_status_name varchar(20) default '' comment '状态名称(冗余展示)',
deadline_time datetime comment '截止时间',
execute_time datetime comment '执行时间(开始执行的时间)',
finish_time datetime comment '完成时间',
execute_result text comment '执行结果/走访记录(富文本或结构化内容)',
feedback_content varchar(1000) default null comment '反馈内容(客户意向、需求等)',
coordinate varchar(100) default null comment '打卡坐标(走访签到用)',
coordinate_address varchar(200) default null comment '打卡地址',
sign_in_time datetime comment '签到时间',
sign_out_time datetime comment '签退时间',
is_read char(1) default '0' comment '是否已读(0未读 1已读)',
read_time datetime comment '阅读时间',
source_task_id bigint(20) default null comment '来源管理端任务ID(关联marketing_task)',
sync_time datetime comment '同步时间(从管理端同步的时间)',
remark varchar(500) default null comment '备注',
del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
create_by varchar(64) default '' comment '创建者(同步来源)',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
primary key (id),
unique key idx_task_no (task_no),
key idx_manager_id (manager_id),
key idx_task_status (task_status),
key idx_deadline_time (deadline_time),
key idx_source_task_id (source_task_id),
key idx_customer_id (customer_id)
) engine=innodb auto_increment=1000 comment = '用户端营销任务表';

客户基础信息
编辑客户画像规则
客户画像配置添加定时任务
Bean调用示例:ryTask.ryParams(‘ry’)
Class类调用示例:com.ruoyi.quartz.task.RyTask.ryParams(‘ry’)
参数说明:支持字符串,布尔类型,长整型,浮点型,整型

客户画像配置添加定时任务,我只需要调用方法:
Bean调用示例:ryTask.ryParams(‘ry’)
Class类调用示例:com.ruoyi.quartz.task.RyTask.ryParams(‘ry’)
参数说明:支持字符串,布尔类型,长整型,浮点型,整型

2026-07-01

新建表
可以将字典换为数据库表吗
画像数据维度 portrait_dimension
画像关联指标 portrait_index
画像条件运算符 portrait_operator
画像计算方式(手动触发,后台自动定时) portrait_calc_type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
-- ----------------------------
-- 初始化-客户标签数据
-- ----------------------------
INSERT INTO t_customer_tag VALUES (1, '高潜力客户', 'portrait', 1, '资产和收入达到一定标准的客户', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_customer_tag VALUES (2, '老年客户', 'portrait', 1, '年龄大于等于60岁的客户', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_customer_tag VALUES (3, '青年客户', 'portrait', 1, '年龄在18-35岁之间的客户', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_customer_tag VALUES (4, '高收入客户', 'portrait', 1, '家庭年收入较高的客户', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_customer_tag VALUES (5, '授信客户', 'portrait', 1, '已完成整村授信的客户', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_customer_tag VALUES (6, '高沟通频率客户', 'portrait', 1, '电话沟通时长较长的客户', 0, 'admin', SYSDATE(), '', NULL);

-- ----------------------------
-- 2、客户画像规则表 t_customer_portrait_rule
-- ----------------------------
DROP TABLE IF EXISTS t_customer_portrait_rule;
CREATE TABLE t_customer_portrait_rule (
rule_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '规则主键',
rule_name VARCHAR(100) NOT NULL COMMENT '画像名称',
data_dimension VARCHAR(50) NOT NULL COMMENT '数据维度编码',
relate_index VARCHAR(100) NOT NULL COMMENT '关联指标编码',
condition_operator VARCHAR(20) NOT NULL COMMENT '条件运算符',

threshold_value VARCHAR(200) DEFAULT '' COMMENT '阈值/值',
min_value VARCHAR(100) DEFAULT '' COMMENT '区间最小值',
max_value VARCHAR(100) DEFAULT '' COMMENT '区间最大值',

bind_tag_id BIGINT(20) DEFAULT NULL COMMENT '绑定标签ID',
bind_tag_name VARCHAR(100) DEFAULT '' COMMENT '绑定标签名称(列表联查展示)',


PRIMARY KEY (rule_id),
INDEX idx_data_dimension (data_dimension),
INDEX idx_status (status),
INDEX idx_del_flag (del_flag),
INDEX idx_bind_tag_id (bind_tag_id)
) ENGINE=INNODB AUTO_INCREMENT=100 COMMENT='客户画像规则表';



-- ----------------------------
-- 4、画像数据维度表 t_portrait_dimension
-- ----------------------------
DROP TABLE IF EXISTS t_portrait_dimension;
CREATE TABLE t_portrait_dimension (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
dimension_code VARCHAR(50) NOT NULL COMMENT '维度编码',
dimension_name VARCHAR(100) NOT NULL COMMENT '维度名称',
sort_order INT(4) DEFAULT 0 COMMENT '排序号',
status INT(1) DEFAULT 0 COMMENT '状态 0启用 1停用',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
del_flag INT(1) DEFAULT 0 COMMENT '逻辑删除 0正常 2删除',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_dimension_code (dimension_code),
INDEX idx_status (status),
INDEX idx_del_flag (del_flag)
) ENGINE=INNODB AUTO_INCREMENT=100 COMMENT='画像数据维度表';

INSERT INTO t_portrait_dimension VALUES (1, 'customer_base', '客户基础信息', 1, 0, '客户基础信息维度', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_dimension VALUES (2, 'asset', '资产数据', 2, 0, '资产数据维度', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_dimension VALUES (3, 'communicate', '沟通行为数据', 3, 0, '沟通行为数据维度', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_dimension VALUES (4, 'village_credit', '整村授信数据', 4, 0, '整村授信数据维度', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_dimension VALUES (5, 'marketing_task', '营销任务清单数据', 5, 0, '营销任务清单数据维度', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_dimension VALUES (6, 'grid_village', '网格行政村数据', 6, 0, '网格行政村数据维度', 0, 'admin', SYSDATE(), '', NULL);

-- ----------------------------
-- 5、画像关联指标表 t_portrait_index
-- ----------------------------
DROP TABLE IF EXISTS t_portrait_index;
CREATE TABLE t_portrait_index (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
dimension_code VARCHAR(50) NOT NULL COMMENT '所属维度编码',
index_code VARCHAR(100) NOT NULL COMMENT '指标编码(对应t_farmer表字段)',
index_name VARCHAR(100) NOT NULL COMMENT '指标名称',
data_type VARCHAR(20) DEFAULT 'number' COMMENT '数据类型 number/text/enum',
sort_order INT(4) DEFAULT 0 COMMENT '排序号',
status INT(1) DEFAULT 0 COMMENT '状态 0启用 1停用',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
del_flag INT(1) DEFAULT 0 COMMENT '逻辑删除 0正常 2删除',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_index_code (index_code),
INDEX idx_dimension_code (dimension_code),
INDEX idx_status (status),
INDEX idx_del_flag (del_flag)
) ENGINE=INNODB AUTO_INCREMENT=100 COMMENT='画像关联指标表';

INSERT INTO t_portrait_index VALUES (1, 'customer_base', 'age', '年龄', 'number', 1, 0, '农户年龄', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_index VALUES (2, 'customer_base', 'gender', '性别', 'enum', 2, 0, '男/女', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_index VALUES (3, 'customer_base', 'family_population', '家庭人口', 'number', 3, 0, '家庭人口数', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_index VALUES (4, 'asset', 'family_annual_income', '家庭年收入', 'number', 1, 0, '家庭年收入(元)', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_index VALUES (5, 'communicate', 'call_duration_min', '累计沟通时长', 'number', 1, 0, '累计电话沟通时长(分钟)', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_index VALUES (6, 'village_credit', 'credit_village_status', '授信状态', 'enum', 1, 0, '整村授信状态', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_index VALUES (7, 'customer_base', 'customer_level', '客户等级', 'enum', 4, 0, '农户等级 normal/potential/premium/core', 0, 'admin', SYSDATE(), '', NULL);

-- ----------------------------
-- 6、画像条件运算符合 t_portrait_operator
-- ----------------------------
DROP TABLE IF EXISTS t_portrait_operator;
CREATE TABLE t_portrait_operator (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
operator_code VARCHAR(20) NOT NULL COMMENT '运算符编码',
operator_name VARCHAR(50) NOT NULL COMMENT '运算符名称',
data_type VARCHAR(20) DEFAULT 'number' COMMENT '支持的数据类型 number/text/enum/all',
sort_order INT(4) DEFAULT 0 COMMENT '排序号',
status INT(1) DEFAULT 0 COMMENT '状态 0启用 1停用',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
del_flag INT(1) DEFAULT 0 COMMENT '逻辑删除 0正常 2删除',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_operator_code (operator_code),
INDEX idx_data_type (data_type),
INDEX idx_status (status),
INDEX idx_del_flag (del_flag)
) ENGINE=INNODB AUTO_INCREMENT=100 COMMENT='画像条件运算符合';

INSERT INTO t_portrait_operator VALUES (1, 'gt', '大于', 'number', 1, 0, '大于指定值', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_operator VALUES (2, 'lt', '小于', 'number', 2, 0, '小于指定值', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_operator VALUES (3, 'eq', '等于', 'all', 3, 0, '等于指定值', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_operator VALUES (4, 'gte', '大于等于', 'number', 4, 0, '大于等于指定值', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_operator VALUES (5, 'lte', '小于等于', 'number', 5, 0, '小于等于指定值', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_operator VALUES (6, 'between', '区间', 'number', 6, 0, '在指定区间内', 0, 'admin', SYSDATE(), '', NULL);
INSERT INTO t_portrait_operator VALUES (7, 'like', '包含', 'text', 7, 0, '包含指定文本', 0, 'admin', SYSDATE(), '', NULL);

客户画像配置
新增或修改客户画像规则时
满足后打标签,应该可以选择多个标签

画像计算方式表 t_portrait_calc_type 没必要创建吧,t_customer_portrait_rule不是有calc_type字段吗

年龄区间
ageRange
性别

客户等级
customerLevel
家庭人口
家庭年收入 yearIncome

所属行政村
电话沟通总时长
callTotalMin
电话平均时长
callAvgMin
近30天沟通次数
callTimesMonth

整村授信状态
creditVillageStatus
授信意愿
creditWill
摸排信息完整度
surveyCompleteRate

任务完成率
taskCompleteRate
待处理任务数
taskPendingNum

行政村授信渗透率
villageCreditRate

http://localhost/users/portrait 计算方式显示有问题,明明选择后台自动,显示手动触发

手动全局刷新报错

1
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '89-15-0' for key 't_farmer_tag.uk_farmer_tag' ### The error may exist in file [E:\数意通\CMMW\ruoyi-customer-mobile\target\classes\mapper\mobile\TFarmerTagMapper.xml] ### The error may involve defaultParameterMap ### The error occurred while setting parameters ### SQL: INSERT INTO t_farmer_tag(farmer_id,tag_id,tag_name,source_type,rule_id,bind_time,deleted) VALUES (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) , (?,?,?,2,?,NOW(),0) ### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '89-15-0' for key 't_farmer_tag.uk_farmer_tag' ; Duplicate entry '89-15-0' for key 't_farmer_tag.uk_farmer_tag'

核心原因
业务场景:给农户批量绑定标签
本次批量插入列表里本身存在重复(同个农户 + 同个标签重复传了两次);
数据库中老数据已存在该农户标签绑定关系,又重复新增;
批量 insert 一次性多条,只要其中一条冲突,整批全部插入失败。

使用 IGNORE

DROP INDEX uk_farmer_tag ON t_farmer_tag;
CREATE UNIQUE INDEX uk_farmer_tag ON t_farmer_tag(farmer_id,tag_id,source_type,rule_id);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    /**
* 查询农户基础信息主列表
*
* @param tFarmer 农户基础信息主
* @return 农户基础信息主
*/
@Override
public List<TFarmer> selectTFarmerList(TFarmer tFarmer)
{
List<TFarmer> list = farmerMapper.selectTFarmerList(tFarmer);
if (list.isEmpty()) {
return list;
}
List<Long> farmerIds = list.stream().map(TFarmer::getId).collect(Collectors.toList());
List<FarmerTagVO> tagVOs = tagMapper.selectTagIdsByFarmerIds(farmerIds);
Map<Long, List<Long>> tagMap = tagVOs.stream()
.collect(Collectors.groupingBy(FarmerTagVO::getFarmerId,
Collectors.mapping(FarmerTagVO::getTagId, Collectors.toList())));
for (TFarmer farmer : list) {
farmer.setTagIds(tagMap.getOrDefault(farmer.getId(), new ArrayList<>()));
}
return list;
// return farmerMapper.selectTFarmerList(tFarmer);
}
``tag
```xml
<!-- 批量查询多个农户的标签ID列表 -->
<select id="selectTagIdsByFarmerIds" resultType="com.ruoyi.mobile.vo.FarmerTagVO">
SELECT farmer_id AS farmerId, tag_id AS tagId
FROM t_farmer_tag
WHERE farmer_id IN
<foreach collection="farmerIds" item="farmerId" open="(" separator="," close=")">
#{farmerId}
</foreach>
AND deleted = 0
</select>

tag

1
2
// 批量查询多个农户的标签ID列表
List<FarmerTagVO> selectTagIdsByFarmerIds(@Param("farmerIds") List<Long> farmerIds);
1
2
3
4
5
6
7
8
9
package com.ruoyi.mobile.vo;

import lombok.Data;

@Data
public class FarmerTagVO {
private Long farmerId;
private Long tagId;
}

2026-07-02

1
2
3
4
5
6
<el-table-column label="推进情况" prop="creditStatus" align="center" width="100">
<template slot-scope="scope">
<dict-tag v-if="scope.row.taskType === '5' || scope.row.taskType === '6'" :options="getFilteredCreditOptions(scope.row.taskType)" :value="scope.row.creditStatus" />
<span v-else></span>
</template>
</el-table-column>

WHERE t.deleted = 0 AND t.id IN (
SELECT ft.farmer_id FROM t_farmer_tag ft WHERE ft.deleted = 0
AND ft.tag_name IN (SELECT tag_name FROM t_customer_tag WHERE tag_id = #{tagId} AND del_flag < 1)
)

客户经理:sys_user
派发维度按客户经理时选择对象关联的数据库错了应该是sys_user

按客户群标签派发的没有显示:客户经理,所属行政村的数据

  1. 数据库新增两个字段org_id,org_name,给前端显示添加所属网点。
  2. 推进情况只显示 undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  <!-- 授信状态查询下拉 -->
<el-form-item label="授信状态" prop="creditVillageStatus">
<el-select v-model="queryParams.creditVillageStatus" placeholder="全部" clearable style="width: 180px">
<el-option
v-for="o in dict.type.credit_status_map || []"
:key="o.value"
:label="o.label"
:value="Number(o.value)"
/>
</el-select>
</el-form-item>


<el-table-column label="授信状态" prop="creditVillageStatus" align="center" width="108">
<template slot-scope="scope">
<dict-tag :options="dict.type.credit_status_map" :value="String(scope.row.creditVillageStatus)"/>
</template>
</el-table-column>

http://localhost/users/customer-list
授信状态

帮我用同样的方法,在 http://localhost/marketing/task-audit 的推进情况下显示这个授信状态

为任务类型整村授信摸排
在审核时添加一个整村授信摸排信息展示,就是
http://localhost/mobile/survey 的查看摸排信息里的摸排信息

派发维度
按客户经理 选择对象时下拉只显示 role_id=2001 的客户经理
按行政村 只能选择村一级的 area_type=3 的

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取客户经理下拉列表(只返回role_id=2001的客户经理)
*/
@GetMapping("/managerList")
public AjaxResult getManagerList()
{
SysUser user = new SysUser();
user.setRoleId(2001L);
List<SysUser> list = sysUserService.selectCustomerList(user);
return success(list);
}

t_portrait_dimension
t_portrait_index

2026-07-03

api/task.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import request from '@/utils/request'

// 查询营销任务列表
export function listTask(query) {
return request({
url: '/marketing/task/list',
method: 'get',
params: query
})
}

// 获取营销任务详情
export function getTask(id) {
return request({
url: '/marketing/task/' + id,
method: 'get'
})
}

// 新增营销任务
export function addTask(data) {
return request({
url: '/marketing/task',
method: 'post',
data: data
})
}

// 修改营销任务
export function updateTask(data) {
return request({
url: '/marketing/task',
method: 'put',
data: data
})
}

// 删除营销任务
export function delTask(ids) {
return request({
url: '/marketing/task/' + ids,
method: 'delete'
})
}

// 批量派发营销任务
export function batchDispatchTask(data) {
return request({
url: '/marketing/task/batchDispatch',
method: 'post',
data: data
})
}

// 保存审核结果
export function saveTaskAudit(data) {
return request({
url: '/marketing/task/saveAudit',
method: 'post',
data: data
})
}

// 任务数据统计
export function getTaskStatistics(params) {
return request({
url: '/marketing/task/statistics',
method: 'get',
params: params
})
}

// 电话沟通时长达标情况
export function getDurationProgress(params) {
return request({
url: '/marketing/task/durationProgress',
method: 'get',
params: params
})
}

// 整村授信任务推进情况
export function getVillageProgress(params) {
return request({
url: '/marketing/task/villageProgress',
method: 'get',
params: params
})
}

// 获取客户经理下拉列表
export function getManagerList() {
return request({
url: '/marketing/task/managerList',
method: 'get'
})
}

走访
整村授信摸排
整村授信审核

电话沟通
应该添加两个字段

1
2
3
ALTER TABLE t_farmer_call
ADD COLUMN IF NOT EXISTS task_id BIGINT DEFAULT NULL COMMENT '关联营销任务ID' AFTER farmer_id,
ADD COLUMN IF NOT EXISTS manager_id BIGINT DEFAULT NULL COMMENT '记录人(客户经理)ID' AFTER task_id;

任务详情上半部分的数据
若接口报 403,请确认 marketing:task:query 权限已分配给当前角色

2026-07-04

周六:9:00-12:00

2026-07-06

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
-- ----------------------------
-- 7、营销拜访计划表 marketing_visit_plan
-- ----------------------------
DROP TABLE IF EXISTS marketing_visit_plan;
CREATE TABLE marketing_visit_plan (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
task_id BIGINT(20) NOT NULL COMMENT '关联营销任务ID',
plan_time DATETIME NOT NULL COMMENT '计划拜访时间',
place VARCHAR(500) DEFAULT NULL COMMENT '拜访地点',
matter VARCHAR(1000) DEFAULT NULL COMMENT '走访事项',
is_village_group INT(1) DEFAULT 0 COMMENT '是否村组拜访 0否 1是',
is_grid_group INT(1) DEFAULT 0 COMMENT '是否网格组拜访 0否 1是',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
del_flag INT(1) DEFAULT 0 COMMENT '逻辑删除 0正常 2删除',
PRIMARY KEY (id),
INDEX idx_task_id (task_id),
INDEX idx_plan_time (plan_time),
INDEX idx_del_flag (del_flag)
) ENGINE=INNODB AUTO_INCREMENT=100 COMMENT='营销拜访计划表';

-- ----------------------------
-- 8、营销执行记录表 marketing_exec_record
-- ----------------------------
DROP TABLE IF EXISTS marketing_exec_record;
CREATE TABLE marketing_exec_record (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
task_id BIGINT(20) NOT NULL COMMENT '关联营销任务ID',
content VARCHAR(2000) DEFAULT NULL COMMENT '执行内容',
photos TEXT DEFAULT NULL COMMENT '走访照片(JSON数组格式存储图片URL)',
punch_time DATETIME COMMENT '打卡时间',
punch_addr VARCHAR(500) DEFAULT NULL COMMENT '打卡地址',
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
del_flag INT(1) DEFAULT 0 COMMENT '逻辑删除 0正常 2删除',
PRIMARY KEY (id),
INDEX idx_task_id (task_id),
INDEX idx_punch_time (punch_time),
INDEX idx_del_flag (del_flag)
) ENGINE=INNODB AUTO_INCREMENT=100 COMMENT='营销执行记录表';

任务详情下的选择时间都只能选择当前时间
(计划走访时间 打卡时间 沟通时间 摸排时间)

摸排任务对接后端,与管理前端http://localhost/mobile/survey 是一样的(摸排人直接显示为当前登录的客户经理)。

我的任务下的过滤条件没效果
电话沟通完成,沟通时长,授信批次
(授信批次应该调用授信批次接口获取批次列表)

将api文件夹下的
call.js
customer.js
plan.js
record.js
survey.js
task.js
移动到了api/mobile下,调整相应的代码

整村授信摸排
整村授信审核

任务详情的电话沟通记录的
沟通类型
沟通时长
沟通摘要
没有返回来

任务详情的审核任务还没有与后端对接

http://localhost/mobile/survey 下的列表是哪个数据表?

2026-07-07

任务来源:系统自动派发 / 支行下发 / 自己创建 / 网格管理员下发

1
2
3
4
5
6
7
USE cmmw;
ALTER TABLE `marketing_task`
ADD COLUMN `source_type` varchar(20) DEFAULT 'system' COMMENT '任务来源(system系统自动派发 branch支行下发 self自己创建 grid网格管理员下发)'
AFTER `dispatch_target`;

-- 已有数据默认归为系统自动派发(历史数据无来源记录)
UPDATE `marketing_task` SET `source_type` = 'system' WHERE `source_type` IS NULL OR `source_type` = '';
  1. 我的任务的授信批次过滤没有识别到该批次下的任务
  2. 任务详情下的电话沟通记录,客户姓名和手机号分开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- ============================================================
-- 营销任务-客户关联表
-- 用于任务详情页「客户清单」多选客户并保存
-- ============================================================
CREATE TABLE `marketing_task_customer` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`task_id` BIGINT NOT NULL COMMENT '关联营销任务ID',
`customer_id` BIGINT NOT NULL COMMENT '关联客户(t_farmer)ID',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_task_customer` (`task_id`, `customer_id`),
KEY `idx_task_id` (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营销任务-客户关联表';

定时任务:逾期处理
TaskOverdueTask.execute()

Task Status 总结

Task Type Operation Status Change
走访任务(4) 提交拜访计划 0待执行 → 1进行中
走访任务(4) 提交执行记录 1进行中 → 2已完成
普通任务(0-3) 提交电话沟通记录 0待执行 → 1进行中
普通任务(0-3) 沟通时长达标 1进行中 → 2已完成
整村授信摸排(5) 提交摸排记录 0待执行 → 2已完成
整村授信审核(6) 提交审核结果 0待执行 → 2已完成
所有任务 超出截止时间 0/1 → 3已逾期

首页代办事项:
待跟进任务可以正确显示相应数据
pages/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const params = { scope: 'mine' }
getTaskStatistics(params)


todoFollowCount() {
return this.todoCounts.follow || 0
},
todoSurveyCount() {
return this.todoCounts.survey || 0
},
todoAuditCount() {
return this.todoCounts.audit || 0
}
},
onLoad() {
this.syncOrgFromUser()
this.loadStatistics()
},
onShow() {
this.syncOrgFromUser()
this.loadStatistics()
},
methods: {
async loadStatistics() {
try {
const params = {}
const [statsRes, durationRes, villageRes] = await Promise.all([
getTaskStatistics(params),
getDurationProgress(params),
getVillageProgress(params)
])
const stats = statsRes.data || {}
const duration = durationRes.data || {}
const village = villageRes.data || {}
this.callDurationSeconds = Number(duration.totalSeconds || (duration.callDurationMin ? duration.callDurationMin * 60 : 0) || stats.callDurationSec || 0) || 0
this.creditDone = Number(village.done || village.creditDone || stats.creditDone || 0) || 0
this.creditTotal = Number(village.total || village.creditTotal || stats.creditTotal || 0) || 0
this.todoCounts = {
follow: Number(stats.todoFollow || stats.pendingTask || 0) || 0,
survey: Number(stats.todoSurvey || stats.pendingSurvey || 0) || 0,
audit: Number(stats.todoAudit || stats.pendingAudit || 0) || 0
}
} catch (e) {
// 接口异常时保持默认值0
}
},

MarketingTaskServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 任务数据统计
*/
@Override
public Map<String, Object> getTaskStatistics(Map<String, Object> params)
{
// 移动端首页传参 scope='mine' 时,只统计当前登录客户经理的数据
Object scope = params != null ? params.get("scope") : null;
if ("mine".equals(String.valueOf(scope)) && params.get("managerId") == null) {
try {
Long userId = SecurityUtils.getUserId();
if (userId != null) {
params.put("managerId", userId);
}
} catch (Exception e) {
// 获取用户失败时忽略
}
}
return marketingTaskMapper.selectTaskStatistics(params);
}

摸排:
审核:
listAuditSurvey
submitSurveyAudit -> submitCreditAudit

2026-07-08

今日数据概览:

业绩统计后台
电话沟通管理后台
[“http://localhost:9077/profile/upload/2026/07/06/台风_20260706143616A001.jpg"]
移动端我的任务提交摸排任务图片到数据库中是blob:http://localhost:9090/fa06c2ad-c051-4bf0-a4e6-3e56fc4a4e70
正确应该是/profile/upload/2026/07/07/微信图片_20240105100215_20260707164038A008.jpg样式的
管理系统的摸排照片上传

批次状态:0未启动 1运行中 2已暂停 3已结束
编辑批次时,没有返回已关联的行政村

admin-village-detail.vue 与后端对接

移动端行政村与后端对接
行政村的过滤栏可以像我的任务的过滤栏一样可以显示出选择的

问题根因
mobile/survey/list 列表查询的 SQL 里 LEFT JOIN 了 village_credit_survey_audit_log(审核记录表):
LEFT JOIN village_credit_survey_audit_log vcsal ON s.id = vcsal.survey_id
当一条摸排记录(id=79)对应多条审核记录时,LEFT JOIN 会把同一行 village_credit_survey 展开成多行,导致返回两条完全一样的数据。修复内容
我把列表查询和审核列表查询改用一个不包含审核表 JOIN 的 SQL 片段,详情查询保留原样(详情页需要 auditOpinion)。修改文件:

摸排任务,摸排人怎么回事,为什么不显示?
admin-village-detail.vue 手机尾号哪去了?
credit-batch-detail.vue 行政村列表行政村旁边的推进进度呢?

credit-batch.vue 未启动的加上启动按钮,运行中的隐藏编辑按钮

admin-village-detail 摸排客户的摸排人所在位置是手机尾号,这个地方不用显示摸排人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<template>
<view class="page">
<view class="block-card">
<text class="block-title">基本信息</text>
<view class="kv-row flex-row align-center flex-between">
<text class="kv-k">批次名称</text>
<text class="kv-v">{{ d.batchName }}</text>
</view>
<view class="kv-row flex-row align-center flex-between">
<text class="kv-k">启动时间</text>
<text class="kv-v">{{ d.startAt }}</text>
</view>
<view class="kv-row flex-row align-center flex-between">
<text class="kv-k">截止时间</text>
<text class="kv-v">{{ d.endAt }}</text>
</view>
<view class="kv-row flex-row align-center flex-between">
<text class="kv-k">涉及行政村</text>
<text class="kv-v">{{ d.villageCount }} 个</text>
</view>
<view class="kv-row flex-row align-center flex-between">
<text class="kv-k">已摸排数</text>
<text class="kv-v">{{ d.surveyedCount }} 户</text>
</view>
<view class="kv-row flex-row align-center flex-between">
<text class="kv-k">已授信数</text>
<text class="kv-v">{{ d.creditedCount }} 户</text>
</view>
<view class="kv-row flex-row align-center flex-between">
<text class="kv-k">完成率</text>
<text class="kv-v">{{ d.completionPercent }}%</text>
</view>
<view class="kv-row kv-row--last flex-row align-center flex-between">
<text class="kv-k">状态</text>
<text class="kv-v kv-v--tag">{{ d.statusText }}</text>
</view>
</view>

<view class="block-card">
<text class="block-title">行政村列表</text>
<view v-for="(v, i) in d.villages" :key="'vil-' + i + '-' + v.id"
class="cell-item flex-column align-stretch" @click="onVillageRowByIndex(i)">
<view class="flex-row align-center flex-between">
<text class="cell-name cell-name--credit">{{ v.name }}</text>
<text class="cell-progress">{{ v.progressText }}</text>
</view>
<view class="progress-wrap" v-if="v.surveyedCount > 0">
<u-line-progress :percentage="v.progressPercent" :showText="false" height="6"
activeColor="#2979ff" />
</view>
<text class="cell-meta">摸排 {{ v.surveyedCount }} 户 · 授信 {{ v.creditedCount }} 户</text>
</view>
<no-data v-if="!d.villages || d.villages.length === 0" text="暂无行政村" :show-image="false"
padding="32rpx 0 8rpx" />
</view>
</view>
</template>

<script>
import { getCreditbatch, stats } from '@/api/mobile/creditbatch'
import { listSurvey } from '@/api/mobile/survey'

/** 批次状态:0=未启动 1=运行中 2=已暂停 3=已结束 */
const STATUS_NOT_STARTED = 0
const STATUS_PAUSED = 2
const STATUS_FINISHED = 3

function statusTextOf(s) {
const n = Number(s)
if (n === STATUS_NOT_STARTED) {
return '未启动'
}
if (n === STATUS_PAUSED) {
return '已暂停'
}
if (n === STATUS_FINISHED) {
return '已结束'
}
return '运行中'
}

function formatTime(t) {
if (!t) return '—'
return String(t).substring(0, 16)
}

/** 解析 villageNames 字符串为行政村列表 */
function parseVillages(names, ids) {
if (!names) return []
const nameArr = String(names).split(/[,,、]/).filter(n => n.trim())
const idArr = Array.isArray(ids) ? ids : []
return nameArr.map((name, i) => ({
id: idArr[i] != null ? String(idArr[i]) : '',
name: name.trim()
}))
}

export default {
data() {
return {
batchId: '',
d: {},
loading: false
}
},
onLoad(options) {
const id = options && options.id !== undefined ? String(options.id) : ''
this.batchId = id
this.loadDetail(id)
},
methods: {
async loadDetail(id) {
if (!id) return
this.loading = true
uni.showLoading({ title: '加载中...', mask: true })
try {
// 并发请求批次详情、统计信息、摸排记录
const [batchRes, statsRes, surveyRes] = await Promise.all([
getCreditbatch(id),
stats({ id: id }),
listSurvey({ batchId: id, pageSize: 9999 }).catch(() => ({ rows: [] }))
])
const batch = batchRes.data || {}
const s = statsRes.data || {}
const surveys = surveyRes.rows || []
const surveyCount = s.surveyCount || 0
const approvedCount = s.approvedCount || 0
const villageCount = s.villageCount || (Array.isArray(batch.villageIds) ? batch.villageIds.length : 0)
const completionPercent = surveyCount > 0
? Math.round(approvedCount / surveyCount * 100)
: 0

// 按行政村分组摸排记录,计算每个村的进度
const surveyByVillage = {}
surveys.forEach(sv => {
const vid = sv.villageId ? String(sv.villageId) : ''
if (!vid) return
if (!surveyByVillage[vid]) surveyByVillage[vid] = []
surveyByVillage[vid].push(sv)
})

// 解析行政村列表并附加进度
const baseVillages = parseVillages(batch.villageNames, batch.villageIds)
const villages = baseVillages.map(v => {
const villageSurveys = surveyByVillage[v.id] || []
const surveyed = villageSurveys.length
const credited = villageSurveys.filter(sv => sv.creditStatus != null && Number(sv.creditStatus) > 0).length
const progressPercent = surveyed > 0 ? Math.round(credited / surveyed * 100) : 0
return {
...v,
surveyedCount: surveyed,
creditedCount: credited,
progressPercent,
progressText: surveyed > 0 ? `${progressPercent}%` : '未摸排'
}
})

this.d = {
batchName: batch.batchName || '—',
startAt: formatTime(batch.startTime),
endAt: formatTime(batch.endTime),
villageCount: villageCount,
surveyedCount: surveyCount,
creditedCount: approvedCount,
completionPercent: completionPercent,
statusText: statusTextOf(batch.batchStatus),
villages: villages
}
const title = this.d && this.d.batchName ? this.d.batchName : '批次详情'
uni.setNavigationBarTitle({ title })
} catch (e) {
uni.showToast({ title: '加载详情失败', icon: 'none' })
this.d = {}
} finally {
this.loading = false
uni.hideLoading()
}
},
onVillageRowByIndex(i) {
const list = (this.d && this.d.villages) || []
const v = list[i]
const vid = v && v.id !== undefined && v.id !== null ? String(v.id) : ''
if (!vid) {
return
}
uni.navigateTo({
url: `/village-special/admin-village-detail?id=${encodeURIComponent(vid)}`
})
}
}
}
</script>

admin-village-detail总户数,未摸排数,没有数据吗

2026-07-09

移动端行政村做客户经理筛选时,有两个客户经理的就不显示了
if (mg && mg !== '全部' && r.managerName !== mg) return false—>
if (mg && mg !== '全部' && !(r.managerName || '').split('、').includes(mg)) return false

审核管理页面批量派发时,派发整村授信任务时对于已经完成授信的,不进行派发
更改 TFarmerMapper.java TFarmerMapper.xml MarketingTaskServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- 按网点ID查询未完成授信的客户列表(用于整村授信任务派发) -->
<select id="selectFarmerByOrgIdNotCredited" resultMap="FarmerResultMap">
<include refid="selectFarmerVo"/>
WHERE t.deleted = 0 AND t.org_id = #{orgId}
AND (t.credit_village_status IS NULL OR t.credit_village_status NOT IN (2, 3, 4))
</select>

<!-- 按客户经理ID查询未完成授信的客户列表(用于整村授信任务派发) -->
<select id="selectFarmerByManagerIdNotCredited" resultMap="FarmerResultMap">
<include refid="selectFarmerVo"/>
WHERE t.deleted = 0 AND t.manager_id = #{managerId}
AND (t.credit_village_status IS NULL OR t.credit_village_status NOT IN (2, 3, 4))
</select>

<!-- 按行政村ID查询未完成授信的客户列表(用于整村授信任务派发) -->
<select id="selectFarmerByVillageIdNotCredited" resultMap="FarmerResultMap">
<include refid="selectFarmerVo"/>
WHERE t.deleted = 0 AND t.village_id = #{villageId}
AND (t.credit_village_status IS NULL OR t.credit_village_status NOT IN (2, 3, 4))
</select>

<!-- 按标签ID查询未完成授信的客户列表(用于整村授信任务派发) -->
<select id="selectFarmerByTagIdNotCredited" resultMap="FarmerResultMap">
<include refid="selectFarmerVo"/>
WHERE t.deleted = 0 AND t.id IN (
SELECT ft.farmer_id FROM t_farmer_tag ft WHERE ft.deleted = 0
AND ft.tag_id = #{tagId}
)
AND (t.credit_village_status IS NULL OR t.credit_village_status NOT IN (2, 3, 4))
</select>
1
2
3
4
5
6
7
8
9
10
11
// 按网点ID查询未完成授信的客户列表(用于整村授信任务派发)
List<TFarmer> selectFarmerByOrgIdNotCredited(@Param("orgId") Long orgId);

// 按客户经理ID查询未完成授信的客户列表(用于整村授信任务派发)
List<TFarmer> selectFarmerByManagerIdNotCredited(@Param("managerId") Long managerId);

// 按行政村ID查询未完成授信的客户列表(用于整村授信任务派发)
List<TFarmer> selectFarmerByVillageIdNotCredited(@Param("villageId") Long villageId);

// 按标签ID查询未完成授信的客户列表(用于整村授信任务派发)
List<TFarmer> selectFarmerByTagIdNotCredited(@Param("tagId") Long tagId);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 判断是否为整村授信任务(任务类型为5:整村授信摸排,6:整村授信审核)
boolean isCreditTask = "5".equals(template.getTaskType()) || "6".equals(template.getTaskType());

if ("org".equals(dispatchType)) {
// 按网点派发
Long orgId = Long.parseLong(dispatchTarget);
if (isCreditTask) {
farmerList = tFarmerMapper.selectFarmerByOrgIdNotCredited(orgId);
} else {
farmerList = tFarmerMapper.selectFarmerByOrgId(orgId);
}
} else if ("manager".equals(dispatchType)) {
// 按客户经理派发
Long managerId = Long.parseLong(dispatchTarget);
if (isCreditTask) {
farmerList = tFarmerMapper.selectFarmerByManagerIdNotCredited(managerId);
} else {
farmerList = tFarmerMapper.selectFarmerByManagerId(managerId);
}
} else if ("village".equals(dispatchType)) {
// 按行政村派发
Long villageId = Long.parseLong(dispatchTarget);
if (isCreditTask) {
farmerList = tFarmerMapper.selectFarmerByVillageIdNotCredited(villageId);
} else {
farmerList = tFarmerMapper.selectFarmerByVillageId(villageId);
}
} else if ("tag".equals(dispatchType)) {
// 按客户群标签派发
Long tagId = Long.parseLong(dispatchTarget);
if (isCreditTask) {
farmerList = tFarmerMapper.selectFarmerByTagIdNotCredited(tagId);
} else {
farmerList = tFarmerMapper.selectFarmerByTagId(tagId);
}
}

if (farmerList.isEmpty()) {
if (isCreditTask) {
throw new RuntimeException("该派发维度下没有未完成授信的客户");
} else {
throw new RuntimeException("该派发维度下没有可派发的客户");
}
}

角色编号 角色名称 权限字符
2005 总行领导 zhadmin
2006 总行各部门 zhgbm
2007 支行管理员 zhz
2008 客户经理 khjl
2009 运维人员 ywry

2026-7-10

• 电话沟通记录新增接口
• 电话沟通记录查询接口
• 电话沟通记录编辑接口
• 企微电话时长同步接口
• 沟通时长统计接口
• 沟通时长异常监控接口

拨打电话 腾讯云联络中心(TCCC)+ 小程序跳转
记录存储 企业微信会话存档功能(企业版) 需付费开通,需二次开发解析数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
-- ============================================================
-- 营销任务类型表
-- 将硬编码的任务类型(0-6)迁移为独立表,便于扩展
-- ============================================================

-- 创建营销任务类型表
CREATE TABLE IF NOT EXISTS `marketing_task_type` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`type_code` VARCHAR(20) NOT NULL COMMENT '类型编码(唯一标识)',
`type_name` VARCHAR(50) NOT NULL COMMENT '类型名称',
`sort_order` INT(4) DEFAULT 0 COMMENT '排序号',
`status` CHAR(1) DEFAULT '0' COMMENT '状态(0启用 1停用)',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_type_code` (`type_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营销任务类型表';

-- 初始化任务类型数据(迁移原有的硬编码类型)
INSERT INTO `marketing_task_type` (`type_code`, `type_name`, `sort_order`, `status`, `create_by`, `create_time`) VALUES
('NEW_CUSTOMER', '新客拓展', 1, '0', 'admin', NOW()),
('DEPOSIT_UPGRADE', '存款提升', 2, '0', 'admin', NOW()),
('LOAN_MARKETING', '贷款营销', 3, '0', 'admin', NOW()),
('EXPIRATION_CONVERT', '到期转化', 4, '0', 'admin', NOW()),
('VISIT', '走访', 5, '0', 'admin', NOW()),
('VILLAGE_CREDIT_SURVEY', '整村授信摸排', 6, '0', 'admin', NOW()),
('VILLAGE_CREDIT_AUDIT', '整村授信审核', 7, '0', 'admin', NOW());

-- ============================================================
-- 迁移现有数据:将 marketing_task 表的 task_type 字段改为 type_id
-- ============================================================

-- 1. 添加临时字段 type_id
ALTER TABLE `marketing_task` ADD COLUMN `type_id` BIGINT DEFAULT NULL COMMENT '任务类型ID' AFTER `task_type`;

-- 2. 迁移数据:将旧的 task_type 值映射到新的 type_id
UPDATE `marketing_task` mt
JOIN `marketing_task_type` mtt ON (
(mt.task_type = '0' AND mtt.type_code = 'NEW_CUSTOMER') OR
(mt.task_type = '1' AND mtt.type_code = 'DEPOSIT_UPGRADE') OR
(mt.task_type = '2' AND mtt.type_code = 'LOAN_MARKETING') OR
(mt.task_type = '3' AND mtt.type_code = 'EXPIRATION_CONVERT') OR
(mt.task_type = '4' AND mtt.type_code = 'VISIT') OR
(mt.task_type = '5' AND mtt.type_code = 'VILLAGE_CREDIT_SURVEY') OR
(mt.task_type = '6' AND mtt.type_code = 'VILLAGE_CREDIT_AUDIT')
)
SET mt.type_id = mtt.id;

-- 3. 添加外键约束
ALTER TABLE `marketing_task` ADD CONSTRAINT `fk_marketing_task_type` FOREIGN KEY (`type_id`) REFERENCES `marketing_task_type`(`id`);

-- ============================================================
-- 迁移现有数据:将 marketing_task_template 表的 task_type 字段改为 type_id
-- ============================================================

-- 1. 添加临时字段 type_id
ALTER TABLE `marketing_task_template` ADD COLUMN `type_id` BIGINT DEFAULT NULL COMMENT '任务类型ID' AFTER `task_type`;

-- 2. 迁移数据:将旧的 task_type 值映射到新的 type_id
UPDATE `marketing_task_template` mtt
JOIN `marketing_task_type` mttt ON (
(mtt.task_type = '0' AND mttt.type_code = 'NEW_CUSTOMER') OR
(mtt.task_type = '1' AND mttt.type_code = 'DEPOSIT_UPGRADE') OR
(mtt.task_type = '2' AND mttt.type_code = 'LOAN_MARKETING') OR
(mtt.task_type = '3' AND mttt.type_code = 'EXPIRATION_CONVERT') OR
(mtt.task_type = '4' AND mttt.type_code = 'VISIT') OR
(mtt.task_type = '5' AND mttt.type_code = 'VILLAGE_CREDIT_SURVEY') OR
(mtt.task_type = '6' AND mttt.type_code = 'VILLAGE_CREDIT_AUDIT')
)
SET mtt.type_id = mttt.id;

-- 3. 添加外键约束
ALTER TABLE `marketing_task_template` ADD CONSTRAINT `fk_marketing_task_template_type` FOREIGN KEY (`type_id`) REFERENCES `marketing_task_type`(`id`);

E:\数意通\CMMW\ruoyi-customer-mobile\src\main\java\com\ruoyi\mobile\controller\VillageCreditSurveyAuditController.java:134:17
java: 找不到符号
符号: 方法 setTaskType(java.lang.String)
位置: 类型为com.ruoyi.mobile.domain.MarketingTask的变量 task

E:\数意通\CMMW\ruoyi-customer-mobile\src\main\java\com\ruoyi\mobile\service\impl\MarketingVisitPlanServiceImpl.java:67:48
java: 找不到符号
符号: 方法 getTaskType()
位置: 类型为com.ruoyi.mobile.domain.MarketingTask的变量 task

E:\数意通\CMMW\ruoyi-customer-mobile\src\main\java\com\ruoyi\mobile\service\impl\MarketingExecRecordServiceImpl.java:68:48
java: 找不到符号
符号: 方法 getTaskType()
位置: 类型为com.ruoyi.mobile.domain.MarketingTask的变量 task

E:\数意通\CMMW\ruoyi-customer-mobile\src\main\java\com\ruoyi\mobile\service\impl\VillageCreditSurveyServiceImpl.java:66:17
java: 找不到符号
符号: 方法 setTaskType(java.lang.String)
位置: 类型为com.ruoyi.mobile.domain.MarketingTask的变量 task

增加营销任务ID与输入框的距离
走访记录的搜索过滤应该与电话沟通记录一样,选择客户和客户经理

列表的间隔距离与电话沟通记录一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- ============================================================
-- 营销任务客户来源配置表
-- 用于配置任务派发时的客户来源维度,实现可扩展
-- ============================================================

CREATE TABLE IF NOT EXISTS `marketing_customer_source` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`source_code` VARCHAR(30) NOT NULL COMMENT '来源编码(唯一标识,如org/manager/village/tag)',
`source_name` VARCHAR(50) NOT NULL COMMENT '来源名称(如按网点/按客户经理/按行政村/按客户群标签)',
`source_desc` VARCHAR(500) DEFAULT NULL COMMENT '来源描述',
`sort_order` INT(4) DEFAULT 0 COMMENT '排序号',
`status` CHAR(1) DEFAULT '0' COMMENT '状态(0启用 1停用)',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_source_code` (`source_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营销任务客户来源配置表';

-- 初始化客户来源数据(迁移原有的硬编码来源)
INSERT INTO `marketing_customer_source` (`source_code`, `source_name`, `source_desc`, `sort_order`, `status`, `create_by`, `create_time`) VALUES
('org', '按网点', '按网点维度派发任务,查询该网点下的所有客户', 1, '0', 'admin', NOW()),
('manager', '按客户经理', '按客户经理维度派发任务,查询该客户经理名下的客户', 2, '0', 'admin', NOW()),
('village', '按行政村', '按行政村维度派发任务,查询该行政村下的客户', 3, '0', 'admin', NOW()),
('tag', '按客户群标签', '按客户群标签维度派发任务,查询带有指定标签的客户', 4, '0', 'admin', NOW());

客户经理是表sys_user里面的并用关联表 sys_user_role 关联 sys_role 角色表 role_id 为 2008

2026-07-11

http://localhost/marketing/task-template 在此页面操作栏添加批量派发按钮,
http://localhost/marketing/task-audit 页面的批量派发按钮不用选择任务模板

2026-07-13

获取客户经理列表(按网点):

1
2
3
4
SysUser query = new SysUser();
query.setRoleId(2008L); // 客户经理角色ID
query.setDeptId(orgId);
managerList = sysUserService.selectCustomerList(query);

轮询(Round-Robin)平均分配给该网点下所有客户经理

支行管理员按网点派发时可以选择所有支行,我希望可以直接自动获取其所在支行不能选择其他支行。
支行管理员只能指定本支行下的客户经理进行派发任务等等

注意:bank_organization是真正的机构表
与sys_user的org_id字段关联,dept_id是废弃的字段

审核管理选择任务类型时还是原来的样子,不是当前的方法

客户经理(role_id = 2008 role_key = khjl)
应该只能看到自己的营销任务,任务电话沟通记录和走访记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
package com.ruoyi.mobile.service.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.mobile.domain.MarketingTask;
import com.ruoyi.mobile.domain.MarketingTaskTemplate;
import com.ruoyi.mobile.domain.TFarmer;
import com.ruoyi.mobile.mapper.MarketingTaskMapper;
import com.ruoyi.mobile.mapper.MarketingTaskTemplateMapper;
import com.ruoyi.mobile.mapper.TFarmerMapper;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.mobile.service.IMarketingTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* 营销任务主Service业务层处理
*
* @author ruoyi
* @date 2026-06-29
*/
@Service
public class MarketingTaskServiceImpl implements IMarketingTaskService
{
@Autowired
private MarketingTaskMapper marketingTaskMapper;

@Autowired
private MarketingTaskTemplateMapper marketingTaskTemplateMapper;

@Autowired
private TFarmerMapper tFarmerMapper;

@Autowired
private ISysUserService sysUserService;

/**
* 查询营销任务主
*
* @param id 营销任务主主键
* @return 营销任务主
*/
@Override
public MarketingTask selectMarketingTaskById(Long id)
{
return marketingTaskMapper.selectMarketingTaskById(id);
}

/**
* 查询营销任务主列表
*
* @param marketingTask 营销任务主
* @return 营销任务主
*/
@Override
public List<MarketingTask> selectMarketingTaskList(MarketingTask marketingTask)
{
applyBranchDataScope(marketingTask);
return marketingTaskMapper.selectMarketingTaskList(marketingTask);
}

/**
* 新增营销任务主
*
* @param marketingTask 营销任务主
* @return 结果
*/
@Override
public int insertMarketingTask(MarketingTask marketingTask)
{
marketingTask.setCreateTime(DateUtils.getNowDate());
marketingTask.setCreateBy(SecurityUtils.getUsername());
// 移动端新增任务默认为自己创建
marketingTask.setSourceType("self");
return marketingTaskMapper.insertMarketingTask(marketingTask);
}

/**
* 修改营销任务主
*
* @param marketingTask 营销任务主
* @return 结果
*/
@Override
public int updateMarketingTask(MarketingTask marketingTask)
{
marketingTask.setUpdateTime(DateUtils.getNowDate());
return marketingTaskMapper.updateMarketingTask(marketingTask);
}

@Override
public int saveAudit(MarketingTask marketingTask)
{
marketingTask.setAuditBy(SecurityUtils.getUsername());
marketingTask.setAuditTime(DateUtils.getNowDate());
if ("pass".equals(marketingTask.getAuditResult())) {
marketingTask.setAuditStatus("1");
} else {
marketingTask.setAuditStatus("2");
}
return marketingTaskMapper.updateMarketingTaskAudit(marketingTask);
}

@Override
public int batchDispatch(MarketingTask marketingTask)
{
// 1. 查询任务模板获取任务类型和时长要求
Long templateId = marketingTask.getTemplateId();
MarketingTaskTemplate template = marketingTaskTemplateMapper.selectMarketingTaskTemplateById(templateId);
if (template == null) {
throw new RuntimeException("任务模板不存在");
}

// 2. 根据派发维度查询对应的客户列表
List<TFarmer> farmerList = new ArrayList<>();
String dispatchType = marketingTask.getDispatchType();
String dispatchTarget = marketingTask.getDispatchTarget();
Long orgId = null;

// 判断当前用户是否为支行管理员
boolean isBranchAdmin = isBranchAdmin();

if ("org".equals(dispatchType)) {
if (isBranchAdmin) {
// 支行管理员按网点派发时,强制使用其所在支行,不允许选择其他支行
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUser() != null) {
orgId = Long.valueOf(loginUser.getUser().getOrgId());
System.out.println("当前用户所在部门ID:" + orgId);
if (orgId == null) {
throw new RuntimeException("当前用户未分配部门");
}
} else {
throw new RuntimeException("获取当前用户信息失败");
}
} else {
orgId = Long.parseLong(dispatchTarget);
}
farmerList = tFarmerMapper.selectFarmerByOrgId(orgId);
} else if ("manager".equals(dispatchType)) {
Long managerId = Long.parseLong(dispatchTarget);
if (isBranchAdmin) {
SysUser manager = sysUserService.selectUserById(managerId);
if (manager == null) {
throw new RuntimeException("客户经理不存在");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
String userOrgId = loginUser.getUser().getOrgId();
if (manager.getOrgId() == null || !manager.getOrgId().equals(userOrgId)) {
throw new RuntimeException("只能派发任务给自己支行下的客户经理");
}
}
farmerList = tFarmerMapper.selectFarmerByManagerId(managerId);
} else if ("village".equals(dispatchType)) {
Long villageId = Long.parseLong(dispatchTarget);
farmerList = tFarmerMapper.selectFarmerByVillageId(villageId);
} else if ("tag".equals(dispatchType)) {
Long tagId = Long.parseLong(dispatchTarget);
farmerList = tFarmerMapper.selectFarmerByTagId(tagId);
}

if (farmerList.isEmpty()) {
throw new RuntimeException("该派发维度下没有可派发的客户");
}

// 3. 获取客户经理列表(按网点派发或需要平均分配时)
List<SysUser> managerList = new ArrayList<>();
if ("org".equals(dispatchType)) {
SysUser query = new SysUser();
query.setRoleId(2008L);
query.setDeptId(orgId);
managerList = sysUserService.selectCustomerList(query);
}

// 4. 批量生成营销任务
List<MarketingTask> taskList = new ArrayList<>();
int managerIndex = 0;
int managerCount = managerList.size();

for (TFarmer farmer : farmerList) {
MarketingTask task = new MarketingTask();
task.setTaskName(marketingTask.getTaskName());
task.setTypeId(template.getTypeId());
task.setTemplateId(templateId);
task.setCustomerId(farmer.getId());
task.setCustomerName(farmer.getCustomerName());

Long assignedManagerId;
String assignedManagerName;

if ("org".equals(dispatchType) && managerCount > 0) {
SysUser manager = managerList.get(managerIndex % managerCount);
assignedManagerId = manager.getUserId();
assignedManagerName = manager.getNickName();
managerIndex++;
} else if ("manager".equals(dispatchType)) {
assignedManagerId = Long.parseLong(dispatchTarget);
SysUser manager = sysUserService.selectUserById(assignedManagerId);
assignedManagerName = manager != null ? manager.getNickName() : null;
} else {
assignedManagerId = farmer.getManagerId();
assignedManagerName = farmer.getManagerName();
}

task.setManagerId(assignedManagerId);
task.setManagerName(assignedManagerName);
task.setVillageId(farmer.getVillageId() != null ? farmer.getVillageId().toString() : null);
task.setVillageName(farmer.getVillageName());
task.setOrgId(farmer.getOrgId() != null ? farmer.getOrgId() : orgId);
task.setOrgName(farmer.getOrgName());
task.setRequireDuration(template.getRequireDuration());
task.setTaskStatus("0");
task.setAuditStatus("0");
task.setDeadlineTime(template.getEndTime());
task.setDispatchType(dispatchType);
task.setDispatchTarget(dispatchTarget);
task.setCreateTime(DateUtils.getNowDate());
task.setCreateBy(SecurityUtils.getUsername());
task.setSourceType(determineSourceType());
taskList.add(task);
}

return marketingTaskMapper.batchInsertMarketingTask(taskList);
}

/**
* 判断当前用户是否为支行管理员
* @return true-支行管理员
*/
private boolean isBranchAdmin() {
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUser() != null) {
List<SysRole> roles = loginUser.getUser().getRoles();
if (roles != null) {
for (SysRole role : roles) {
// 支行管理员角色ID=2007,role_key="zhz"
if (role.getRoleId() != null && role.getRoleId().equals(2007L)) {
return true;
}
if ("zhz".equals(role.getRoleKey())) {
return true;
}
}
}
}
} catch (Exception e) {
// 忽略异常,返回false
}
return false;
}

/**
* 判断当前用户是否为客户经理(role_id=2008 或 role_key="khjl")
*/
private boolean isCustomerManager() {
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUser() != null) {
List<SysRole> roles = loginUser.getUser().getRoles();
if (roles != null && !roles.isEmpty()) {
for (SysRole role : roles) {
if (role.getRoleId() != null && role.getRoleId().equals(2008L)) {
return true;
}
if ("khjl".equals(role.getRoleKey())) {
return true;
}
}
}
}
} catch (Exception e) {
// 忽略异常,返回false
}
return false;
}

/**
* 应用数据权限过滤
* 支行管理员:只能查看自己支行下的营销任务
* 客户经理:只能查看自己的营销任务
*/
private void applyBranchDataScope(MarketingTask marketingTask) {
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUser() != null) {
if (isBranchAdmin()) {
String orgIdStr = loginUser.getUser().getOrgId();
if (orgIdStr != null && !orgIdStr.isEmpty()) {
marketingTask.setOrgId(Long.parseLong(orgIdStr));
}
} else if (isCustomerManager()) {
marketingTask.setManagerId(loginUser.getUser().getUserId());
}
}
} catch (Exception e) {
// 忽略异常
}
}
}

/**
* 根据当前登录用户的角色判定任务来源
* @return system/branch/grid
*/
private String determineSourceType()
{
try
{
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUser() != null)
{
List<SysRole> roles = loginUser.getUser().getRoles();
if (roles != null)
{
for (SysRole role : roles)
{
String roleName = role.getRoleName();
if (roleName != null && roleName.contains("支行"))
{
return "branch";
}
if (roleName != null && roleName.contains("网格"))
{
return "grid";
}
}
}
}
}
catch (Exception e)
{
// 忽略异常,返回默认值
}
return "system";
}

/**
* 批量删除营销任务主
*
* @param ids 需要删除的营销任务主主键
* @return 结果
*/
@Override
public int deleteMarketingTaskByIds(Long[] ids)
{
return marketingTaskMapper.deleteMarketingTaskByIds(ids);
}

/**
* 删除营销任务主信息
*
* @param id 营销任务主主键
* @return 结果
*/
@Override
public int deleteMarketingTaskById(Long id)
{
return marketingTaskMapper.deleteMarketingTaskById(id);
}

/**
* 任务数据统计
*/
@Override
public Map<String, Object> getTaskStatistics(Map<String, Object> params)
{
applyBranchDataScopeToParams(params);
Object scope = params != null ? params.get("scope") : null;
if ("mine".equals(String.valueOf(scope)) && params.get("managerId") == null) {
try {
Long userId = SecurityUtils.getUserId();
if (userId != null) {
params.put("managerId", userId);
}
} catch (Exception e) {
// 获取用户失败时忽略
}
}
return marketingTaskMapper.selectTaskStatistics(params);
}

/**
* 电话沟通时长达标情况(按客户经理统计)
*/
@Override
public List<Map<String, Object>> getDurationProgress(Map<String, Object> params)
{
applyBranchDataScopeToParams(params);
return marketingTaskMapper.selectDurationProgress(params);
}

/**
* 整村授信任务推进情况(按行政村统计)
*/
@Override
public List<Map<String, Object>> getVillageProgress(Map<String, Object> params)
{
applyBranchDataScopeToParams(params);
return marketingTaskMapper.selectVillageProgress(params);
}

private void applyBranchDataScopeToParams(Map<String, Object> params) {
if (params != null) {
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUser() != null) {
if (isBranchAdmin() && params.get("orgId") == null) {
String orgIdStr = loginUser.getUser().getOrgId();
if (orgIdStr != null && !orgIdStr.isEmpty()) {
params.put("orgId", Long.parseLong(orgIdStr));
}
} else if (isCustomerManager() && params.get("managerId") == null) {
params.put("managerId", loginUser.getUser().getUserId());
}
}
} catch (Exception e) {
// 忽略异常
}
}
}

/**
* 更新任务状态
* @param id
* @param taskStatus
* @return
*/
@Override
public int updateTaskStatus(Long id, String taskStatus)
{
MarketingTask task = marketingTaskMapper.selectMarketingTaskById(id);
if (task == null) {
return 0;
}
String currentStatus = task.getTaskStatus();
if ("2".equals(currentStatus) || "3".equals(currentStatus)) {
return 0;
}
return marketingTaskMapper.updateTaskStatus(id, taskStatus);
}

@Override
public int updateOverdueTaskStatus()
{
return marketingTaskMapper.updateOverdueTaskStatus();
}

@Override
public int updateTaskStatusDirect(Long id, String taskStatus)
{
return marketingTaskMapper.updateTaskStatus(id, taskStatus);
}

@Override
public int updateRealDurationAndCheckComplete(Long id, Long realDuration)
{
MarketingTask task = marketingTaskMapper.selectMarketingTaskById(id);
if (task == null) {
return 0;
}
marketingTaskMapper.updateRealDuration(id, realDuration);
Long requireDuration = task.getRequireDuration();
if (requireDuration != null && realDuration != null && realDuration >= requireDuration) {
return marketingTaskMapper.updateTaskStatus(id, "2");
}
return 1;
}
}

  • 支行长按行政村派发可以派发任务到不在自己辖区的其他村子里,派发到其他支行的客户经理上,按客户群标签派发也有这个问题:

给客户添加一个字段,记录客户经理的所属支行ID:private Long tagId;
客户Mapper的按ID查询客户列表最下面添加一行代码:
<if test="orgId != null"> AND t.org_id = #{orgId}</if>
添加逻辑:

1
2
3
4
5
6
7
8
9
10
if (isBranchAdmin) {
LoginUser loginUser = SecurityUtils.getLoginUser();
String orgIdStr = loginUser.getUser().getOrgId();
if (orgIdStr != null && !orgIdStr.isEmpty()) {
orgId = Long.parseLong(orgIdStr);
}
}
TFarmer farmerQuery = new TFarmer();
farmerQuery.setVillageId(villageId);
farmerQuery.setOrgId(orgId);

Mapper接口的
方法签名改为接收 TFarmer 对象

2026-07-14


角色编号 角色名称 权限字符
2005 总行领导 zhadmin
2006 总行各部门 zhgbm
2007 支行管理员 zhz
2008 客户经理 khjl
2009 运维人员 ywry

数据看板与报表

整村授信数据看板

组件路径 mobile/report/VillageCreditBoard
权限字符 mobile:report:villageCredit:list

客户营销数据看板

组件路径 mobile/report/CustomerMarketingBoard
权限字符 mobile:report:marketing:list

员工业绩报表(报表可导出)

组件路径 mobile/report/ManagerPerfReport
权限字符 mobile:report:manager:list
需求:也就是统计客户经理的业绩,包括客户管户统计,整村授信任务和营销任务完成情况,走访统计,电话沟通统计等等。
对于总行,可查看所有下辖客户经理的业绩(月度,季度,年度)。
对于支行管理员,可以查看本支行业绩报表(月度,季度,年度)。
对于客户经理,可以查看自己的业绩报表(月度,季度,年度)。

网点月度/季度汇总报表(报表可导出)

组件路径 mobile/report/OrgPeriodSummaryReport
权限字符 mobile:report:orgSummary:list
需求:对于管理员,可以统计所有网点的月度/季度汇总;对于支行管理员,可以统计本支行(网点)的月度/季度汇总报表;

t_farmer_visit已经废弃,使用marketing_exec_record
当前电话沟通表,帮我添加create_time,update_time,del_flag:

1
2
3
4
5
6
7
8
9
10
11
DROP TABLE IF EXISTS `t_farmer_call`;
CREATE TABLE `t_farmer_call` (
`id` bigint NOT NULL AUTO_INCREMENT,
`farmer_id` bigint NOT NULL,
`call_time` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '沟通时间',
`duration_min` int NOT NULL COMMENT '沟通时长分钟',
`summary` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '沟通摘要',
`call_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '沟通类型',
`deleted` tinyint NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '农户电话沟通记录' ROW_FORMAT = Dynamic;
1
2
3
4
ALTER TABLE `t_farmer_call`
ADD COLUMN `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
ADD COLUMN `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
ADD COLUMN `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标记(0正常,1删除)';

表 t_farmer_call 中并不存在名为 manager_id 的列,关联的t_farmer表有manager_id列

sys_dept表已废弃,使用bank_organization表作为网点/支行表

管户总数
有效客户数
累计走访次数
任务完成率

选择所属支行只能选择总行
继续完善代码,实现还未实现的功能
都没有数据,看板还有一些是假数据

选择客户经理时无数据

数据看板与报表模块没有显示数据的原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
Request URL
http://192.168.0.163/dev-api/mobile/report/villageCreditBoard?pageNum=1&pageSize=10&periodType=month&periodValue=2026-07
Request Method
GET
Status Code
200 OK
Remote Address
192.168.0.163:80
Referrer Policy
strict-origin-when-cross-origin
{
"msg": "操作成功",
"code": 200,
"data": {
"boardData": {
"creditedHousehold": 0,
"totalCreditAmount": 0,
"totalHousehold": 0,
"totalVillage": 0
},
"creditAmountDist": [],
"creditRateRank": [],
"creditTrend": {
"creditAmount": null,
"creditNum": null,
"months": null
},
"total": 0,
"villageList": []
}
}

Request URL
http://192.168.0.163/dev-api/mobile/report/customerMarketingBoard?pageNum=1&pageSize=10&periodType=month&periodValue=2026-07
Request Method
GET
Status Code
200 OK
Remote Address
192.168.0.163:80
Referrer Policy
strict-origin-when-cross-origin
{
"msg": "操作成功",
"code": 200,
"data": {
"boardData": {
"completedTask": 0,
"totalCall": 0,
"totalTask": 0,
"totalVisit": 0
},
"managerList": [
{
"nickName": "赵行长",
"userId": 10003
},
{
"nickName": "杨杨杨",
"userId": 10005
},
{
"nickName": "王",
"userId": 10006
},
{
"nickName": "孙",
"userId": 10007
},
{
"nickName": "周",
"userId": 10008
},
{
"nickName": "吴",
"userId": 10009
},
{
"nickName": "郑",
"userId": 10010
},
{
"nickName": "钱",
"userId": 10011
},
{
"nickName": "真太谷明星支行客户经理",
"userId": 10012
},
{
"nickName": "明星客户经理2",
"userId": 10014
}
],
"marketingTrend": {
"callNum": null,
"months": null,
"visitNum": null
},
"taskList": [],
"taskTypeDist": [],
"total": 0
}
}


Request URL
http://192.168.0.163/dev-api/mobile/report/managerPerf/list?pageNum=1&pageSize=10&periodType=month&periodValue=2026-07
Request Method
GET
Status Code
200 OK
Remote Address
192.168.0.163:80
Referrer Policy
strict-origin-when-cross-origin
{
"msg": "操作成功",
"code": 200,
"data": {
"managerList": [
{
"nickName": "赵行长",
"userId": 10003
},
{
"nickName": "杨杨杨",
"userId": 10005
},
{
"nickName": "王",
"userId": 10006
},
{
"nickName": "孙",
"userId": 10007
},
{
"nickName": "周",
"userId": 10008
},
{
"nickName": "吴",
"userId": 10009
},
{
"nickName": "郑",
"userId": 10010
},
{
"nickName": "钱",
"userId": 10011
},
{
"nickName": "真太谷明星支行客户经理",
"userId": 10012
},
{
"nickName": "明星客户经理2",
"userId": 10014
}
],
"rows": [],
"summary": {
"completedTask": 0,
"managerCount": 0,
"totalCreditAmount": 0,
"totalCustomer": 0
},
"total": 0
}
}

Request URL
http://192.168.0.163/dev-api/mobile/report/orgSummary/list?pageNum=1&pageSize=10&periodType=month&periodValue=2026-07
Request Method
GET
Status Code
200 OK
Remote Address
192.168.0.163:80
Referrer Policy
strict-origin-when-cross-origin
{
"msg": "操作成功",
"code": 200,
"data": {
"rows": [],
"summary": {
"avgCreditAmount": 0,
"orgCount": 0,
"totalCreditAmount": 0,
"totalCreditCount": 0
},
"total": 0
}
}

我真的糊涂了

2026-07-15

Wyz6d@zsxb
D:\qemu\qemu-system-x86_64 -drive file=E:\HikOS-Enterprise-V1.1.0-x86_64-E-server-20230421.qcow2,format=qcow2 -m 4096 -net nic -net user

D:\qemu\qemu-system-x86_64 -drive file=D:\HikOS-Enterprise-V1.1.0-x86_64-E-server-20230421.qcow2,format=qcow2 -m 4096 -nic user,hostfwd=tcp::55555-:55555

D:\qemu\qemu-system-x86_64 -drive file=D:\HikOS-Enterprise-V1.1.0-x86_64-E-server-20230421.qcow2,format=qcow2,cache=writeback -m 4096 -nic user,hostfwd=tcp::55555-:55555

2026-07-16

搞系统,安装平台
安装一下午没成功,8.30才走,7点就到了
因为硬盘的原因,失败了

2026-07-17

完善数据看板与报表

http://localhost/#/report/VillageCreditBoard
授信趋势的授信金额不对

走访数
电话沟通数

1
2
3
4
5
6
7
SELECT COUNT(*) FROM t_farmer_call tfc
LEFT JOIN marketing_task mt ON tfc.task_id = mt.id AND mt.del_flag = '0'
WHERE tfc.deleted = 0 AND mt.org_id = 2001;

SELECT COUNT(*) FROM t_farmer_call tfc
LEFT JOIN t_farmer tf ON tfc.farmer_id = tf.id
WHERE tfc.deleted = 0 AND tf.deleted = 0 AND tf.org_id = 2001;

为数据看板与报表添加对应权限细分

看板与员工业绩报表

对于支行管理员,可以查看所属支行的数据
对于客户经理,可以查看自己的的数据

网点月度/季度汇总

对于管理员,可以统计所有网点的月度/季度汇总;
对于支行管理员,可以统计本支行(网点)的月度/季度汇总报表;

前端支行管理员或客户经理所属支行的下拉选择框直接固定显示所在支行

http://localhost/#/report/VillageCreditBoard
授信率排行(TOP10行政村)在支行管理员或客户经理不可见

除了支行管理员,客户经理,其他角色和总行管理员一样不受限制

“photos”: “["http://localhost:9077/profile/upload/2026/07/10/台风_20260710143831A001.jpg\",\"http://localhost:9077/profile/upload/2026/07/11/2ucKzaVp2FZT812647eaee05755e6cf0eafb1b361b1d_20260711093413A001.jpg\"]“,

数据看板与报表
客户经理所属支行显示的是id,还能选择客户经理
授信率排行(TOP10行政村)和授信趋势没有数据显示

2026-07-18

http://localhost/#/report/CustomerMarketingBoard 客户经理账号直接不显示选择客户经理
http://localhost/#/report/VillageCreditBoard 授信率排行(TOP10行政村)和授信趋势没有数据显示

客户营销管理和数据看板与报表添加权限细分
2005,总行领导,zhadmin只能得到自己下辖的支行的所有数据

2026-07-20

客户营销数据看板

后端修改

  1. MarketingTaskMapper.xml
    selectMarketingTaskList 添加了 mt.del_flag = ‘0’ 条件,排除已删除任务
    新增 typeCode 过滤条件,支持按类型编码筛选
    新增 params.keyword 模糊搜索,支持按客户姓名、手机号、地址、任务名搜索
    添加 order by mt.create_time desc 按创建时间倒序排列
    selectTaskStatistics 新增字段:inProgressCount、overdueCount、customerCount、noPhoneCount、noVisitCount
    统计数据接口新增 typeId、typeCode 过滤条件
  2. MarketingTaskController.java
    list 接口新增 @RequestParam(required = false) String keyword 参数,传递给 MyBatis 的 params.keyword

前端修改
3. api/mobile/task.js
新增 listTaskType() 方法,调用 /marketing/taskType/listAll 动态获取任务类型
4. task-my/index.vue(核心重写)
移除硬编码的 TASK_TYPE_MAP 和 taskTypeSegmentValues
onLoad 时调用 listTaskType() 动态加载任务类型,构建 tabs(第一项为”全部”)
buildTaskQuery() 改为发送 typeId(而非原来的 taskType),匹配后端 MarketingTask.typeId 字段
搜索 keyword 发送给后端处理(而非前端本地过滤)
统计面板改为展示后端返回的 totalTaskCount、pendingCount、completedCount、overdueCount
mapTask() 使用后端返回的 typeName 作为任务类型显示名,typeCode 判断 typeKey(survey/audit/visit)
5. task-my/detail.vue
新增 import { getCustomer } from ‘@/api/mobile/customer’
新增 loadCustomerDetail(customerId) 方法,加载任务后自动调用 /mobile/customer/{id} 获取客户详情
正确映射农户字段到”基本信息”面板(customerNo、householdRelation、gender、age、idCardMasked、mobile、liveAddress 等)
加载失败时降级使用任务自身字段填充

在 MarketingTask.java 中添加了:
private String customerMobile 字段 + getter/setter
private String customerAddress 字段 + getter/setter
toString() 方法中补充这两个字段

客户数 → COUNT(DISTINCT mt.customer_id) as customerCount
未电访 → SUM(CASE WHEN COALESCE(mt.real_duration, 0) = 0 AND mt.task_status != ‘2’ THEN 1 ELSE 0 END) as noPhoneCount
未走访 → SUM(CASE WHEN mt.task_status IN (‘0’, ‘1’) THEN 1 ELSE 0 END) as noVisitCount

1
2
3
4
5
6
// 我的任务角标 = 待跟进任务数
let tasksItem = null
for (const menu of this.menuList) {
tasksItem = menu.children.find((q) => q.id === 'tasks')
if (tasksItem) break
}
1
2
3
4
5
6
7
8
9
// // 入户摸排角标 = 待摸排任务数
const tasksItem = this.menuList[0].children.find((q) => q.id === 'vs_survey_manage')
if (tasksItem) tasksItem.badge = badgeRes.data.unprocessedCount
// 授信审核角标 = 待审核任务数
const auditItem = this.menuList[0].children.find((q) => q.id === 'vs_credit_audit')
if (auditItem) auditItem.badge = badgeRes.data.auditCount
// 客户营销角标 = 待跟进任务数
const followItem = this.menuList[1].children.find((q) => q.id === 'tasks')
if (followItem) followItem.badge = statsRes.data.todoFollow

2026-07-21

已对接后端的功能:
行政村列表:调用 listVillage() 获取数据,过滤 areaType === 3 的行政村
摸排统计:调用 listSurvey() 按村分组计算已摸排/已授信/授信总额/推进进度
客户经理:调用 selectauthKhjl() 获取每村已分配的客户经理

指派客户经理弹窗:调用 selectauthKhjl + selectunauthKhjl 加载已分配/可分配列表,allocKhjl/cancelAllocKhjl 分配/取消
筛选器:乡镇/推进进度/客户经理三个维度,已改用 u-picker 组件
客户经理多值筛选:已修复(split(‘、’).includes(mg))
你手动改动的部分:
“分配客户经理” → “指派客户经理”
筛选从 showActionSheet 改为 u-picker 滚轮选择
loadData() 简化:去掉重复的 listVillage 调用,只请求一次
搜索确认 onSearchConfirm 不再重新请求后端,改为纯前端过滤

listVillage + listSurvey + selectauthKhjl

可以把调/mobile/village/list接口的都改成
/mobile/village/page吗

2026-07-22

将管理端http://localhost/#/report/VillageCreditBoard
http://localhost/#/report/CustomerMarketingBoard
的六个图表复制到移动端的数据看板里,替换调原来的假数据

http://localhost/#/report/CustomerMarketingBoard
的营销趋势(走访&电话沟通)怎么有两个七月

授信趋势的图表两个数据量差距太大了

帮我检查检查派发时还有哪些潜在问题

营销趋势(走访&电话沟通)竖轴要整数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 构建营销趋势柱状图
buildMarketingTrendChart(trend) {
const months = trend.months || []
const visitNum = trend.visitNum || []
const callNum = trend.callNum || []
this.chartMarketingTrend = {
categories: months,
series: [
{ name: '走访客户数', data: visitNum },
{ name: '电话沟通数', data: callNum }
]
}
},

2026-07-23

个人业绩的整村授信统计的数据是个人的吗

http://localhost/#/supervision/call-record
导出不了客户经理的名字

移动端首页的今日数据概览
目前后端可以不用修改直接对接

2026-07-24

AND EXISTS (SELECT 1 FROM t_farmer tfm WHERE tfm.village_id = rga.area_id AND tfm.deleted = 0 AND tfm.manager_id = #{managerId})
AND EXISTS (SELECT 1 FROM t_farmer tfm WHERE tfm.village_id = rga.area_id AND tfm.deleted = 0 AND tfm.manager_id = #{managerId})
删除 <if test="managerId != null">AND tf.manager_id = #{managerId}</if>
GROUP BY rga.area_id, rga.area_name, COALESCE(bo.org_name, bo2.org_name)
GROUP BY rga.area_id, rga.area_name, bo.org_name

2026-07-25

http://localhost/#/report/VillageCreditBoard
查询条件应该还要有批次

行政村授信明细
所属支行应该使用的是村子上面的镇对应的支行

今日客户电话沟通总时长

2026-07-27

http://localhost/#/report/VillageCreditBoard
授信批次 加个默认第一个
以摸排表village_credit_survey为主
上面四个统计也应该随着筛选走

1
2
3
4
5
6
7
8
9
10
11
getBatchList() {
batchDropdown().then(response => {
this.batchList = response.data || []
// 设置默认批次
if (this.batchList.length > 0 && !this.queryParams.batchId) {
this.queryParams.batchId = this.batchList[0].id
}
this.getList()
//
})
},

http://localhost/#/grid/village
添加权限细分
客户经理不能操作,支行长只能操作下辖的行政村
如果既是客户经理又是支行长(支行管理员)的,也可以操作。
为什么我的超级管理员账号,操作栏没有东西

2026-07-28

首页的今日授信概览还没有实现,选择后端有现成的接口可以用吗

2026-07-29

管理端
http://localhost/#/marketing/task-template
模板管理添加权限细分,客户经理添加的模板,只能自己看到,且只能给自己派发任务。
支行长只能看到自己支行下的,客户经理只能看自己的,总行可以看所有下辖的

移动端营销任务电话沟通提交没有绑定任务ID

– 电话沟通记录绑定营销任务ID
ALTER TABLE t_farmer_call ADD COLUMN task_id bigint NULL DEFAULT NULL COMMENT ‘关联营销任务ID’ AFTER farmer_id;

– 按任务维度查询记录的索引
ALTER TABLE t_farmer_call ADD INDEX idx_farmer_call_task_id (task_id);

marketing_task 的id是1199
但是移动端上传后 t_farmer_call 的task_id是1

[task-my] onRow id= 1199 row= {“id”:1199,”typeKey”:”visit”,”typeId”:2,”title”:”去”,”taskType”:”存款提升”,”status”:”待执行”,”taskStatus”:”0”,”source”:”支行下发”,”targetName”:”午”,”_mobile”:”13834552238”,”_address”:”深圳”,”dueShort”:”07-30”,”phoneDone”:”否”}
[task-detail] onLoad options= {“id”:”1199”,”type”:”visit”} taskId= 1199
[phone-records] submit id=

console.log(‘[phone-records] submit taskId=’, this.taskId)

未走访
未电访
这两个数据怎么不变

除了走访,其他所有都是用电话沟通时长更新任务状态的啊

http://localhost/#/report/CustomerMarketingBoard
营销任务明细的客户数改为客户名

累计走访次数
电话沟通时长

移动端数据看板的数据怎么不显示
{“msg”:”操作成功”,”code”:200,”data”:{“callDurationSeconds”:50940,”creditTotal”:0,”creditDone”:0}}
{“msg”:”操作成功”,”code”:200,”data”:{“boardData”:{“completedTask”:2,”totalCall”:2,”totalTask”:9,”totalVisit”:2},”managerList”:[{“nickName”:”rihito”,”userId”:10017},{“nickName”:”刘客户经理”,”userId”:10021},{“nickName”:”张客户经理”,”userId”:10024}],”marketingTrend”:{“callNum”:[0,0,0,0,0,0,2,0,0,0,0,0],”months”:[“1月”,”2月”,”3月”,”4月”,”5月”,”6月”,”7月”,”8月”,”9月”,”10月”,”11月”,”12月”],”visitNum”:[0,0,0,0,0,0,2,0,0,0,0,0]},”taskList”:[{“callNum”:0,”completionRate”:0.0,”customerNames”:”辰”,”managerName”:”rihito”,”orgName”:”榆次郭家堡支行”,”status”:0,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:0},{“callNum”:0,”completionRate”:0.0,”customerNames”:”巳”,”managerName”:”刘客户经理”,”orgName”:”榆次郭家堡支行”,”status”:0,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:0},{“callNum”:1,”completionRate”:100.0,”customerNames”:”午”,”managerName”:”张客户经理”,”orgName”:”榆次郭家堡支行”,”status”:2,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:1},{“callNum”:0,”completionRate”:0.0,”customerNames”:”未”,”managerName”:”rihito”,”orgName”:”榆次郭家堡支行”,”status”:0,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:0},{“callNum”:0,”completionRate”:0.0,”customerNames”:”申”,”managerName”:”刘客户经理”,”orgName”:”榆次郭家堡支行”,”status”:0,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:0},{“callNum”:1,”completionRate”:100.0,”customerNames”:”酉”,”managerName”:”张客户经理”,”orgName”:”榆次郭家堡支行”,”status”:2,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:0},{“callNum”:0,”completionRate”:0.0,”customerNames”:”戌”,”managerName”:”rihito”,”orgName”:”榆次郭家堡支行”,”status”:0,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:0},{“callNum”:0,”completionRate”:0.0,”customerNames”:”亥”,”managerName”:”刘客户经理”,”orgName”:”榆次郭家堡支行”,”status”:0,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:0},{“callNum”:0,”completionRate”:0.0,”customerNames”:”子”,”managerName”:”张客户经理”,”orgName”:”榆次郭家堡支行”,”status”:0,”taskName”:”去”,”typeName”:”存款提升”,”visitNum”:1}],”taskTypeDist”:[{“name”:”存款提升”,”value”:9},{“name”:”新客拓展”,”value”:0},{“name”:”贷款营销”,”value”:0},{“name”:”到期转化”,”value”:0},{“name”:”走访”,”value”:0}],”total”:9}}
{“msg”:”操作成功”,”code”:200,”data”:{“boardData”:{“creditedHousehold”:0,”totalCreditAmount”:0.00,”totalHousehold”:6,”totalVillage”:1},”creditAmountDist”:[{“name”:”5万以下”,”value”:0},{“name”:”5-10万”,”value”:0},{“name”:”10-20万”,”value”:0},{“name”:”20-30万”,”value”:0},{“name”:”30万以上”,”value”:0}],”creditRateRank”:[{“creditRate”:0.0,”villageName”:”南沟村”}],”creditTrend”:{“creditAmount”:[0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00],”creditNum”:[0,0,0,0,0,0,0,0,0,0,0,0],”months”:[“1月”,”2月”,”3月”,”4月”,”5月”,”6月”,”7月”,”8月”,”9月”,”10月”,”11月”,”12月”]},”total”:1,”villageList”:[{“creditAmount”:0.00,”creditRate”:0.0,”creditedNum”:0,”orgName”:”榆次郭家堡支行”,”preCreditNum”:0,”surveyNum”:6,”totalHousehold”:6,”villageName”:”南沟村”}]}}

2026-07-31

only_full_group_by 报错风险
only_full_group_by 是 MySQL SQL_MODE(SQL 语法校验规则) 里的一个选项。
MySQL 5.7+、MySQL8.0 默认开启。
规则核心
在 SELECT … GROUP BY 查询中:
SELECT、HAVING、ORDER BY 里出现的所有非聚合字段,必须全部写在 GROUP BY 后面。
聚合函数:SUM() COUNT() MAX() MIN() GROUP_CONCAT()
非聚合字段:普通列 mtc_f.customer_name、mt.task_name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
<?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.mobile.mapper.ReportMapper">

<select id="selectVillageCreditBoardData" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.VillageCreditBoardVO$BoardData">
SELECT
COUNT(DISTINCT rga.area_id) AS totalVillage,
COUNT(DISTINCT tf.id) AS totalHousehold,
COUNT(DISTINCT CASE WHEN vcs.credit_status = 3 THEN tf.id END) AS creditedHousehold,
COALESCE((
SELECT SUM(sub_amt.total_amt)
FROM (
SELECT vcs2.customer_id, SUM(vcs2.suggest_credit_amount) AS total_amt
FROM village_credit_survey vcs2
INNER JOIN t_farmer tf2 ON vcs2.customer_id = tf2.id AND tf2.deleted = 0
WHERE vcs2.credit_status = 3 AND vcs2.del_flag = 0
<if test="batchId != null">
AND vcs2.batch_id = #{batchId}
</if>
<if test="periodValue != null and periodType == 'month'">
AND DATE_FORMAT(vcs2.survey_time, '%Y-%m') = #{periodValue}
</if>
<if test="periodValue != null and periodType == 'quarter'">
AND YEAR(vcs2.survey_time) = SUBSTRING(#{periodValue}, 1, 4)
AND QUARTER(vcs2.survey_time) = SUBSTRING(#{periodValue}, 6, 1)
</if>
<if test="periodValue != null and periodType == 'year'">
AND YEAR(vcs2.survey_time) = #{periodValue}
</if>
<if test="orgId != null">
AND tf2.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf2.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND tf2.manager_id = #{managerId}
</if>
GROUP BY vcs2.customer_id
) sub_amt
), 0) AS totalCreditAmount
FROM village_credit_survey vcs
INNER JOIN t_farmer tf ON vcs.customer_id = tf.id AND tf.deleted = 0
LEFT JOIN rural_grid_area rga ON tf.village_id = rga.area_id AND rga.area_type = 3 AND rga.del_flag = '0'
WHERE vcs.del_flag = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
</if>
<if test="periodValue != null and periodType == 'month'">
AND DATE_FORMAT(vcs.survey_time, '%Y-%m') = #{periodValue}
</if>
<if test="periodValue != null and periodType == 'quarter'">
AND YEAR(vcs.survey_time) = SUBSTRING(#{periodValue}, 1, 4)
AND QUARTER(vcs.survey_time) = SUBSTRING(#{periodValue}, 6, 1)
</if>
<if test="periodValue != null and periodType == 'year'">
AND YEAR(vcs.survey_time) = #{periodValue}
</if>
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND tf.manager_id = #{managerId}
</if>
</select>

<select id="selectCreditRateRank" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.VillageCreditBoardVO$VillageCreditRankVO">
SELECT
rga.area_name AS villageName,
CASE WHEN COUNT(DISTINCT tf.id) = 0 THEN 0
ELSE ROUND(COUNT(DISTINCT CASE WHEN vcs.credit_status = 3 THEN tf.id END) * 100.0 / COUNT(DISTINCT tf.id), 2)
END AS creditRate
FROM village_credit_survey vcs
INNER JOIN t_farmer tf ON vcs.customer_id = tf.id AND tf.deleted = 0
LEFT JOIN rural_grid_area rga ON tf.village_id = rga.area_id AND rga.area_type = 3 AND rga.del_flag = '0'
WHERE vcs.del_flag = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
</if>
<if test="orgId != null">
AND (rga.belong_org_id = #{orgId} OR tf.org_id = #{orgId})
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND (rga.belong_org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
OR tf.org_id IN
<foreach collection="orgIds" item="orgIdItem2" open="(" separator="," close=")">
#{orgIdItem2}
</foreach>
)
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND EXISTS (SELECT 1 FROM t_farmer tfm WHERE tfm.village_id = rga.area_id AND tfm.deleted = 0 AND tfm.manager_id = #{managerId})
</if>
GROUP BY rga.area_id, rga.area_name
ORDER BY creditRate DESC
LIMIT 10
</select>

<select id="selectCreditAmountDist" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.VillageCreditBoardVO$CreditAmountDistVO">
SELECT
t.name AS name,
COUNT(vcs.suggest_credit_amount) AS value
FROM (
SELECT '5万以下' AS name, 0 AS min_val, 5 AS max_val
UNION ALL SELECT '5-10万', 5, 10
UNION ALL SELECT '10-20万', 10, 20
UNION ALL SELECT '20-30万', 20, 30
UNION ALL SELECT '30万以上', 30, 999999
) t
LEFT JOIN (
SELECT vcs.suggest_credit_amount
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.credit_status = 3 AND vcs.del_flag = 0 AND tf.deleted = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
</if>
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND tf.manager_id = #{managerId}
</if>
) vcs ON vcs.suggest_credit_amount &gt;= t.min_val AND vcs.suggest_credit_amount &lt; t.max_val
GROUP BY t.name, t.min_val
ORDER BY t.min_val
</select>

<select id="selectCreditTrend" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="java.util.Map">
SELECT
GROUP_CONCAT(t.month_label ORDER BY t.month_order) AS monthList,
GROUP_CONCAT(COALESCE(stats.credit_num, 0) ORDER BY t.month_order) AS creditNumList,
GROUP_CONCAT(COALESCE(stats.credit_amount, 0) ORDER BY t.month_order) AS creditAmountList
FROM (
SELECT 1 AS month_order, '1月' AS month_label
UNION ALL SELECT 2, '2月' UNION ALL SELECT 3, '3月' UNION ALL SELECT 4, '4月'
UNION ALL SELECT 5, '5月' UNION ALL SELECT 6, '6月' UNION ALL SELECT 7, '7月'
UNION ALL SELECT 8, '8月' UNION ALL SELECT 9, '9月' UNION ALL SELECT 10, '10月'
UNION ALL SELECT 11, '11月' UNION ALL SELECT 12, '12月'
) t
LEFT JOIN (
SELECT
stats_month.period_key,
COUNT(*) AS credit_num,
COALESCE(SUM(stats_month.total_amt), 0) AS credit_amount
FROM (
SELECT
MONTH(vcs.survey_time) AS period_key,
vcs.customer_id,
SUM(vcs.suggest_credit_amount) AS total_amt
FROM village_credit_survey vcs
INNER JOIN t_farmer tf ON vcs.customer_id = tf.id AND tf.deleted = 0
WHERE vcs.credit_status = 3 AND vcs.del_flag = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
</if>
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND tf.manager_id = #{managerId}
</if>
<if test="periodValue != null">
AND YEAR(vcs.survey_time) = SUBSTRING(#{periodValue}, 1, 4)
</if>
GROUP BY MONTH(vcs.survey_time), vcs.customer_id
) stats_month
GROUP BY stats_month.period_key
) stats ON t.month_order = stats.period_key
</select>

<select id="selectVillageCreditDetailList" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.VillageCreditBoardVO$VillageCreditDetailVO">
SELECT
bo.org_name AS orgName,
rga.area_name AS villageName,
COUNT(DISTINCT tf.id) AS totalHousehold,
COUNT(DISTINCT vcs_survey.id) AS surveyNum,
COUNT(DISTINCT CASE WHEN vcs_survey.credit_status IN (2, 3) THEN vcs_survey.id END) AS preCreditNum,
COUNT(DISTINCT CASE WHEN vcs_survey.credit_status = 3 THEN tf.id END) AS creditedNum,
COALESCE(SUM(CASE WHEN vcs_survey.credit_status = 3 THEN vcs_survey.suggest_credit_amount END), 0) AS creditAmount,
CASE WHEN COUNT(DISTINCT tf.id) = 0 THEN 0
ELSE ROUND(COUNT(DISTINCT CASE WHEN vcs_survey.credit_status = 3 THEN tf.id END) * 100.0 / COUNT(DISTINCT tf.id), 2)
END AS creditRate
FROM village_credit_survey vcs_survey
INNER JOIN t_farmer tf ON vcs_survey.customer_id = tf.id AND tf.deleted = 0
LEFT JOIN rural_grid_area rga ON tf.village_id = rga.area_id AND rga.area_type = 3 AND rga.del_flag = '0'
LEFT JOIN rural_grid_area rga_town ON rga.parent_id = rga_town.area_id AND rga_town.del_flag = '0'
LEFT JOIN bank_organization bo ON COALESCE(rga_town.belong_org_id, rga.belong_org_id) = bo.org_id AND bo.del_flag = '0'
WHERE vcs_survey.del_flag = 0
<if test="batchId != null">
AND vcs_survey.batch_id = #{batchId}
</if>
<if test="orgId != null">
AND (rga.belong_org_id = #{orgId} OR tf.org_id = #{orgId})
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND (rga.belong_org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
OR tf.org_id IN
<foreach collection="orgIds" item="orgIdItem2" open="(" separator="," close=")">
#{orgIdItem2}
</foreach>
)
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND EXISTS (SELECT 1 FROM t_farmer tfm WHERE tfm.village_id = rga.area_id AND tfm.deleted = 0 AND tfm.manager_id = #{managerId})
</if>
GROUP BY rga.area_id, rga.area_name, bo.org_name
ORDER BY creditRate DESC
LIMIT #{offset}, #{limit}
</select>

<select id="selectVillageCreditDetailCount" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="Long">
SELECT COUNT(DISTINCT rga.area_id)
FROM village_credit_survey vcs
INNER JOIN t_farmer tf ON vcs.customer_id = tf.id AND tf.deleted = 0
LEFT JOIN rural_grid_area rga ON tf.village_id = rga.area_id AND rga.area_type = 3 AND rga.del_flag = '0'
WHERE vcs.del_flag = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
</if>
<if test="orgId != null">
AND (rga.belong_org_id = #{orgId} OR tf.org_id = #{orgId})
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND (rga.belong_org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
OR tf.org_id IN
<foreach collection="orgIds" item="orgIdItem2" open="(" separator="," close=")">
#{orgIdItem2}
</foreach>
)
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND EXISTS (SELECT 1 FROM t_farmer tfm WHERE tfm.village_id = rga.area_id AND tfm.deleted = 0 AND tfm.manager_id = #{managerId})
</if>
</select>

<select id="selectCustomerMarketingBoardData" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.CustomerMarketingBoardVO$BoardData">
SELECT
(SELECT COUNT(*) FROM marketing_task mt
WHERE mt.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS totalTask,
(SELECT COUNT(*) FROM marketing_task mt
WHERE mt.del_flag = '0' AND mt.task_status = '2'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS completedTask,
(SELECT COUNT(DISTINCT mt.customer_id) FROM marketing_exec_record mer
LEFT JOIN marketing_task mt ON mer.task_id = mt.id AND mt.del_flag = '0'
WHERE mer.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS totalVisit,
(SELECT COUNT(*) FROM t_farmer_call tfc
LEFT JOIN marketing_task mt ON tfc.task_id = mt.id AND mt.del_flag = '0'
WHERE tfc.deleted = 0
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS totalCall
</select>

<select id="selectTaskTypeDist" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.CustomerMarketingBoardVO$TaskTypeDistVO">
SELECT
mtt.type_name AS name,
COUNT(mt.id) AS value
FROM marketing_task_type mtt
LEFT JOIN marketing_task mt ON mt.type_id = mtt.id AND mt.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
WHERE mtt.status = 0
GROUP BY mtt.id, mtt.type_name
ORDER BY value DESC
</select>

<select id="selectMarketingTrend" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="java.util.Map">
SELECT
GROUP_CONCAT(t.month_label ORDER BY t.month_order) AS months,
GROUP_CONCAT(COALESCE(stats.visit_num, 0) ORDER BY t.month_order) AS visit_num,
GROUP_CONCAT(COALESCE(stats.call_num, 0) ORDER BY t.month_order) AS call_num
FROM (
SELECT 1 AS month_order, '1月' AS month_label
UNION ALL SELECT 2, '2月' UNION ALL SELECT 3, '3月' UNION ALL SELECT 4, '4月'
UNION ALL SELECT 5, '5月' UNION ALL SELECT 6, '6月' UNION ALL SELECT 7, '7月'
UNION ALL SELECT 8, '8月' UNION ALL SELECT 9, '9月' UNION ALL SELECT 10, '10月'
UNION ALL SELECT 11, '11月' UNION ALL SELECT 12, '12月'
) t
LEFT JOIN (
SELECT
period_key,
SUM(visit_num) AS visit_num,
SUM(call_num) AS call_num
FROM (
SELECT
MONTH(mer.create_time) AS period_key,
COUNT(DISTINCT mt.customer_id) AS visit_num,
0 AS call_num
FROM marketing_exec_record mer
LEFT JOIN marketing_task mt ON mer.task_id = mt.id AND mt.del_flag = '0'
WHERE mer.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
<if test="periodValue != null">AND YEAR(mer.create_time) = SUBSTRING(#{periodValue}, 1, 4)</if>
GROUP BY MONTH(mer.create_time)
UNION ALL
SELECT
MONTH(c.create_time) AS period_key,
0 AS visit_num,
COUNT(*) AS call_num
FROM t_farmer_call c
LEFT JOIN marketing_task mt ON c.task_id = mt.id AND mt.del_flag = '0'
WHERE c.deleted = 0
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
<if test="periodValue != null">AND YEAR(c.create_time) = SUBSTRING(#{periodValue}, 1, 4)</if>
GROUP BY MONTH(c.create_time)
) union_stats
GROUP BY period_key
) stats ON t.month_order = stats.period_key
</select>

<select id="selectManagerListByOrg" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.CustomerMarketingBoardVO$ManagerSimpleVO">
SELECT DISTINCT
su.user_id AS userId,
su.nick_name AS nickName
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
<if test="orgId != null">
AND su.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND su.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
ORDER BY su.nick_name
</select>

<select id="selectMarketingTaskDetailList" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.CustomerMarketingBoardVO$MarketingTaskDetailVO">
SELECT
mt.task_name AS taskName,
mtt.type_name AS typeName,
mt.org_name AS orgName,
mt.manager_name AS managerName,
COALESCE(mt.customer_name, mtc_f.customer_name) AS customerNames,
(SELECT COUNT(*) FROM marketing_exec_record mer
WHERE mer.del_flag = '0' AND mer.task_id = mt.id
) AS visitNum,
(SELECT COUNT(*) FROM t_farmer_call tfc
WHERE tfc.deleted = 0 AND tfc.task_id = mt.id
) AS callNum,
CASE WHEN mt.task_status = '2' THEN 100 ELSE 0 END AS completionRate,
CASE mt.task_status
WHEN '0' THEN 0
WHEN '1' THEN 1
WHEN '2' THEN 2
ELSE 3
END AS status
FROM marketing_task mt
LEFT JOIN marketing_task_type mtt ON mt.type_id = mtt.id
LEFT JOIN marketing_task_customer mtc ON mt.id = mtc.task_id AND mtc.del_flag = 0
LEFT JOIN t_farmer mtc_f ON mtc.customer_id = mtc_f.id
WHERE mt.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
GROUP BY mt.id, mt.task_name, mtt.type_name, mt.org_name, mt.manager_name, mt.manager_id, mt.task_status
ORDER BY mt.create_time DESC
LIMIT #{offset}, #{limit}
</select>

<select id="selectMarketingTaskDetailCount" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="Long">
SELECT COUNT(*)
FROM marketing_task mt
WHERE mt.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
</select>

<select id="selectManagerPerfSummary" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.ManagerPerfReportVO$SummaryData">
SELECT
(SELECT COUNT(DISTINCT su.user_id)
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
<if test="orgId != null">
AND su.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND su.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
) AS managerCount,
(SELECT COUNT(*) FROM t_farmer tf
WHERE tf.deleted = 0
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND tf.manager_id = #{managerId}</if>
) AS totalCustomer,
(SELECT COUNT(*) FROM marketing_task mt
WHERE mt.del_flag = '0' AND mt.task_status = '2'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS completedTask,
COALESCE((SELECT SUM(vcs.suggest_credit_amount)
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.credit_status = 3 AND vcs.del_flag = 0 AND tf.deleted = 0
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND tf.manager_id = #{managerId}</if>
), 0) AS totalCreditAmount
</select>

<select id="selectManagerPerfDetailList" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.ManagerPerfReportVO$ManagerPerfDetailVO">
SELECT
bo.org_name AS orgName,
su.nick_name AS managerName,
COALESCE(customer_stats.customer_count, 0) AS customerCount,
COALESCE(customer_stats.valid_customer_count, 0) AS validCustomerCount,
COALESCE(credit_stats.survey_count, 0) AS surveyCount,
COALESCE(credit_stats.credit_count, 0) AS creditCount,
COALESCE(credit_stats.credit_amount, 0) AS creditAmount,
COALESCE(task_stats.task_count, 0) AS taskCount,
COALESCE(task_stats.task_completed, 0) AS taskCompleted,
CASE WHEN COALESCE(task_stats.task_count, 0) = 0 THEN 0
ELSE ROUND(COALESCE(task_stats.task_completed, 0) * 100.0 / task_stats.task_count, 2)
END AS taskRate,
COALESCE(visit_stats.visit_count, 0) AS visitCount,
COALESCE(visit_stats.visit_times, 0) AS visitTimes,
COALESCE(call_stats.call_count, 0) AS callCount,
COALESCE(call_stats.call_total_duration, 0) AS callTotalDuration
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
LEFT JOIN bank_organization bo ON su.org_id = bo.org_id AND bo.del_flag = '0'
LEFT JOIN (
SELECT
tf.manager_id,
COUNT(DISTINCT tf.id) AS customer_count,
COUNT(DISTINCT CASE WHEN vcs.credit_status = 3 THEN tf.id END) AS valid_customer_count
FROM t_farmer tf
LEFT JOIN village_credit_survey vcs ON tf.id = vcs.customer_id AND vcs.del_flag = 0
WHERE tf.deleted = 0
GROUP BY tf.manager_id
) customer_stats ON su.user_id = customer_stats.manager_id
LEFT JOIN (
SELECT
vcs.survey_user_id AS manager_id,
COUNT(*) AS survey_count,
SUM(CASE WHEN vcs.credit_status = 3 THEN 1 ELSE 0 END) AS credit_count,
COALESCE(SUM(CASE WHEN vcs.credit_status = 3 THEN vcs.suggest_credit_amount END), 0) AS credit_amount
FROM village_credit_survey vcs
WHERE vcs.del_flag = 0
GROUP BY vcs.survey_user_id
) credit_stats ON su.user_id = credit_stats.manager_id
LEFT JOIN (
SELECT
mt.manager_id,
COUNT(*) AS task_count,
SUM(CASE WHEN mt.task_status = '2' THEN 1 ELSE 0 END) AS task_completed
FROM marketing_task mt
WHERE mt.del_flag = '0'
GROUP BY mt.manager_id
) task_stats ON su.user_id = task_stats.manager_id
LEFT JOIN (
SELECT
mt.manager_id,
COUNT(DISTINCT mt.customer_id) AS visit_count,
COUNT(mer.id) AS visit_times
FROM marketing_exec_record mer
LEFT JOIN marketing_task mt ON mer.task_id = mt.id AND mt.del_flag = '0'
WHERE mer.del_flag = '0'
GROUP BY mt.manager_id
) visit_stats ON su.user_id = visit_stats.manager_id
LEFT JOIN (
SELECT
mt.manager_id,
COUNT(*) AS call_count,
COALESCE(SUM(tfc.duration_min), 0) AS call_total_duration
FROM t_farmer_call tfc
LEFT JOIN marketing_task mt ON tfc.task_id = mt.id AND mt.del_flag = '0'
WHERE tfc.deleted = 0
GROUP BY mt.manager_id
) call_stats ON su.user_id = call_stats.manager_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
<if test="orgId != null">
AND su.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND su.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND su.user_id = #{managerId}
</if>
ORDER BY su.user_id DESC
LIMIT #{offset}, #{limit}
</select>

<select id="selectManagerPerfDetailCount" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="Long">
SELECT COUNT(*)
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
<if test="orgId != null">
AND su.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND su.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND su.user_id = #{managerId}</if>
</select>

<select id="selectOrgSummary" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.OrgPeriodSummaryReportVO$SummaryData">
SELECT
(SELECT COUNT(*) FROM bank_organization bo
WHERE bo.del_flag = '0' AND bo.org_type = 2
<if test="orgId != null">
AND bo.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND bo.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
) AS orgCount,
(SELECT COUNT(DISTINCT tf.id) FROM t_farmer tf
LEFT JOIN village_credit_survey vcs ON tf.id = vcs.customer_id AND vcs.del_flag = 0
WHERE tf.deleted = 0 AND vcs.credit_status = 3
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
) AS totalCreditCount,
COALESCE((SELECT SUM(vcs.suggest_credit_amount)
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.credit_status = 3 AND vcs.del_flag = 0 AND tf.deleted = 0
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
), 0) AS totalCreditAmount,
0 AS avgCreditAmount
</select>

<select id="selectOrgSummaryDetailList" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.OrgPeriodSummaryReportVO$OrgSummaryDetailVO">
SELECT
bo.org_name AS orgName,
COALESCE(manager_stats.manager_count, 0) AS managerCount,
COALESCE(village_stats.village_count, 0) AS villageCount,
COALESCE(customer_stats.total_customer, 0) AS totalCustomer,
COALESCE(customer_stats.valid_customer_count, 0) AS validCustomerCount,
COALESCE(credit_stats.survey_count, 0) AS surveyCount,
COALESCE(credit_stats.credit_count, 0) AS creditCount,
COALESCE(credit_stats.credit_amount, 0) AS creditAmount,
CASE WHEN COALESCE(customer_stats.total_customer, 0) = 0 THEN 0
ELSE ROUND(COALESCE(credit_stats.credit_count, 0) * 100.0 / customer_stats.total_customer, 2)
END AS creditRate,
COALESCE(task_stats.task_count, 0) AS taskCount,
COALESCE(task_stats.task_completed, 0) AS taskCompleted,
CASE WHEN COALESCE(task_stats.task_count, 0) = 0 THEN 0
ELSE ROUND(COALESCE(task_stats.task_completed, 0) * 100.0 / task_stats.task_count, 2)
END AS taskRate,
COALESCE(visit_stats.visit_count, 0) AS visitCount,
COALESCE(visit_stats.visit_times, 0) AS visitTimes,
COALESCE(call_stats.call_count, 0) AS callCount,
COALESCE(call_stats.call_total_duration, 0) AS callTotalDuration
FROM bank_organization bo
LEFT JOIN (
SELECT
su.org_id,
COUNT(*) AS manager_count
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
GROUP BY su.org_id
) manager_stats ON bo.org_id = manager_stats.org_id
LEFT JOIN (
SELECT
rga.belong_org_id AS dept_id,
COUNT(*) AS village_count
FROM rural_grid_area rga
WHERE rga.area_type = 3 AND rga.del_flag = '0'
GROUP BY rga.belong_org_id
) village_stats ON bo.org_id = village_stats.dept_id
LEFT JOIN (
SELECT
tf.org_id AS dept_id,
COUNT(DISTINCT tf.id) AS total_customer,
COUNT(DISTINCT CASE WHEN vcs.credit_status = 3 THEN tf.id END) AS valid_customer_count
FROM t_farmer tf
LEFT JOIN village_credit_survey vcs ON tf.id = vcs.customer_id AND vcs.del_flag = 0
WHERE tf.deleted = 0
GROUP BY tf.org_id
) customer_stats ON bo.org_id = customer_stats.dept_id
LEFT JOIN (
SELECT
tf.org_id AS dept_id,
COUNT(vcs.id) AS survey_count,
SUM(CASE WHEN vcs.credit_status = 3 THEN 1 ELSE 0 END) AS credit_count,
COALESCE(SUM(CASE WHEN vcs.credit_status = 3 THEN vcs.suggest_credit_amount END), 0) AS credit_amount
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.del_flag = 0 AND tf.deleted = 0
GROUP BY tf.org_id
) credit_stats ON bo.org_id = credit_stats.dept_id
LEFT JOIN (
SELECT
mt.org_id AS dept_id,
COUNT(*) AS task_count,
SUM(CASE WHEN mt.task_status = '2' THEN 1 ELSE 0 END) AS task_completed
FROM marketing_task mt
WHERE mt.del_flag = '0'
GROUP BY mt.org_id
) task_stats ON bo.org_id = task_stats.dept_id
LEFT JOIN (
SELECT
mt.org_id AS dept_id,
COUNT(DISTINCT mt.customer_id) AS visit_count,
COUNT(mer.id) AS visit_times
FROM marketing_exec_record mer
LEFT JOIN marketing_task mt ON mer.task_id = mt.id AND mt.del_flag = '0'
WHERE mer.del_flag = '0'
GROUP BY mt.org_id
) visit_stats ON bo.org_id = visit_stats.dept_id
LEFT JOIN (
SELECT
mt.org_id AS dept_id,
COUNT(*) AS call_count,
COALESCE(SUM(tfc.duration_min), 0) AS call_total_duration
FROM t_farmer_call tfc
LEFT JOIN marketing_task mt ON tfc.task_id = mt.id AND mt.del_flag = '0'
WHERE tfc.deleted = 0
GROUP BY mt.org_id
) call_stats ON bo.org_id = call_stats.dept_id
WHERE bo.org_type = 2 AND bo.del_flag = '0'
<if test="orgId != null">
AND bo.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND bo.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="orgName != null and orgName != ''">
AND bo.org_name LIKE CONCAT('%', #{orgName}, '%')
</if>
ORDER BY bo.org_id
LIMIT #{offset}, #{limit}
</select>

<select id="selectOrgSummaryDetailCount" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="Long">
SELECT COUNT(*)
FROM bank_organization bo
WHERE bo.org_type = 2 AND bo.del_flag = '0'
<if test="orgId != null">
AND bo.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND bo.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="orgName != null and orgName != ''">AND bo.org_name LIKE CONCAT('%', #{orgName}, '%')</if>
</select>
</mapper>

http://localhost/#/report/VillageCreditBoard
行政村授信明细 添加排序

  1. http://localhost/#/report/VillageCreditBoard 授信批次 行政村授信明细 选择第二个为什么第一个的也显示?
  2. 数据看板与报表 四个明细 添加排序(label=”序号”)

行政村授信明细 按所属支行
营销任务明细 按所属支行和客户经理
员工业绩明细 按所属支行和客户经理
网点汇总明细 按网点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
<?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.mobile.mapper.ReportMapper">

<select id="selectVillageCreditBoardData" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.VillageCreditBoardVO$BoardData">
SELECT
COUNT(DISTINCT rga.area_id) AS totalVillage,
COUNT(DISTINCT tf.id) AS totalHousehold,
COUNT(DISTINCT CASE WHEN vcs.credit_status = 3 THEN tf.id END) AS creditedHousehold,
COALESCE(SUM(CASE WHEN vcs.credit_status = 3 THEN vcs.suggest_credit_amount END), 0) AS totalCreditAmount
FROM village_credit_survey vcs
INNER JOIN t_farmer tf ON vcs.customer_id = tf.id AND tf.deleted = 0
LEFT JOIN rural_grid_area rga ON tf.village_id = rga.area_id AND rga.area_type = 3 AND rga.del_flag = '0'
WHERE vcs.del_flag = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
AND EXISTS (SELECT 1 FROM credit_batch cb WHERE cb.id = #{batchId} AND FIND_IN_SET(tf.village_id, cb.village_ids) > 0)
</if>
<if test="periodValue != null and periodType == 'month'">
AND DATE_FORMAT(vcs.survey_time, '%Y-%m') = #{periodValue}
</if>
<if test="periodValue != null and periodType == 'quarter'">
AND YEAR(vcs.survey_time) = SUBSTRING(#{periodValue}, 1, 4)
AND QUARTER(vcs.survey_time) = SUBSTRING(#{periodValue}, 6, 1)
</if>
<if test="periodValue != null and periodType == 'year'">
AND YEAR(vcs.survey_time) = #{periodValue}
</if>
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND tf.manager_id = #{managerId}
</if>
</select>

<select id="selectCreditRateRank" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.VillageCreditBoardVO$VillageCreditRankVO">
SELECT
rga.area_name AS villageName,
CASE WHEN COUNT(DISTINCT tf.id) = 0 THEN 0
ELSE ROUND(COUNT(DISTINCT CASE WHEN vcs.credit_status = 3 THEN tf.id END) * 100.0 / COUNT(DISTINCT tf.id), 2)
END AS creditRate
FROM village_credit_survey vcs
INNER JOIN t_farmer tf ON vcs.customer_id = tf.id AND tf.deleted = 0
LEFT JOIN rural_grid_area rga ON tf.village_id = rga.area_id AND rga.area_type = 3 AND rga.del_flag = '0'
WHERE vcs.del_flag = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
AND EXISTS (SELECT 1 FROM credit_batch cb WHERE cb.id = #{batchId} AND FIND_IN_SET(tf.village_id, cb.village_ids) > 0)
</if>
<if test="orgId != null">
AND (rga.belong_org_id = #{orgId} OR tf.org_id = #{orgId})
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND (rga.belong_org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
OR tf.org_id IN
<foreach collection="orgIds" item="orgIdItem2" open="(" separator="," close=")">
#{orgIdItem2}
</foreach>
)
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND EXISTS (SELECT 1 FROM t_farmer tfm WHERE tfm.village_id = rga.area_id AND tfm.deleted = 0 AND tfm.manager_id = #{managerId})
</if>
GROUP BY rga.area_id, rga.area_name
ORDER BY creditRate DESC
LIMIT 10
</select>

<select id="selectCreditAmountDist" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.VillageCreditBoardVO$CreditAmountDistVO">
SELECT
t.name AS name,
COUNT(vcs.suggest_credit_amount) AS value
FROM (
SELECT '5万以下' AS name, 0 AS min_val, 5 AS max_val
UNION ALL SELECT '5-10万', 5, 10
UNION ALL SELECT '10-20万', 10, 20
UNION ALL SELECT '20-30万', 20, 30
UNION ALL SELECT '30万以上', 30, 999999
) t
LEFT JOIN (
SELECT vcs.suggest_credit_amount
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.credit_status = 3 AND vcs.del_flag = 0 AND tf.deleted = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
AND EXISTS (SELECT 1 FROM credit_batch cb WHERE cb.id = #{batchId} AND FIND_IN_SET(tf.village_id, cb.village_ids) > 0)
</if>
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND tf.manager_id = #{managerId}
</if>
) vcs ON vcs.suggest_credit_amount &gt;= t.min_val AND vcs.suggest_credit_amount &lt; t.max_val
GROUP BY t.name, t.min_val
ORDER BY t.min_val
</select>

<select id="selectCreditTrend" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="java.util.Map">
SELECT
GROUP_CONCAT(t.month_label ORDER BY t.month_order) AS monthList,
GROUP_CONCAT(COALESCE(stats.credit_num, 0) ORDER BY t.month_order) AS creditNumList,
GROUP_CONCAT(COALESCE(stats.credit_amount, 0) ORDER BY t.month_order) AS creditAmountList
FROM (
SELECT 1 AS month_order, '1月' AS month_label
UNION ALL SELECT 2, '2月' UNION ALL SELECT 3, '3月' UNION ALL SELECT 4, '4月'
UNION ALL SELECT 5, '5月' UNION ALL SELECT 6, '6月' UNION ALL SELECT 7, '7月'
UNION ALL SELECT 8, '8月' UNION ALL SELECT 9, '9月' UNION ALL SELECT 10, '10月'
UNION ALL SELECT 11, '11月' UNION ALL SELECT 12, '12月'
) t
LEFT JOIN (
SELECT
MONTH(vcs.survey_time) AS period_key,
COUNT(vcs.id) AS credit_num,
COALESCE(SUM(vcs.suggest_credit_amount), 0) AS credit_amount
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.credit_status = 3 AND vcs.del_flag = 0 AND tf.deleted = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
AND EXISTS (SELECT 1 FROM credit_batch cb WHERE cb.id = #{batchId} AND FIND_IN_SET(tf.village_id, cb.village_ids) > 0)
</if>
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND tf.manager_id = #{managerId}
</if>
<if test="periodValue != null">
AND YEAR(vcs.survey_time) = SUBSTRING(#{periodValue}, 1, 4)
</if>
GROUP BY MONTH(vcs.survey_time)
) stats ON t.month_order = stats.period_key
</select>

<select id="selectVillageCreditDetailList" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.VillageCreditBoardVO$VillageCreditDetailVO">
SELECT
bo.org_name AS orgName,
rga.area_name AS villageName,
COUNT(DISTINCT tf.id) AS totalHousehold,
COUNT(DISTINCT vcs_survey.id) AS surveyNum,
COUNT(DISTINCT CASE WHEN vcs_survey.credit_status IN (2, 3) THEN vcs_survey.id END) AS preCreditNum,
COUNT(DISTINCT CASE WHEN vcs_survey.credit_status = 3 THEN vcs_survey.id END) AS creditedNum,
COALESCE(SUM(CASE WHEN vcs_survey.credit_status = 3 THEN vcs_survey.suggest_credit_amount END), 0) AS creditAmount,
CASE WHEN COUNT(DISTINCT tf.id) = 0 THEN 0
ELSE ROUND(COUNT(DISTINCT CASE WHEN vcs_survey.credit_status = 3 THEN tf.id END) * 100.0 / COUNT(DISTINCT tf.id), 2)
END AS creditRate
FROM village_credit_survey vcs_survey
INNER JOIN t_farmer tf ON vcs_survey.customer_id = tf.id AND tf.deleted = 0
LEFT JOIN rural_grid_area rga ON tf.village_id = rga.area_id AND rga.area_type = 3 AND rga.del_flag = '0'
LEFT JOIN rural_grid_area rga_town ON rga.parent_id = rga_town.area_id AND rga_town.del_flag = '0'
LEFT JOIN bank_organization bo ON COALESCE(rga_town.belong_org_id, rga.belong_org_id) = bo.org_id AND bo.del_flag = '0'
WHERE vcs_survey.del_flag = 0
<if test="batchId != null">
AND vcs_survey.batch_id = #{batchId}
AND EXISTS (SELECT 1 FROM credit_batch cb WHERE cb.id = #{batchId} AND FIND_IN_SET(tf.village_id, cb.village_ids) > 0)
</if>
<if test="orgId != null">
AND (rga.belong_org_id = #{orgId} OR tf.org_id = #{orgId})
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND (rga.belong_org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
OR tf.org_id IN
<foreach collection="orgIds" item="orgIdItem2" open="(" separator="," close=")">
#{orgIdItem2}
</foreach>
)
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND EXISTS (SELECT 1 FROM t_farmer tfm WHERE tfm.village_id = rga.area_id AND tfm.deleted = 0 AND tfm.manager_id = #{managerId})
</if>
GROUP BY rga.area_id, rga.area_name, bo.org_name
ORDER BY bo.org_name
LIMIT #{offset}, #{limit}
</select>

<select id="selectVillageCreditDetailCount" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="Long">
SELECT COUNT(DISTINCT rga.area_id)
FROM village_credit_survey vcs
INNER JOIN t_farmer tf ON vcs.customer_id = tf.id AND tf.deleted = 0
LEFT JOIN rural_grid_area rga ON tf.village_id = rga.area_id AND rga.area_type = 3 AND rga.del_flag = '0'
WHERE vcs.del_flag = 0
<if test="batchId != null">
AND vcs.batch_id = #{batchId}
AND EXISTS (SELECT 1 FROM credit_batch cb WHERE cb.id = #{batchId} AND FIND_IN_SET(tf.village_id, cb.village_ids) > 0)
</if>
<if test="orgId != null">
AND (rga.belong_org_id = #{orgId} OR tf.org_id = #{orgId})
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND (rga.belong_org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
OR tf.org_id IN
<foreach collection="orgIds" item="orgIdItem2" open="(" separator="," close=")">
#{orgIdItem2}
</foreach>
)
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND EXISTS (SELECT 1 FROM t_farmer tfm WHERE tfm.village_id = rga.area_id AND tfm.deleted = 0 AND tfm.manager_id = #{managerId})
</if>
</select>

<select id="selectCustomerMarketingBoardData" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.CustomerMarketingBoardVO$BoardData">
SELECT
(SELECT COUNT(*) FROM marketing_task mt
WHERE mt.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS totalTask,
(SELECT COUNT(*) FROM marketing_task mt
WHERE mt.del_flag = '0' AND mt.task_status = '2'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS completedTask,
(SELECT COUNT(DISTINCT mt.customer_id) FROM marketing_exec_record mer
LEFT JOIN marketing_task mt ON mer.task_id = mt.id AND mt.del_flag = '0'
WHERE mer.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS totalVisit,
(SELECT COUNT(*) FROM t_farmer_call tfc
LEFT JOIN marketing_task mt ON tfc.task_id = mt.id AND mt.del_flag = '0'
WHERE tfc.deleted = 0
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS totalCall
</select>

<select id="selectTaskTypeDist" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.CustomerMarketingBoardVO$TaskTypeDistVO">
SELECT
mtt.type_name AS name,
COUNT(mt.id) AS value
FROM marketing_task_type mtt
LEFT JOIN marketing_task mt ON mt.type_id = mtt.id AND mt.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
WHERE mtt.status = 0
GROUP BY mtt.id, mtt.type_name
ORDER BY value DESC
</select>

<select id="selectMarketingTrend" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="java.util.Map">
SELECT
GROUP_CONCAT(t.month_label ORDER BY t.month_order) AS months,
GROUP_CONCAT(COALESCE(stats.visit_num, 0) ORDER BY t.month_order) AS visit_num,
GROUP_CONCAT(COALESCE(stats.call_num, 0) ORDER BY t.month_order) AS call_num
FROM (
SELECT 1 AS month_order, '1月' AS month_label
UNION ALL SELECT 2, '2月' UNION ALL SELECT 3, '3月' UNION ALL SELECT 4, '4月'
UNION ALL SELECT 5, '5月' UNION ALL SELECT 6, '6月' UNION ALL SELECT 7, '7月'
UNION ALL SELECT 8, '8月' UNION ALL SELECT 9, '9月' UNION ALL SELECT 10, '10月'
UNION ALL SELECT 11, '11月' UNION ALL SELECT 12, '12月'
) t
LEFT JOIN (
SELECT
period_key,
SUM(visit_num) AS visit_num,
SUM(call_num) AS call_num
FROM (
SELECT
MONTH(mer.create_time) AS period_key,
COUNT(DISTINCT mt.customer_id) AS visit_num,
0 AS call_num
FROM marketing_exec_record mer
LEFT JOIN marketing_task mt ON mer.task_id = mt.id AND mt.del_flag = '0'
WHERE mer.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
<if test="periodValue != null">AND YEAR(mer.create_time) = SUBSTRING(#{periodValue}, 1, 4)</if>
GROUP BY MONTH(mer.create_time)
UNION ALL
SELECT
MONTH(c.create_time) AS period_key,
0 AS visit_num,
COUNT(*) AS call_num
FROM t_farmer_call c
LEFT JOIN marketing_task mt ON c.task_id = mt.id AND mt.del_flag = '0'
WHERE c.deleted = 0
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
<if test="periodValue != null">AND YEAR(c.create_time) = SUBSTRING(#{periodValue}, 1, 4)</if>
GROUP BY MONTH(c.create_time)
) union_stats
GROUP BY period_key
) stats ON t.month_order = stats.period_key
</select>

<select id="selectManagerListByOrg" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.CustomerMarketingBoardVO$ManagerSimpleVO">
SELECT DISTINCT
su.user_id AS userId,
su.nick_name AS nickName
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
<if test="orgId != null">
AND su.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND su.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
ORDER BY su.nick_name
</select>

<select id="selectMarketingTaskDetailList" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.CustomerMarketingBoardVO$MarketingTaskDetailVO">
SELECT
mt.task_name AS taskName,
mtt.type_name AS typeName,
mt.org_name AS orgName,
mt.manager_name AS managerName,
GROUP_CONCAT(DISTINCT COALESCE(mt.customer_name, mtc_f.customer_name) SEPARATOR ',') AS customerNames,
(SELECT COUNT(*) FROM marketing_exec_record mer
WHERE mer.del_flag = '0' AND mer.task_id = mt.id
) AS visitNum,
(SELECT COUNT(*) FROM t_farmer_call tfc
WHERE tfc.deleted = 0 AND tfc.task_id = mt.id
) AS callNum,
CASE WHEN mt.task_status = '2' THEN 100 ELSE 0 END AS completionRate,
CASE mt.task_status
WHEN '0' THEN 0
WHEN '1' THEN 1
WHEN '2' THEN 2
ELSE 3
END AS status
FROM marketing_task mt
LEFT JOIN marketing_task_type mtt ON mt.type_id = mtt.id
LEFT JOIN marketing_task_customer mtc ON mt.id = mtc.task_id AND mtc.del_flag = 0
LEFT JOIN t_farmer mtc_f ON mtc.customer_id = mtc_f.id
WHERE mt.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
GROUP BY mt.id, mt.task_name, mtt.type_name, mt.org_name, mt.manager_name, mt.manager_id, mt.task_status
ORDER BY mt.org_name, mt.manager_name
LIMIT #{offset}, #{limit}
</select>

<select id="selectMarketingTaskDetailCount" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="Long">
SELECT COUNT(*)
FROM marketing_task mt
WHERE mt.del_flag = '0'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
</select>

<select id="selectManagerPerfSummary" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.ManagerPerfReportVO$SummaryData">
SELECT
(SELECT COUNT(DISTINCT su.user_id)
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
<if test="orgId != null">
AND su.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND su.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
) AS managerCount,
(SELECT COUNT(*) FROM t_farmer tf
WHERE tf.deleted = 0
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND tf.manager_id = #{managerId}</if>
) AS totalCustomer,
(SELECT COUNT(*) FROM marketing_task mt
WHERE mt.del_flag = '0' AND mt.task_status = '2'
<if test="orgId != null">
AND mt.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND mt.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND mt.manager_id = #{managerId}</if>
) AS completedTask,
COALESCE((SELECT SUM(vcs.suggest_credit_amount)
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.credit_status = 3 AND vcs.del_flag = 0 AND tf.deleted = 0
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND tf.manager_id = #{managerId}</if>
), 0) AS totalCreditAmount
</select>

<select id="selectManagerPerfDetailList" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.ManagerPerfReportVO$ManagerPerfDetailVO">
SELECT
bo.org_name AS orgName,
su.nick_name AS managerName,
COALESCE(customer_stats.customer_count, 0) AS customerCount,
COALESCE(customer_stats.valid_customer_count, 0) AS validCustomerCount,
COALESCE(credit_stats.survey_count, 0) AS surveyCount,
COALESCE(credit_stats.credit_count, 0) AS creditCount,
COALESCE(credit_stats.credit_amount, 0) AS creditAmount,
COALESCE(task_stats.task_count, 0) AS taskCount,
COALESCE(task_stats.task_completed, 0) AS taskCompleted,
CASE WHEN COALESCE(task_stats.task_count, 0) = 0 THEN 0
ELSE ROUND(COALESCE(task_stats.task_completed, 0) * 100.0 / task_stats.task_count, 2)
END AS taskRate,
COALESCE(visit_stats.visit_count, 0) AS visitCount,
COALESCE(visit_stats.visit_times, 0) AS visitTimes,
COALESCE(call_stats.call_count, 0) AS callCount,
COALESCE(call_stats.call_total_duration, 0) AS callTotalDuration
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
LEFT JOIN bank_organization bo ON su.org_id = bo.org_id AND bo.del_flag = '0'
LEFT JOIN (
SELECT
tf.manager_id,
COUNT(DISTINCT tf.id) AS customer_count,
COUNT(DISTINCT CASE WHEN vcs.credit_status = 3 THEN tf.id END) AS valid_customer_count
FROM t_farmer tf
LEFT JOIN village_credit_survey vcs ON tf.id = vcs.customer_id AND vcs.del_flag = 0
WHERE tf.deleted = 0
GROUP BY tf.manager_id
) customer_stats ON su.user_id = customer_stats.manager_id
LEFT JOIN (
SELECT
vcs.survey_user_id AS manager_id,
COUNT(*) AS survey_count,
SUM(CASE WHEN vcs.credit_status = 3 THEN 1 ELSE 0 END) AS credit_count,
COALESCE(SUM(CASE WHEN vcs.credit_status = 3 THEN vcs.suggest_credit_amount END), 0) AS credit_amount
FROM village_credit_survey vcs
WHERE vcs.del_flag = 0
GROUP BY vcs.survey_user_id
) credit_stats ON su.user_id = credit_stats.manager_id
LEFT JOIN (
SELECT
mt.manager_id,
COUNT(*) AS task_count,
SUM(CASE WHEN mt.task_status = '2' THEN 1 ELSE 0 END) AS task_completed
FROM marketing_task mt
WHERE mt.del_flag = '0'
GROUP BY mt.manager_id
) task_stats ON su.user_id = task_stats.manager_id
LEFT JOIN (
SELECT
mt.manager_id,
COUNT(DISTINCT mt.customer_id) AS visit_count,
COUNT(mer.id) AS visit_times
FROM marketing_exec_record mer
LEFT JOIN marketing_task mt ON mer.task_id = mt.id AND mt.del_flag = '0'
WHERE mer.del_flag = '0'
GROUP BY mt.manager_id
) visit_stats ON su.user_id = visit_stats.manager_id
LEFT JOIN (
SELECT
mt.manager_id,
COUNT(*) AS call_count,
COALESCE(SUM(tfc.duration_min), 0) AS call_total_duration
FROM t_farmer_call tfc
LEFT JOIN marketing_task mt ON tfc.task_id = mt.id AND mt.del_flag = '0'
WHERE tfc.deleted = 0
GROUP BY mt.manager_id
) call_stats ON su.user_id = call_stats.manager_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
<if test="orgId != null">
AND su.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND su.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">
AND su.user_id = #{managerId}
</if>
ORDER BY bo.org_name, su.nick_name
LIMIT #{offset}, #{limit}
</select>

<select id="selectManagerPerfDetailCount" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="Long">
SELECT COUNT(*)
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
<if test="orgId != null">
AND su.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND su.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="managerId != null">AND su.user_id = #{managerId}</if>
</select>

<select id="selectOrgSummary" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.OrgPeriodSummaryReportVO$SummaryData">
SELECT
(SELECT COUNT(*) FROM bank_organization bo
WHERE bo.del_flag = '0' AND bo.org_type = 2
<if test="orgId != null">
AND bo.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND bo.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
) AS orgCount,
(SELECT COUNT(DISTINCT tf.id) FROM t_farmer tf
LEFT JOIN village_credit_survey vcs ON tf.id = vcs.customer_id AND vcs.del_flag = 0
WHERE tf.deleted = 0 AND vcs.credit_status = 3
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
) AS totalCreditCount,
COALESCE((SELECT SUM(vcs.suggest_credit_amount)
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.credit_status = 3 AND vcs.del_flag = 0 AND tf.deleted = 0
<if test="orgId != null">
AND tf.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND tf.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
), 0) AS totalCreditAmount,
0 AS avgCreditAmount
</select>

<select id="selectOrgSummaryDetailList" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="com.ruoyi.mobile.vo.OrgPeriodSummaryReportVO$OrgSummaryDetailVO">
SELECT
bo.org_name AS orgName,
COALESCE(manager_stats.manager_count, 0) AS managerCount,
COALESCE(village_stats.village_count, 0) AS villageCount,
COALESCE(customer_stats.total_customer, 0) AS totalCustomer,
COALESCE(customer_stats.valid_customer_count, 0) AS validCustomerCount,
COALESCE(credit_stats.survey_count, 0) AS surveyCount,
COALESCE(credit_stats.credit_count, 0) AS creditCount,
COALESCE(credit_stats.credit_amount, 0) AS creditAmount,
CASE WHEN COALESCE(customer_stats.total_customer, 0) = 0 THEN 0
ELSE ROUND(COALESCE(credit_stats.credit_count, 0) * 100.0 / customer_stats.total_customer, 2)
END AS creditRate,
COALESCE(task_stats.task_count, 0) AS taskCount,
COALESCE(task_stats.task_completed, 0) AS taskCompleted,
CASE WHEN COALESCE(task_stats.task_count, 0) = 0 THEN 0
ELSE ROUND(COALESCE(task_stats.task_completed, 0) * 100.0 / task_stats.task_count, 2)
END AS taskRate,
COALESCE(visit_stats.visit_count, 0) AS visitCount,
COALESCE(visit_stats.visit_times, 0) AS visitTimes,
COALESCE(call_stats.call_count, 0) AS callCount,
COALESCE(call_stats.call_total_duration, 0) AS callTotalDuration
FROM bank_organization bo
LEFT JOIN (
SELECT
su.org_id,
COUNT(*) AS manager_count
FROM sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
WHERE sur.role_id = 2008 AND su.del_flag = '0'
GROUP BY su.org_id
) manager_stats ON bo.org_id = manager_stats.org_id
LEFT JOIN (
SELECT
rga.belong_org_id AS dept_id,
COUNT(*) AS village_count
FROM rural_grid_area rga
WHERE rga.area_type = 3 AND rga.del_flag = '0'
GROUP BY rga.belong_org_id
) village_stats ON bo.org_id = village_stats.dept_id
LEFT JOIN (
SELECT
tf.org_id AS dept_id,
COUNT(DISTINCT tf.id) AS total_customer,
COUNT(DISTINCT CASE WHEN vcs.credit_status = 3 THEN tf.id END) AS valid_customer_count
FROM t_farmer tf
LEFT JOIN village_credit_survey vcs ON tf.id = vcs.customer_id AND vcs.del_flag = 0
WHERE tf.deleted = 0
GROUP BY tf.org_id
) customer_stats ON bo.org_id = customer_stats.dept_id
LEFT JOIN (
SELECT
tf.org_id AS dept_id,
COUNT(vcs.id) AS survey_count,
SUM(CASE WHEN vcs.credit_status = 3 THEN 1 ELSE 0 END) AS credit_count,
COALESCE(SUM(CASE WHEN vcs.credit_status = 3 THEN vcs.suggest_credit_amount END), 0) AS credit_amount
FROM village_credit_survey vcs
LEFT JOIN t_farmer tf ON vcs.customer_id = tf.id
WHERE vcs.del_flag = 0 AND tf.deleted = 0
GROUP BY tf.org_id
) credit_stats ON bo.org_id = credit_stats.dept_id
LEFT JOIN (
SELECT
mt.org_id AS dept_id,
COUNT(*) AS task_count,
SUM(CASE WHEN mt.task_status = '2' THEN 1 ELSE 0 END) AS task_completed
FROM marketing_task mt
WHERE mt.del_flag = '0'
GROUP BY mt.org_id
) task_stats ON bo.org_id = task_stats.dept_id
LEFT JOIN (
SELECT
mt.org_id AS dept_id,
COUNT(DISTINCT mt.customer_id) AS visit_count,
COUNT(mer.id) AS visit_times
FROM marketing_exec_record mer
LEFT JOIN marketing_task mt ON mer.task_id = mt.id AND mt.del_flag = '0'
WHERE mer.del_flag = '0'
GROUP BY mt.org_id
) visit_stats ON bo.org_id = visit_stats.dept_id
LEFT JOIN (
SELECT
mt.org_id AS dept_id,
COUNT(*) AS call_count,
COALESCE(SUM(tfc.duration_min), 0) AS call_total_duration
FROM t_farmer_call tfc
LEFT JOIN marketing_task mt ON tfc.task_id = mt.id AND mt.del_flag = '0'
WHERE tfc.deleted = 0
GROUP BY mt.org_id
) call_stats ON bo.org_id = call_stats.dept_id
WHERE bo.org_type = 2 AND bo.del_flag = '0'
<if test="orgId != null">
AND bo.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND bo.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="orgName != null and orgName != ''">
AND bo.org_name LIKE CONCAT('%', #{orgName}, '%')
</if>
ORDER BY bo.org_name
LIMIT #{offset}, #{limit}
</select>

<select id="selectOrgSummaryDetailCount" parameterType="com.ruoyi.mobile.dto.ReportQueryDTO" resultType="Long">
SELECT COUNT(*)
FROM bank_organization bo
WHERE bo.org_type = 2 AND bo.del_flag = '0'
<if test="orgId != null">
AND bo.org_id = #{orgId}
</if>
<if test="orgIds != null">
<if test="orgIds.size() > 0">
AND bo.org_id IN
<foreach collection="orgIds" item="orgIdItem" open="(" separator="," close=")">
#{orgIdItem}
</foreach>
</if>
<if test="orgIds.size() == 0">
AND 1 = 0
</if>
</if>
<if test="orgName != null and orgName != ''">AND bo.org_name LIKE CONCAT('%', #{orgName}, '%')</if>
</select>
</mapper>

2026-08-01