From db5ca691d7b7fa76bd1c53447d355ae2980e8a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Tue, 14 Apr 2026 11:57:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(customer-lead):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E5=AE=A2=E8=B5=84=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8F=8A=E5=85=A8=E6=B0=91=E6=8E=A8=E8=8D=90=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增客资管理系统数据库变更脚本,扩展客资表及新增派单、推荐关系等多张表 - 实现客资派单、跟进、统计、导出等核心业务逻辑,支持多管理员配置 - 开发Java后端实体、参数、Mapper和服务,实现完整业务流程接口 - 提供客资管理相关REST API,涵盖分页查询、详情、状态更新、派单、跟进和统计等 - 新增全民推荐模块,支持匿名及注册用户报备推荐客户,并提供推荐记录管理 - 开发推荐人相关API接口,支持推荐码生成与查询,推荐确认及结算功能 - Vue后台新增客资管理页面,实现客资列表、派单、跟进、详情查看等功能 - 微信小程序端新增推荐客户页面,支持推荐记录展示和推荐状态跟踪 - 完善数据字典和部署说明,涵盖状态说明、来源类型和跟进方式 - 提出后续优化建议,包括权限细化、数据看板、消息通知以及推荐海报功能等 --- docs/ai/customer-lead-system-summary.md | 230 +++++++++++ docs/sql/customer_lead_system.sql | 178 ++++++++ .../controller/CustomerLeadController.java | 132 ++++++ .../controller/LeadReferralController.java | 106 +++++ .../cms/entity/CustomerLeadEntity.java | 112 +++++ .../gxwebsoft/cms/entity/LeadDispatch.java | 69 ++++ .../gxwebsoft/cms/entity/LeadFollowLog.java | 84 ++++ .../gxwebsoft/cms/entity/LeadReferral.java | 89 ++++ .../gxwebsoft/cms/entity/LeadStatistics.java | 72 ++++ .../cms/mapper/CustomerLeadMapper.java | 78 ++++ .../cms/mapper/LeadDispatchMapper.java | 34 ++ .../cms/mapper/LeadFollowLogMapper.java | 35 ++ .../cms/mapper/LeadReferralMapper.java | 44 ++ .../cms/param/CustomerLeadParam.java | 81 ++++ .../cms/param/LeadDispatchParam.java | 38 ++ .../gxwebsoft/cms/param/LeadFollowParam.java | 43 ++ .../cms/param/LeadReferralParam.java | 44 ++ .../cms/service/CustomerLeadService.java | 84 ++++ .../cms/service/LeadReferralService.java | 73 ++++ .../service/impl/CustomerLeadServiceImpl.java | 382 ++++++++++++++++++ .../service/impl/LeadReferralServiceImpl.java | 305 ++++++++++++++ .../common/system/entity/UserRoleExtend.java | 87 ++++ 22 files changed, 2400 insertions(+) create mode 100644 docs/ai/customer-lead-system-summary.md create mode 100644 docs/sql/customer_lead_system.sql create mode 100644 src/main/java/com/gxwebsoft/cms/controller/CustomerLeadController.java create mode 100644 src/main/java/com/gxwebsoft/cms/controller/LeadReferralController.java create mode 100644 src/main/java/com/gxwebsoft/cms/entity/CustomerLeadEntity.java create mode 100644 src/main/java/com/gxwebsoft/cms/entity/LeadDispatch.java create mode 100644 src/main/java/com/gxwebsoft/cms/entity/LeadFollowLog.java create mode 100644 src/main/java/com/gxwebsoft/cms/entity/LeadReferral.java create mode 100644 src/main/java/com/gxwebsoft/cms/entity/LeadStatistics.java create mode 100644 src/main/java/com/gxwebsoft/cms/mapper/CustomerLeadMapper.java create mode 100644 src/main/java/com/gxwebsoft/cms/mapper/LeadDispatchMapper.java create mode 100644 src/main/java/com/gxwebsoft/cms/mapper/LeadFollowLogMapper.java create mode 100644 src/main/java/com/gxwebsoft/cms/mapper/LeadReferralMapper.java create mode 100644 src/main/java/com/gxwebsoft/cms/param/CustomerLeadParam.java create mode 100644 src/main/java/com/gxwebsoft/cms/param/LeadDispatchParam.java create mode 100644 src/main/java/com/gxwebsoft/cms/param/LeadFollowParam.java create mode 100644 src/main/java/com/gxwebsoft/cms/param/LeadReferralParam.java create mode 100644 src/main/java/com/gxwebsoft/cms/service/CustomerLeadService.java create mode 100644 src/main/java/com/gxwebsoft/cms/service/LeadReferralService.java create mode 100644 src/main/java/com/gxwebsoft/cms/service/impl/CustomerLeadServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/cms/service/impl/LeadReferralServiceImpl.java create mode 100644 src/main/java/com/gxwebsoft/common/system/entity/UserRoleExtend.java diff --git a/docs/ai/customer-lead-system-summary.md b/docs/ai/customer-lead-system-summary.md new file mode 100644 index 0000000..7950573 --- /dev/null +++ b/docs/ai/customer-lead-system-summary.md @@ -0,0 +1,230 @@ +# 客资管理系统实施总结 + +> 创建时间:2026-04-14 +> 作者:AI助手 +> 状态:实施完成 + +--- + +## 一、需求概述 + +根据客户需求,实现一个完整的**客资管理系统**,具备以下功能: + +| 功能 | 描述 | +|------|------| +| 客资派单 | 管理员可以直接派单客资给业务员 | +| 全民推荐 | 任何人都可以推荐客户赚取推荐费 | +| 推荐人报备 | 注册用户可以报备客户(推荐人报备) | +| 实时跟进 | 实时查看跟进情况和成交状态 | +| 多管理员 | 支持多管理员设置 | +| 数据统计导出 | 生成统计报表功能 | + +--- + +## 二、实施成果 + +### 2.1 Java后端 (`/Users/gxwebsoft/JAVA/mp-java`) + +#### 数据库变更 +**文件**: `docs/sql/customer_lead_system.sql` + +| 表名 | 说明 | +|------|------| +| `cms_contact_lead` | 扩展现有客资表,添加派单、推荐人等字段 | +| `lead_dispatch` | 派单记录表 | +| `lead_follow_log` | 跟进记录表 | +| `lead_referral` | 推荐人关系表(全民推荐) | +| `sys_user_role_extend` | 用户角色扩展表(多管理员) | +| `lead_statistics` | 数据统计汇总表 | +| `lead_referral_settlement` | 推荐费结算记录表 | +| `v_lead_full_info` | 客资完整信息视图 | + +#### 新增实体类 +| 文件路径 | 说明 | +|----------|------| +| `cms/entity/CustomerLeadEntity.java` | 客资管理扩展实体 | +| `cms/entity/LeadDispatch.java` | 派单记录实体 | +| `cms/entity/LeadFollowLog.java` | 跟进记录实体 | +| `cms/entity/LeadReferral.java` | 推荐关系实体 | +| `cms/entity/LeadStatistics.java` | 统计实体 | +| `common/system/entity/UserRoleExtend.java` | 用户角色扩展实体 | + +#### 新增参数类 +| 文件路径 | 说明 | +|----------|------| +| `cms/param/CustomerLeadParam.java` | 客资查询参数 | +| `cms/param/LeadDispatchParam.java` | 派单请求参数 | +| `cms/param/LeadFollowParam.java` | 跟进请求参数 | +| `cms/param/LeadReferralParam.java` | 推荐人报备参数 | + +#### 新增Mapper +| 文件路径 | 说明 | +|----------|------| +| `cms/mapper/CustomerLeadMapper.java` | 客资管理Mapper | +| `cms/mapper/LeadDispatchMapper.java` | 派单记录Mapper | +| `cms/mapper/LeadFollowLogMapper.java` | 跟进记录Mapper | +| `cms/mapper/LeadReferralMapper.java` | 推荐关系Mapper | + +#### 新增Service +| 文件路径 | 说明 | +|----------|------| +| `cms/service/CustomerLeadService.java` | 客资管理服务接口 | +| `cms/service/impl/CustomerLeadServiceImpl.java` | 客资管理服务实现 | +| `cms/service/LeadReferralService.java` | 推荐人服务接口 | +| `cms/service/impl/LeadReferralServiceImpl.java` | 推荐人服务实现 | + +#### 新增Controller +| 文件路径 | 说明 | +|----------|------| +| `cms/controller/CustomerLeadController.java` | 客资管理控制器 | +| `cms/controller/LeadReferralController.java` | 推荐人控制器 | + +#### API端点 +| 接口 | 方法 | 说明 | +|------|------|------| +| `/customer/lead/page` | GET | 分页查询客资列表 | +| `/customer/lead/detail/{leadId}` | GET | 获取客资详情 | +| `/customer/lead/create` | POST | 创建客资 | +| `/customer/lead/update` | PUT | 更新客资信息 | +| `/customer/lead/status/{leadId}` | PUT | 更新客资状态 | +| `/customer/lead/dispatch` | POST | 派单给业务员 | +| `/customer/lead/dispatch/batch` | POST | 批量派单 | +| `/customer/lead/follow` | POST | 添加跟进记录 | +| `/customer/lead/follow/history/{leadId}` | GET | 获取跟进历史 | +| `/customer/lead/statistics` | GET | 获取统计数据 | +| `/customer/lead/export` | GET | 导出客资数据 | +| `/customer/lead/unassigned` | GET | 获取未分配客资 | +| `/lead/referral/anonymous` | POST | 匿名用户报备 | +| `/lead/referral/user` | POST | 注册用户报备 | +| `/lead/referral/page` | GET | 推荐人推荐记录 | +| `/lead/referral/stats/{userId}` | GET | 推荐人统计 | + +--- + +### 2.2 Vue后台管理端 (`/Users/gxwebsoft/VUE/mp-vue`) + +#### 新增API +| 文件路径 | 说明 | +|----------|------| +| `api/cms/customerLead/model.ts` | 类型定义 | +| `api/cms/customerLead/index.ts` | API接口 | + +#### 新增页面 +| 文件路径 | 说明 | +|----------|------| +| `views/cms/customerLead/index.vue` | 客资管理列表页面 | + +**功能特性**: +- 客资列表(分页、筛选、搜索) +- 统计卡片(总客资、待跟进、已成交、成交金额) +- 新增/编辑客资 +- 派单给业务员(支持批量派单) +- 添加跟进记录 +- 查看跟进历史 +- 客资详情弹窗 + +--- + +### 2.3 微信小程序端 (`/Users/gxwebsoft/VUE/template-10582`) + +#### 新增API +| 文件路径 | 说明 | +|----------|------| +| `api/shop/referral.ts` | 推荐人API | + +#### 新增页面 +| 文件路径 | 说明 | +|----------|------| +| `dealer/referral/index.config.ts` | 页面配置 | +| `dealer/referral/index.tsx` | 推荐人报备页面 | +| `dealer/referral/index.scss` | 页面样式 | + +**功能特性**: +- 推荐人统计(总推荐、待确认、有效、待结算金额) +- 推荐新客户表单 +- 推荐记录列表 +- 状态追踪 + +#### 首页入口 +在分销商首页 `/dealer/index.tsx` 添加了「推荐客户」入口 + +--- + +## 三、数据字典 + +### 客资状态 +| 值 | 文本 | 说明 | +|----|------|------| +| 0 | 待跟进 | 新客资,未分配或未联系 | +| 1 | 跟进中 | 正在跟进 | +| 2 | 已成交 | 客户已付款 | +| 3 | 无效 | 商机流失 | + +### 客资来源 +| 值 | 文本 | 说明 | +|----|------|------| +| form | 表单 | 网站/小程序表单提交 | +| website | 网站 | 网站表单 | +| miniapp | 小程序 | 小程序表单 | +| referral | 推荐人 | 推荐人报备 | +| admin | 管理员录入 | 后台手动添加 | + +### 跟进方式 +| 值 | 文本 | 说明 | +|----|------|------| +| 1 | 电话 | 电话沟通 | +| 2 | 微信 | 微信联系 | +| 3 | 上门 | 上门拜访 | +| 4 | 短信 | 短信通知 | +| 5 | 其他 | 其他方式 | + +### 推荐状态 +| 值 | 文本 | 说明 | +|----|------|------| +| 0 | 待确认 | 等待管理员确认 | +| 1 | 有效 | 推荐有效 | +| 2 | 无效 | 推荐无效 | +| 3 | 已结算 | 推荐费已结算 | + +--- + +## 四、部署说明 + +### 4.1 数据库部署 +```bash +# 执行SQL脚本 +mysql -u root -p your_database < docs/sql/customer_lead_system.sql +``` + +### 4.2 后端部署 +1. 重新编译Java项目 +2. 部署到应用服务器 +3. 确保Mapper XML文件正确部署 + +### 4.3 前端部署 +1. Vue后台:`npm run build` 部署dist目录 +2. 小程序:使用Taro构建并上传 + +--- + +## 五、后续优化建议 + +1. **权限细化**:根据实际业务需求,配置细粒度的按钮权限 +2. **数据看板**:开发可视化数据大屏 +3. **消息通知**:接入微信模板消息,实时推送派单/跟进通知 +4. **小程序入口**:在首页增加「全民推荐」独立入口(非分销商专属) +5. **推荐海报**:生成带参数的推广海报,方便分享传播 +6. **佣金结算**:完善佣金提现流程 + +--- + +## 六、注意事项 + +1. **SQL执行顺序**:先执行数据库变更SQL,再部署后端代码 +2. **Mapper XML**:需要创建对应的Mapper XML文件 +3. **权限配置**:在后台管理系统中配置对应的菜单和按钮权限 +4. **推荐费配置**:可后续在配置表中添加推荐费比例等配置项 + +--- + +*文档生成时间:2026-04-14* diff --git a/docs/sql/customer_lead_system.sql b/docs/sql/customer_lead_system.sql new file mode 100644 index 0000000..22c231b --- /dev/null +++ b/docs/sql/customer_lead_system.sql @@ -0,0 +1,178 @@ +-- ===================================================== +-- 客资管理系统数据库变更脚本 +-- 适用于: mp-java 数据库 +-- 创建时间: 2026-04-14 +-- ===================================================== + +-- 1. 扩展客资表 - 添加派单、推荐人相关字段 +ALTER TABLE cms_contact_lead +ADD COLUMN assigned_user_id INT DEFAULT NULL COMMENT '被分配的业务员用户ID', +ADD COLUMN referrer_user_id INT DEFAULT NULL COMMENT '推荐人用户ID(全民推荐)', +ADD COLUMN referral_fee DECIMAL(10,2) DEFAULT 0.00 COMMENT '推荐费金额', +ADD COLUMN referral_fee_paid TINYINT DEFAULT 0 COMMENT '推荐费是否已支付 0否 1是', +ADD COLUMN referrer_share DECIMAL(5,2) DEFAULT 0.00 COMMENT '推荐人分成比例%', +ADD COLUMN dispatch_time DATETIME DEFAULT NULL COMMENT '派单时间', +ADD COLUMN dispatch_admin_id INT DEFAULT NULL COMMENT '派单管理员ID', +ADD COLUMN follow_count INT DEFAULT 0 COMMENT '跟进次数', +ADD COLUMN last_follow_time DATETIME DEFAULT NULL COMMENT '最后跟进时间', +ADD COLUMN appointment_time DATETIME DEFAULT NULL COMMENT '预约时间', +ADD COLUMN deal_amount DECIMAL(12,2) DEFAULT NULL COMMENT '成交金额', +ADD COLUMN deal_time DATETIME DEFAULT NULL COMMENT '成交时间', +ADD COLUMN source_type VARCHAR(20) DEFAULT 'form' COMMENT '来源类型: form表单 website网站 miniapp小程序 referral推荐人 admin录入'; + +-- 创建索引 +CREATE INDEX idx_lead_assigned ON cms_contact_lead(assigned_user_id); +CREATE INDEX idx_lead_referrer ON cms_contact_lead(referrer_user_id); +CREATE INDEX idx_lead_status ON cms_contact_lead(status); +CREATE INDEX idx_lead_source ON cms_contact_lead(source_type); + +-- 2. 派单记录表 +CREATE TABLE IF NOT EXISTS lead_dispatch ( + dispatch_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '派单ID', + lead_id INT NOT NULL COMMENT '客资ID', + from_user_id INT DEFAULT NULL COMMENT '原分配用户ID(如果重新分配)', + to_user_id INT NOT NULL COMMENT '新分配用户ID(业务员)', + admin_id INT NOT NULL COMMENT '执行派单的管理员ID', + dispatch_remarks VARCHAR(500) DEFAULT NULL COMMENT '派单备注', + dispatch_type TINYINT DEFAULT 1 COMMENT '派单类型: 1新分配 2重新分配 3抢单', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '派单时间', + INDEX idx_dispatch_lead (lead_id), + INDEX idx_dispatch_to_user (to_user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客资派单记录表'; + +-- 3. 跟进记录表 +CREATE TABLE IF NOT EXISTS lead_follow_log ( + follow_id INT PRIMARY KEY AUTO_INCREMENT, + lead_id INT NOT NULL COMMENT '客资ID', + user_id INT NOT NULL COMMENT '跟进人ID', + follow_type TINYINT NOT NULL COMMENT '跟进方式: 1电话 2微信 3上门 4短信 5其他', + follow_content VARCHAR(1000) NOT NULL COMMENT '跟进内容', + next_follow_time DATETIME DEFAULT NULL COMMENT '下次跟进时间', + next_follow_plan VARCHAR(500) DEFAULT NULL COMMENT '下次跟进计划', + attachment_urls VARCHAR(1000) DEFAULT NULL COMMENT '附件URLs(JSON数组)', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_follow_lead (lead_id), + INDEX idx_follow_user (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客资跟进记录表'; + +-- 4. 推荐人关系表(全民推荐) +CREATE TABLE IF NOT EXISTS lead_referral ( + referral_id INT PRIMARY KEY AUTO_INCREMENT, + referrer_user_id INT NOT NULL COMMENT '推荐人用户ID', + referred_lead_id INT NOT NULL COMMENT '被推荐的客资ID', + referral_code VARCHAR(32) DEFAULT NULL COMMENT '推荐码(用于匿名推荐)', + referral_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '推荐费', + referral_status TINYINT DEFAULT 0 COMMENT '推荐状态: 0待确认 1有效 2无效 3已结算', + customer_name VARCHAR(100) DEFAULT NULL COMMENT '客户姓名(匿名推荐时存储)', + customer_phone VARCHAR(20) DEFAULT NULL COMMENT '客户电话(匿名推荐时存储)', + settlement_time DATETIME DEFAULT NULL COMMENT '结算时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_referral_referrer (referrer_user_id), + INDEX idx_referral_lead (referred_lead_id), + INDEX idx_referral_code (referral_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客资推荐关系表'; + +-- 5. 用户角色扩展表(多管理员支持) +CREATE TABLE IF NOT EXISTS gxwebsoft_core.sys_user_role_extend ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL COMMENT '用户ID', + role_type VARCHAR(20) NOT NULL COMMENT '角色类型: admin管理员 salesman业务员 referrer推荐人', + is_primary TINYINT DEFAULT 1 COMMENT '是否主角色 0否 1是', + permissions JSON DEFAULT NULL COMMENT '权限配置(JSON)', + max_leads INT DEFAULT NULL COMMENT '最大客资分配数(业务员)', + commission_rate DECIMAL(5,2) DEFAULT NULL COMMENT '佣金比例%(业务员)', + referral_bonus DECIMAL(10,2) DEFAULT NULL COMMENT '推荐奖金%(推荐人)', + status TINYINT DEFAULT 1 COMMENT '状态: 0禁用 1启用', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_user_role (user_id, role_type), + INDEX idx_user (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色扩展表'; + +-- 6. 数据统计汇总表(定期生成报表用) +CREATE TABLE IF NOT EXISTS lead_statistics ( + stat_id INT PRIMARY KEY AUTO_INCREMENT, + stat_date DATE NOT NULL COMMENT '统计日期', + user_id INT DEFAULT NULL COMMENT '用户ID(Null表示全站)', + role_type VARCHAR(20) DEFAULT NULL COMMENT '角色类型', + total_leads INT DEFAULT 0 COMMENT '总客资数', + new_leads INT DEFAULT 0 COMMENT '新增客资数', + assigned_leads INT DEFAULT 0 COMMENT '已分配客资数', + followed_leads INT DEFAULT 0 COMMENT '已跟进客资数', + dealed_leads INT DEFAULT 0 COMMENT '已成交客资数', + deal_amount DECIMAL(15,2) DEFAULT 0.00 COMMENT '成交总金额', + referral_count INT DEFAULT 0 COMMENT '推荐成功数', + referral_fee DECIMAL(12,2) DEFAULT 0.00 COMMENT '推荐费总额', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_stat_date_user (stat_date, user_id, role_type), + INDEX idx_stat_date (stat_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客资数据统计表'; + +-- 7. 推荐费结算记录表 +CREATE TABLE IF NOT EXISTS lead_referral_settlement ( + settlement_id INT PRIMARY KEY AUTO_INCREMENT, + referral_id INT NOT NULL COMMENT '推荐关系ID', + referrer_user_id INT NOT NULL COMMENT '推荐人ID', + lead_id INT NOT NULL COMMENT '关联客资ID', + settlement_amount DECIMAL(10,2) NOT NULL COMMENT '结算金额', + settlement_type TINYINT DEFAULT 1 COMMENT '结算方式: 1自动 2手动', + settlement_admin_id INT DEFAULT NULL COMMENT '操作管理员ID', + settlement_remarks VARCHAR(500) DEFAULT NULL COMMENT '结算备注', + status TINYINT DEFAULT 0 COMMENT '状态: 0待确认 1已转账 2已到账', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + confirm_time DATETIME DEFAULT NULL COMMENT '确认时间', + INDEX idx_settlement_referrer (referrer_user_id), + INDEX idx_settlement_lead (lead_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐费结算记录表'; + +-- ===================================================== +-- 初始化数据 +-- ===================================================== + +-- 插入默认管理员角色扩展 +INSERT INTO gxwebsoft_core.sys_user_role_extend (user_id, role_type, is_primary, permissions, status) +SELECT user_id, 'admin', 1, '{"canDispatch": true, "canManageUsers": true, "canViewStats": true, "canSetCommission": true}', 1 +FROM gxwebsoft_core.sys_user WHERE status = 0 AND deleted = 0 LIMIT 1; + +-- ===================================================== +-- 视图: 客资完整信息视图 +-- ===================================================== +CREATE OR REPLACE VIEW v_lead_full_info AS +SELECT + l.lead_id, + l.name AS customer_name, + l.phone AS customer_phone, + l.company, + l.need AS requirement, + l.status, + l.source_type, + l.create_time, + l.dispatch_time, + l.deal_amount, + l.deal_time, + l.referral_fee, + l.referral_fee_paid, + au.user_id AS assigned_user_id, + au.nickname AS assigned_user_name, + au.real_name AS assigned_real_name, + au.phone AS assigned_user_phone, + ru.user_id AS referrer_user_id, + ru.nickname AS referrer_name, + ru.phone AS referrer_phone, + admin.user_id AS dispatch_admin_id, + admin.nickname AS dispatch_admin_name, + l.follow_count, + l.last_follow_time, + l.appointment_time, + CASE l.status + WHEN 0 THEN '待跟进' + WHEN 1 THEN '跟进中' + WHEN 2 THEN '已成交' + WHEN 3 THEN '无效' + ELSE '未知' + END AS status_text +FROM cms_contact_lead l +LEFT JOIN gxwebsoft_core.sys_user au ON l.assigned_user_id = au.user_id +LEFT JOIN gxwebsoft_core.sys_user ru ON l.referrer_user_id = ru.user_id +LEFT JOIN gxwebsoft_core.sys_user admin ON l.dispatch_admin_id = admin.user_id +WHERE l.deleted = 0; diff --git a/src/main/java/com/gxwebsoft/cms/controller/CustomerLeadController.java b/src/main/java/com/gxwebsoft/cms/controller/CustomerLeadController.java new file mode 100644 index 0000000..524e793 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/controller/CustomerLeadController.java @@ -0,0 +1,132 @@ +package com.gxwebsoft.cms.controller; + +import com.gxwebsoft.cms.param.CustomerLeadParam; +import com.gxwebsoft.cms.param.LeadDispatchParam; +import com.gxwebsoft.cms.param.LeadFollowParam; +import com.gxwebsoft.cms.service.CustomerLeadService; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 客资管理控制器 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Slf4j +@RestController +@RequestMapping("/customer/lead") +@Tag(name = "客资管理", description = "客资管理相关接口") +public class CustomerLeadController extends BaseController { + + @Autowired + private CustomerLeadService customerLeadService; + + @Operation(summary = "分页查询客资列表") + @GetMapping("/page") + @PreAuthorize("@pms.hasPermission('customer:lead:page') or @pms.hasPermission('customer:lead:view')") + public ApiResult> getLeadPage(CustomerLeadParam param) { + return success(customerLeadService.getLeadPage(param)); + } + + @Operation(summary = "获取客资详情") + @GetMapping("/detail/{leadId}") + @PreAuthorize("@pms.hasPermission('customer:lead:detail') or @pms.hasPermission('customer:lead:view')") + public ApiResult getLeadDetail( + @Parameter(description = "客资ID") @PathVariable Integer leadId) { + return success(customerLeadService.getLeadDetail(leadId)); + } + + @Operation(summary = "创建客资(管理员录入)") + @PostMapping("/create") + @PreAuthorize("@pms.hasPermission('customer:lead:create')") + public ApiResult createLead(@RequestBody CustomerLeadParam param) { + return success(customerLeadService.createLead(param)); + } + + @Operation(summary = "更新客资信息") + @PutMapping("/update") + @PreAuthorize("@pms.hasPermission('customer:lead:update')") + public ApiResult updateLead(@RequestBody CustomerLeadParam param) { + return success(customerLeadService.updateLead(param)); + } + + @Operation(summary = "更新客资状态") + @PutMapping("/status/{leadId}") + @PreAuthorize("@pms.hasPermission('customer:lead:status')") + public ApiResult updateLeadStatus( + @Parameter(description = "客资ID") @PathVariable Integer leadId, + @Parameter(description = "状态") @RequestParam Integer status, + @Parameter(description = "备注") @RequestParam(required = false) String remarks) { + return success(customerLeadService.updateLeadStatus(leadId, status, remarks)); + } + + @Operation(summary = "派单给业务员") + @PostMapping("/dispatch") + @PreAuthorize("@pms.hasPermission('customer:lead:dispatch')") + public ApiResult dispatchLead(@RequestBody LeadDispatchParam param) { + return success(customerLeadService.dispatchLead(param)); + } + + @Operation(summary = "批量派单") + @PostMapping("/dispatch/batch") + @PreAuthorize("@pms.hasPermission('customer:lead:dispatch')") + public ApiResult batchDispatchLeads(@RequestBody LeadDispatchParam param) { + return success(customerLeadService.batchDispatchLeads(param)); + } + + @Operation(summary = "添加跟进记录") + @PostMapping("/follow") + @PreAuthorize("@pms.hasPermission('customer:lead:follow')") + public ApiResult addFollowLog(@RequestBody LeadFollowParam param) { + return success(customerLeadService.addFollowLog(param)); + } + + @Operation(summary = "获取跟进历史") + @GetMapping("/follow/history/{leadId}") + @PreAuthorize("@pms.hasPermission('customer:lead:view')") + public ApiResult getFollowHistory( + @Parameter(description = "客资ID") @PathVariable Integer leadId) { + return success(customerLeadService.getFollowHistory(leadId)); + } + + @Operation(summary = "获取统计数据") + @GetMapping("/statistics") + @PreAuthorize("@pms.hasPermission('customer:lead:statistics')") + public ApiResult getStatistics(CustomerLeadParam param) { + return success(customerLeadService.getStatistics(param)); + } + + @Operation(summary = "导出客资数据") + @GetMapping("/export") + @PreAuthorize("@pms.hasPermission('customer:lead:export')") + public ApiResult exportLeads(CustomerLeadParam param) { + List> data = customerLeadService.exportLeads(param); + return success(data); + } + + @Operation(summary = "获取未分配客资") + @GetMapping("/unassigned") + @PreAuthorize("@pms.hasPermission('customer:lead:view')") + public ApiResult getUnassignedLeads() { + return success(customerLeadService.getUnassignedLeads()); + } + + @Operation(summary = "获取业务员客资统计") + @GetMapping("/salesman/stats/{salesmanId}") + @PreAuthorize("@pms.hasPermission('customer:lead:view')") + public ApiResult getSalesmanLeadStats( + @Parameter(description = "业务员ID") @PathVariable Integer salesmanId) { + return success(customerLeadService.getSalesmanLeadStats(salesmanId)); + } +} diff --git a/src/main/java/com/gxwebsoft/cms/controller/LeadReferralController.java b/src/main/java/com/gxwebsoft/cms/controller/LeadReferralController.java new file mode 100644 index 0000000..6da51d2 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/controller/LeadReferralController.java @@ -0,0 +1,106 @@ +package com.gxwebsoft.cms.controller; + +import com.gxwebsoft.cms.param.CustomerLeadParam; +import com.gxwebsoft.cms.param.LeadReferralParam; +import com.gxwebsoft.cms.service.LeadReferralService; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 推荐人控制器(全民推荐) + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Slf4j +@RestController +@RequestMapping("/lead/referral") +@Tag(name = "全民推荐", description = "推荐人报备相关接口") +public class LeadReferralController extends BaseController { + + @Autowired + private LeadReferralService leadReferralService; + + @Operation(summary = "匿名用户报备客户") + @PostMapping("/anonymous") + public ApiResult anonymousReferral(@RequestBody LeadReferralParam param) { + return success(leadReferralService.anonymousReferral(param)); + } + + @Operation(summary = "注册用户报备客户") + @PostMapping("/user") + @PreAuthorize("@pms.hasPermission('referral:user') or true") + public ApiResult userReferral(@RequestBody LeadReferralParam param) { + return success(leadReferralService.userReferral(param)); + } + + @Operation(summary = "获取推荐人的推荐记录") + @GetMapping("/page") + @PreAuthorize("@pms.hasPermission('referral:page') or @pms.hasPermission('referral:view')") + public ApiResult> getReferralPage(CustomerLeadParam param) { + return success(leadReferralService.getReferralPage(param)); + } + + @Operation(summary = "获取推荐人统计") + @GetMapping("/stats/{userId}") + @PreAuthorize("@pms.hasPermission('referral:view')") + public ApiResult getReferralStats( + @Parameter(description = "用户ID") @PathVariable Integer userId) { + return success(leadReferralService.getReferralStats(userId)); + } + + @Operation(summary = "生成推荐码") + @GetMapping("/generateCode") + @PreAuthorize("@pms.hasPermission('referral:generate')") + public ApiResult generateReferralCode() { + return success(leadReferralService.generateReferralCode()); + } + + @Operation(summary = "根据推荐码获取推荐人信息") + @GetMapping("/referrer/{code}") + public ApiResult getReferrerByCode( + @Parameter(description = "推荐码") @PathVariable String code) { + return success(leadReferralService.getReferrerByCode(code)); + } + + @Operation(summary = "确认推荐有效(管理员)") + @PutMapping("/confirm/{referralId}") + @PreAuthorize("@pms.hasPermission('referral:confirm')") + public ApiResult confirmReferral( + @Parameter(description = "推荐ID") @PathVariable Integer referralId) { + return success(leadReferralService.confirmReferral(referralId)); + } + + @Operation(summary = "作废推荐(管理员)") + @PutMapping("/invalidate/{referralId}") + @PreAuthorize("@pms.hasPermission('referral:invalidate')") + public ApiResult invalidateReferral( + @Parameter(description = "推荐ID") @PathVariable Integer referralId, + @Parameter(description = "原因") @RequestParam(required = false) String reason) { + return success(leadReferralService.invalidateReferral(referralId, reason)); + } + + @Operation(summary = "结算推荐费(管理员)") + @PutMapping("/settle/{referralId}") + @PreAuthorize("@pms.hasPermission('referral:settle')") + public ApiResult settleReferral( + @Parameter(description = "推荐ID") @PathVariable Integer referralId) { + return success(leadReferralService.settleReferral(referralId)); + } + + @Operation(summary = "批量结算推荐费") + @PutMapping("/settle/batch") + @PreAuthorize("@pms.hasPermission('referral:settle')") + public ApiResult batchSettleReferrals(@RequestBody Integer[] referralIds) { + return success(leadReferralService.batchSettleReferrals(referralIds)); + } +} diff --git a/src/main/java/com/gxwebsoft/cms/entity/CustomerLeadEntity.java b/src/main/java/com/gxwebsoft/cms/entity/CustomerLeadEntity.java new file mode 100644 index 0000000..f428c87 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/entity/CustomerLeadEntity.java @@ -0,0 +1,112 @@ +package com.gxwebsoft.cms.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 客资管理扩展实体 + * 在CmsContactLead基础上扩展派单、推荐人等字段 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cms_contact_lead") +@Schema(name = "CustomerLeadEntity", description = "客资管理扩展实体") +public class CustomerLeadEntity extends CmsContactLead implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "被分配的业务员用户ID") + private Integer assignedUserId; + + @Schema(description = "推荐人用户ID") + private Integer referrerUserId; + + @Schema(description = "推荐费金额") + private BigDecimal referralFee; + + @Schema(description = "推荐费是否已支付 0否 1是") + private Integer referralFeePaid; + + @Schema(description = "推荐人分成比例%") + private BigDecimal referrerShare; + + @Schema(description = "派单时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime dispatchTime; + + @Schema(description = "派单管理员ID") + private Integer dispatchAdminId; + + @Schema(description = "跟进次数") + private Integer followCount; + + @Schema(description = "最后跟进时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime lastFollowTime; + + @Schema(description = "预约时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime appointmentTime; + + @Schema(description = "成交金额") + private BigDecimal dealAmount; + + @Schema(description = "成交时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime dealTime; + + @Schema(description = "来源类型: form表单 website网站 miniapp小程序 referral推荐人 admin录入") + private String sourceType; + + // ========== 关联用户信息(非数据库字段)========== + @Schema(description = "被分配的业务员昵称") + private String assignedUserName; + + @Schema(description = "被分配的业务员真实姓名") + private String assignedRealName; + + @Schema(description = "被分配的业务员电话") + private String assignedUserPhone; + + @Schema(description = "推荐人昵称") + private String referrerName; + + @Schema(description = "推荐人电话") + private String referrerPhone; + + @Schema(description = "派单管理员昵称") + private String dispatchAdminName; + + @Schema(description = "业务员跟进次数统计") + private Integer userLeadCount; + + @Schema(description = "业务员本月成交数") + private Integer userDealCount; + + // ========== 来源类型常量 ========== + public static final String SOURCE_FORM = "form"; // 表单 + public static final String SOURCE_WEBSITE = "website"; // 网站 + public static final String SOURCE_MINIAPP = "miniapp"; // 小程序 + public static final String SOURCE_REFERRAL = "referral"; // 推荐人 + public static final String SOURCE_ADMIN = "admin"; // 管理员录入 + + // ========== 状态常量 ========== + public static final int STATUS_PENDING = 0; // 待跟进 + public static final int STATUS_FOLLOWING = 1; // 跟进中 + public static final int STATUS_DEALED = 2; // 已成交 + public static final int STATUS_INVALID = 3; // 无效 + +} diff --git a/src/main/java/com/gxwebsoft/cms/entity/LeadDispatch.java b/src/main/java/com/gxwebsoft/cms/entity/LeadDispatch.java new file mode 100644 index 0000000..b7498dd --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/entity/LeadDispatch.java @@ -0,0 +1,69 @@ +package com.gxwebsoft.cms.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 客资派单记录实体 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@TableName("lead_dispatch") +@Schema(name = "LeadDispatch", description = "客资派单记录") +public class LeadDispatch implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "派单ID") + @TableId(type = IdType.AUTO) + private Integer dispatchId; + + @Schema(description = "客资ID") + private Integer leadId; + + @Schema(description = "原分配用户ID") + private Integer fromUserId; + + @Schema(description = "新分配用户ID(业务员)") + private Integer toUserId; + + @Schema(description = "执行派单的管理员ID") + private Integer adminId; + + @Schema(description = "派单备注") + private String dispatchRemarks; + + @Schema(description = "派单类型: 1新分配 2重新分配 3抢单") + private Integer dispatchType; + + @Schema(description = "派单时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + // ========== 关联信息(非数据库字段)========== + @Schema(description = "业务员昵称") + private String toUserName; + + @Schema(description = "管理员昵称") + private String adminName; + + @Schema(description = "客资客户姓名") + private String leadCustomerName; + + @Schema(description = "客资客户电话") + private String leadCustomerPhone; + + // ========== 派单类型常量 ========== + public static final int TYPE_NEW = 1; // 新分配 + public static final int TYPE_REDISPATCH = 2; // 重新分配 + public static final int TYPE_GRAB = 3; // 抢单 +} diff --git a/src/main/java/com/gxwebsoft/cms/entity/LeadFollowLog.java b/src/main/java/com/gxwebsoft/cms/entity/LeadFollowLog.java new file mode 100644 index 0000000..0762330 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/entity/LeadFollowLog.java @@ -0,0 +1,84 @@ +package com.gxwebsoft.cms.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 客资跟进记录实体 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@TableName("lead_follow_log") +@Schema(name = "LeadFollowLog", description = "客资跟进记录") +public class LeadFollowLog implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "跟进ID") + @TableId(type = IdType.AUTO) + private Integer followId; + + @Schema(description = "客资ID") + private Integer leadId; + + @Schema(description = "跟进人ID") + private Integer userId; + + @Schema(description = "跟进方式: 1电话 2微信 3上门 4短信 5其他") + private Integer followType; + + @Schema(description = "跟进内容") + private String followContent; + + @Schema(description = "下次跟进时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime nextFollowTime; + + @Schema(description = "下次跟进计划") + private String nextFollowPlan; + + @Schema(description = "附件URLs(JSON数组)") + private String attachmentUrls; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + // ========== 关联信息(非数据库字段)========== + @Schema(description = "跟进人昵称") + private String userName; + + @Schema(description = "客资客户姓名") + private String customerName; + + @Schema(description = "客资客户电话") + private String customerPhone; + + // ========== 跟进方式常量 ========== + public static final int TYPE_PHONE = 1; // 电话 + public static final int TYPE_WECHAT = 2; // 微信 + public static final int TYPE_VISIT = 3; // 上门 + public static final int TYPE_SMS = 4; // 短信 + public static final int TYPE_OTHER = 5; // 其他 + + public String getFollowTypeText() { + if (this.followType == null) return ""; + return switch (this.followType) { + case TYPE_PHONE -> "电话"; + case TYPE_WECHAT -> "微信"; + case TYPE_VISIT -> "上门"; + case TYPE_SMS -> "短信"; + case TYPE_OTHER -> "其他"; + default -> ""; + }; + } +} diff --git a/src/main/java/com/gxwebsoft/cms/entity/LeadReferral.java b/src/main/java/com/gxwebsoft/cms/entity/LeadReferral.java new file mode 100644 index 0000000..b1aadac --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/entity/LeadReferral.java @@ -0,0 +1,89 @@ +package com.gxwebsoft.cms.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 推荐人关系实体(全民推荐) + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@TableName("lead_referral") +@Schema(name = "LeadReferral", description = "推荐人关系") +public class LeadReferral implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "推荐关系ID") + @TableId(type = IdType.AUTO) + private Integer referralId; + + @Schema(description = "推荐人用户ID") + private Integer referrerUserId; + + @Schema(description = "被推荐的客资ID") + private Integer referredLeadId; + + @Schema(description = "推荐码") + private String referralCode; + + @Schema(description = "推荐费") + private BigDecimal referralFee; + + @Schema(description = "推荐状态: 0待确认 1有效 2无效 3已结算") + private Integer referralStatus; + + @Schema(description = "客户姓名") + private String customerName; + + @Schema(description = "客户电话") + private String customerPhone; + + @Schema(description = "结算时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime settlementTime; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + // ========== 关联信息(非数据库字段)========== + @Schema(description = "推荐人昵称") + private String referrerName; + + @Schema(description = "推荐人电话") + private String referrerPhone; + + @Schema(description = "客资状态") + private Integer leadStatus; + + @Schema(description = "客资成交金额") + private BigDecimal dealAmount; + + // ========== 状态常量 ========== + public static final int STATUS_PENDING = 0; // 待确认 + public static final int STATUS_VALID = 1; // 有效 + public static final int STATUS_INVALID = 2; // 无效 + public static final int STATUS_SETTLED = 3; // 已结算 + + public String getStatusText() { + if (this.referralStatus == null) return ""; + return switch (this.referralStatus) { + case STATUS_PENDING -> "待确认"; + case STATUS_VALID -> "有效"; + case STATUS_INVALID -> "无效"; + case STATUS_SETTLED -> "已结算"; + default -> ""; + }; + } +} diff --git a/src/main/java/com/gxwebsoft/cms/entity/LeadStatistics.java b/src/main/java/com/gxwebsoft/cms/entity/LeadStatistics.java new file mode 100644 index 0000000..94ad726 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/entity/LeadStatistics.java @@ -0,0 +1,72 @@ +package com.gxwebsoft.cms.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 客资数据统计实体 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@TableName("lead_statistics") +@Schema(name = "LeadStatistics", description = "客资数据统计") +public class LeadStatistics implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "统计ID") + @TableId(type = IdType.AUTO) + private Integer statId; + + @Schema(description = "统计日期") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDateTime statDate; + + @Schema(description = "用户ID") + private Integer userId; + + @Schema(description = "角色类型") + private String roleType; + + @Schema(description = "总客资数") + private Integer totalLeads; + + @Schema(description = "新增客资数") + private Integer newLeads; + + @Schema(description = "已分配客资数") + private Integer assignedLeads; + + @Schema(description = "已跟进客资数") + private Integer followedLeads; + + @Schema(description = "已成交客资数") + private Integer dealedLeads; + + @Schema(description = "成交总金额") + private BigDecimal dealAmount; + + @Schema(description = "推荐成功数") + private Integer referralCount; + + @Schema(description = "推荐费总额") + private BigDecimal referralFee; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + // ========== 关联信息(非数据库字段)========== + @Schema(description = "用户昵称") + private String userName; +} diff --git a/src/main/java/com/gxwebsoft/cms/mapper/CustomerLeadMapper.java b/src/main/java/com/gxwebsoft/cms/mapper/CustomerLeadMapper.java new file mode 100644 index 0000000..48f6efe --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/mapper/CustomerLeadMapper.java @@ -0,0 +1,78 @@ +package com.gxwebsoft.cms.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.gxwebsoft.cms.entity.CustomerLeadEntity; +import com.gxwebsoft.cms.param.CustomerLeadParam; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 客资管理Mapper + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Mapper +public interface CustomerLeadMapper extends BaseMapper { + + /** + * 分页查询客资列表 + */ + List selectLeadPage(CustomerLeadParam param); + + /** + * 查询客资详情 + */ + CustomerLeadEntity selectLeadDetail(@Param("leadId") Integer leadId); + + /** + * 获取业务员待处理客资数量 + */ + Integer countBySalesman(@Param("salesmanId") Integer salesmanId, @Param("status") Integer status); + + /** + * 批量更新客资状态 + */ + int batchUpdateStatus(@Param("leadIds") List leadIds, @Param("status") Integer status); + + /** + * 派单给业务员 + */ + int dispatchToUser(@Param("leadId") Integer leadId, @Param("userId") Integer userId, + @Param("adminId") Integer adminId, @Param("remarks") String remarks); + + /** + * 统计数据 + */ + Map selectStatistics(@Param("startDate") String startDate, @Param("endDate") String endDate, + @Param("salesmanId") Integer salesmanId); + + /** + * 按状态统计 + */ + List> selectStatusStatistics(@Param("startDate") String startDate, @Param("endDate") String endDate); + + /** + * 按来源统计 + */ + List> selectSourceStatistics(@Param("startDate") String startDate, @Param("endDate") String endDate); + + /** + * 按日统计趋势 + */ + List> selectDailyTrend(@Param("startDate") String startDate, @Param("endDate") String endDate); + + /** + * 查询未分配客资 + */ + List selectUnassignedLeads(); + + /** + * 批量派单 + */ + int batchDispatch(@Param("leadIds") List leadIds, @Param("userId") Integer userId, + @Param("adminId") Integer adminId, @Param("remarks") String remarks); +} diff --git a/src/main/java/com/gxwebsoft/cms/mapper/LeadDispatchMapper.java b/src/main/java/com/gxwebsoft/cms/mapper/LeadDispatchMapper.java new file mode 100644 index 0000000..6561a88 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/mapper/LeadDispatchMapper.java @@ -0,0 +1,34 @@ +package com.gxwebsoft.cms.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.gxwebsoft.cms.entity.LeadDispatch; +import com.gxwebsoft.cms.param.CustomerLeadParam; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 客资派单Mapper + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Mapper +public interface LeadDispatchMapper extends BaseMapper { + + /** + * 查询客资的派单历史 + */ + List selectDispatchHistory(@Param("leadId") Integer leadId); + + /** + * 查询业务员的派单记录 + */ + List selectSalesmanDispatches(CustomerLeadParam param); + + /** + * 查询今日派单数量 + */ + Integer countTodayDispatches(@Param("adminId") Integer adminId); +} diff --git a/src/main/java/com/gxwebsoft/cms/mapper/LeadFollowLogMapper.java b/src/main/java/com/gxwebsoft/cms/mapper/LeadFollowLogMapper.java new file mode 100644 index 0000000..c431200 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/mapper/LeadFollowLogMapper.java @@ -0,0 +1,35 @@ +package com.gxwebsoft.cms.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.gxwebsoft.cms.entity.LeadFollowLog; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 客资跟进记录Mapper + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Mapper +public interface LeadFollowLogMapper extends BaseMapper { + + /** + * 查询客资的跟进历史 + */ + List selectFollowHistory(@Param("leadId") Integer leadId); + + /** + * 查询用户的跟进记录 + */ + List selectUserFollows(@Param("userId") Integer userId, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** + * 查询需要跟进的客资(根据下次跟进时间) + */ + List selectNeedFollowLeadIds(@Param("userId") Integer userId); +} diff --git a/src/main/java/com/gxwebsoft/cms/mapper/LeadReferralMapper.java b/src/main/java/com/gxwebsoft/cms/mapper/LeadReferralMapper.java new file mode 100644 index 0000000..5183bed --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/mapper/LeadReferralMapper.java @@ -0,0 +1,44 @@ +package com.gxwebsoft.cms.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.gxwebsoft.cms.entity.LeadReferral; +import com.gxwebsoft.cms.param.CustomerLeadParam; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 推荐人关系Mapper + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Mapper +public interface LeadReferralMapper extends BaseMapper { + + /** + * 分页查询推荐关系 + */ + List selectReferralPage(CustomerLeadParam param); + + /** + * 查询用户的推荐记录 + */ + List selectUserReferrals(@Param("userId") Integer userId); + + /** + * 根据推荐码查询 + */ + LeadReferral selectByCode(@Param("referralCode") String referralCode); + + /** + * 统计推荐人的推荐数量 + */ + Integer countUserReferrals(@Param("userId") Integer userId, @Param("status") Integer status); + + /** + * 统计推荐人待结算金额 + */ + java.math.BigDecimal sumPendingFee(@Param("userId") Integer userId); +} diff --git a/src/main/java/com/gxwebsoft/cms/param/CustomerLeadParam.java b/src/main/java/com/gxwebsoft/cms/param/CustomerLeadParam.java new file mode 100644 index 0000000..5d6c345 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/param/CustomerLeadParam.java @@ -0,0 +1,81 @@ +package com.gxwebsoft.cms.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 客资管理请求参数 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@Schema(name = "CustomerLeadParam", description = "客资管理请求参数") +public class CustomerLeadParam implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "客资ID") + private Integer leadId; + + @Schema(description = "客户姓名") + private String name; + + @Schema(description = "客户电话") + private String phone; + + @Schema(description = "单位/公司名称") + private String company; + + @Schema(description = "需求描述") + private String need; + + @Schema(description = "分配给的用户ID") + private Integer assignedUserId; + + @Schema(description = "派单备注") + private String dispatchRemarks; + + @Schema(description = "跟进状态: 0待跟进 1跟进中 2已成交 3无效") + private Integer status; + + @Schema(description = "销售备注") + private String remarks; + + @Schema(description = "预约时间") + private String appointmentTime; + + @Schema(description = "成交金额") + private BigDecimal dealAmount; + + @Schema(description = "来源类型") + private String sourceType; + + // ========== 查询条件 ========== + @Schema(description = "开始日期") + private String startDate; + + @Schema(description = "结束日期") + private String endDate; + + @Schema(description = "跟进状态(可多选,逗号分隔)") + private String statusList; + + @Schema(description = "业务员ID") + private Integer salesmanId; + + @Schema(description = "推荐人ID") + private Integer referrerId; + + @Schema(description = "关键词(姓名/电话/公司)") + private String keyword; + + @Schema(description = "当前页码") + private Integer pageNum = 1; + + @Schema(description = "每页数量") + private Integer pageSize = 10; +} diff --git a/src/main/java/com/gxwebsoft/cms/param/LeadDispatchParam.java b/src/main/java/com/gxwebsoft/cms/param/LeadDispatchParam.java new file mode 100644 index 0000000..dd46eae --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/param/LeadDispatchParam.java @@ -0,0 +1,38 @@ +package com.gxwebsoft.cms.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 客资派单请求参数 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@Schema(name = "LeadDispatchParam", description = "客资派单请求参数") +public class LeadDispatchParam implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "客资ID") + private Integer leadId; + + @Schema(description = "目标用户ID(业务员)") + private Integer toUserId; + + @Schema(description = "派单备注") + private String remarks; + + @Schema(description = "派单类型: 1新分配 2重新分配") + private Integer dispatchType = 1; + + // ========== 批量派单 ========== + @Schema(description = "批量客资ID列表") + private Integer[] leadIds; + + @Schema(description = "是否批量派单") + private Boolean batchMode = false; +} diff --git a/src/main/java/com/gxwebsoft/cms/param/LeadFollowParam.java b/src/main/java/com/gxwebsoft/cms/param/LeadFollowParam.java new file mode 100644 index 0000000..28f7322 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/param/LeadFollowParam.java @@ -0,0 +1,43 @@ +package com.gxwebsoft.cms.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 客资跟进请求参数 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@Schema(name = "LeadFollowParam", description = "客资跟进请求参数") +public class LeadFollowParam implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "客资ID") + private Integer leadId; + + @Schema(description = "跟进方式: 1电话 2微信 3上门 4短信 5其他") + private Integer followType; + + @Schema(description = "跟进内容") + private String followContent; + + @Schema(description = "下次跟进时间(yyyy-MM-dd HH:mm:ss)") + private String nextFollowTime; + + @Schema(description = "下次跟进计划") + private String nextFollowPlan; + + @Schema(description = "附件URLs(JSON数组字符串)") + private String attachmentUrls; + + @Schema(description = "是否同时更新状态") + private Boolean updateStatus = false; + + @Schema(description = "新状态(当updateStatus=true时生效): 0待跟进 1跟进中 2已成交 3无效") + private Integer newStatus; +} diff --git a/src/main/java/com/gxwebsoft/cms/param/LeadReferralParam.java b/src/main/java/com/gxwebsoft/cms/param/LeadReferralParam.java new file mode 100644 index 0000000..f9ace58 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/param/LeadReferralParam.java @@ -0,0 +1,44 @@ +package com.gxwebsoft.cms.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 推荐人报备请求参数(全民推荐) + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@Schema(name = "LeadReferralParam", description = "推荐人报备请求参数") +public class LeadReferralParam implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "推荐码(匿名推荐时使用)") + private String referralCode; + + @Schema(description = "客户姓名") + private String customerName; + + @Schema(description = "客户电话") + private String customerPhone; + + @Schema(description = "客户公司") + private String customerCompany; + + @Schema(description = "需求描述") + private String requirement; + + @Schema(description = "预约时间") + private String appointmentTime; + + @Schema(description = "备注说明") + private String remarks; + + @Schema(description = "推荐费(可选,管理员可设置默认值)") + private BigDecimal referralFee; +} diff --git a/src/main/java/com/gxwebsoft/cms/service/CustomerLeadService.java b/src/main/java/com/gxwebsoft/cms/service/CustomerLeadService.java new file mode 100644 index 0000000..1782950 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/service/CustomerLeadService.java @@ -0,0 +1,84 @@ +package com.gxwebsoft.cms.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.cms.entity.CustomerLeadEntity; +import com.gxwebsoft.cms.param.CustomerLeadParam; +import com.gxwebsoft.cms.param.LeadDispatchParam; +import com.gxwebsoft.cms.param.LeadFollowParam; + +import java.util.List; +import java.util.Map; + +/** + * 客资管理服务接口 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +public interface CustomerLeadService extends IService { + + /** + * 分页查询客资列表 + */ + Map getLeadPage(CustomerLeadParam param); + + /** + * 获取客资详情 + */ + CustomerLeadEntity getLeadDetail(Integer leadId); + + /** + * 创建客资(管理员录入) + */ + CustomerLeadEntity createLead(CustomerLeadParam param); + + /** + * 更新客资信息 + */ + boolean updateLead(CustomerLeadParam param); + + /** + * 更新客资状态 + */ + boolean updateLeadStatus(Integer leadId, Integer status, String remarks); + + /** + * 派单给业务员 + */ + boolean dispatchLead(LeadDispatchParam param); + + /** + * 批量派单 + */ + Map batchDispatchLeads(LeadDispatchParam param); + + /** + * 添加跟进记录 + */ + boolean addFollowLog(LeadFollowParam param); + + /** + * 获取客资跟进历史 + */ + List> getFollowHistory(Integer leadId); + + /** + * 获取统计数据 + */ + Map getStatistics(CustomerLeadParam param); + + /** + * 导出客资数据 + */ + List> exportLeads(CustomerLeadParam param); + + /** + * 获取未分配客资列表 + */ + List getUnassignedLeads(); + + /** + * 获取业务员的客资统计 + */ + Map getSalesmanLeadStats(Integer salesmanId); +} diff --git a/src/main/java/com/gxwebsoft/cms/service/LeadReferralService.java b/src/main/java/com/gxwebsoft/cms/service/LeadReferralService.java new file mode 100644 index 0000000..fce8ead --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/service/LeadReferralService.java @@ -0,0 +1,73 @@ +package com.gxwebsoft.cms.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.cms.entity.LeadReferral; +import com.gxwebsoft.cms.param.CustomerLeadParam; +import com.gxwebsoft.cms.param.LeadReferralParam; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 推荐人服务接口(全民推荐) + * + * @author 科技小王子 + * @since 2026-04-14 + */ +public interface LeadReferralService extends IService { + + /** + * 匿名用户通过推荐码报备客户 + */ + LeadReferral anonymousReferral(LeadReferralParam param); + + /** + * 注册用户报备客户 + */ + LeadReferral userReferral(LeadReferralParam param); + + /** + * 获取推荐人的推荐记录 + */ + Map getReferralPage(CustomerLeadParam param); + + /** + * 获取推荐人的推荐统计 + */ + Map getReferralStats(Integer userId); + + /** + * 生成推荐码 + */ + String generateReferralCode(); + + /** + * 根据推荐码获取推荐人信息 + */ + Map getReferrerByCode(String referralCode); + + /** + * 确认推荐有效 + */ + boolean confirmReferral(Integer referralId); + + /** + * 作废推荐 + */ + boolean invalidateReferral(Integer referralId, String reason); + + /** + * 结算推荐费 + */ + boolean settleReferral(Integer referralId); + + /** + * 批量结算推荐费 + */ + Map batchSettleReferrals(Integer[] referralIds); + + /** + * 计算推荐费(根据成交金额和配置比例) + */ + BigDecimal calculateReferralFee(BigDecimal dealAmount); +} diff --git a/src/main/java/com/gxwebsoft/cms/service/impl/CustomerLeadServiceImpl.java b/src/main/java/com/gxwebsoft/cms/service/impl/CustomerLeadServiceImpl.java new file mode 100644 index 0000000..79940a4 --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/service/impl/CustomerLeadServiceImpl.java @@ -0,0 +1,382 @@ +package com.gxwebsoft.cms.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.cms.entity.CustomerLeadEntity; +import com.gxwebsoft.cms.entity.LeadDispatch; +import com.gxwebsoft.cms.entity.LeadFollowLog; +import com.gxwebsoft.cms.mapper.CustomerLeadMapper; +import com.gxwebsoft.cms.mapper.LeadDispatchMapper; +import com.gxwebsoft.cms.mapper.LeadFollowLogMapper; +import com.gxwebsoft.cms.param.CustomerLeadParam; +import com.gxwebsoft.cms.param.LeadDispatchParam; +import com.gxwebsoft.cms.param.LeadFollowParam; +import com.gxwebsoft.cms.service.CustomerLeadService; +import com.gxwebsoft.common.system.entity.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 客资管理服务实现 + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Service +public class CustomerLeadServiceImpl extends ServiceImpl implements CustomerLeadService { + + @Autowired + private CustomerLeadMapper customerLeadMapper; + + @Autowired + private LeadDispatchMapper leadDispatchMapper; + + @Autowired + private LeadFollowLogMapper leadFollowLogMapper; + + /** + * 获取当前登录用户(与 BaseController.getLoginUser() 逻辑一致) + */ + private User getLoginUser() { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof User) { + return (User) principal; + } + } + } catch (Exception ignored) { + } + return null; + } + + @Override + public Map getLeadPage(CustomerLeadParam param) { + // 处理分页参数 + if (param.getPageNum() == null || param.getPageNum() < 1) { + param.setPageNum(1); + } + if (param.getPageSize() == null || param.getPageSize() < 1 || param.getPageSize() > 100) { + param.setPageSize(10); + } + + Page page = new Page<>(param.getPageNum(), param.getPageSize()); + List list = customerLeadMapper.selectLeadPage(param); + long total = page.getTotal(); + + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", total); + result.put("pageNum", param.getPageNum()); + result.put("pageSize", param.getPageSize()); + result.put("pages", (total + param.getPageSize() - 1) / param.getPageSize()); + + return result; + } + + @Override + public CustomerLeadEntity getLeadDetail(Integer leadId) { + return customerLeadMapper.selectLeadDetail(leadId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public CustomerLeadEntity createLead(CustomerLeadParam param) { + CustomerLeadEntity lead = new CustomerLeadEntity(); + lead.setName(param.getName()); + lead.setPhone(param.getPhone()); + lead.setCompany(param.getCompany()); + lead.setNeed(param.getNeed()); + lead.setStatus(CustomerLeadEntity.STATUS_PENDING); // 默认待跟进 + lead.setSourceType(CustomerLeadEntity.SOURCE_ADMIN); // 管理员录入 + lead.setReferralFee(BigDecimal.ZERO); + lead.setReferralFeePaid(0); + lead.setFollowCount(0); + + // 如果指定了业务员,直接派单 + if (param.getAssignedUserId() != null) { + User currentUser = getLoginUser(); + lead.setAssignedUserId(param.getAssignedUserId()); + lead.setDispatchTime(LocalDateTime.now()); + lead.setDispatchAdminId(currentUser != null ? currentUser.getUserId() : null); + lead.setStatus(CustomerLeadEntity.STATUS_FOLLOWING); + } + + this.save(lead); + return lead; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateLead(CustomerLeadParam param) { + CustomerLeadEntity lead = this.getById(param.getLeadId()); + if (lead == null) { + throw new RuntimeException("客资不存在"); + } + + if (StringUtils.hasText(param.getName())) { + lead.setName(param.getName()); + } + if (StringUtils.hasText(param.getPhone())) { + lead.setPhone(param.getPhone()); + } + if (StringUtils.hasText(param.getCompany())) { + lead.setCompany(param.getCompany()); + } + if (StringUtils.hasText(param.getNeed())) { + lead.setNeed(param.getNeed()); + } + if (StringUtils.hasText(param.getRemarks())) { + lead.setRemarks(param.getRemarks()); + } + if (param.getDealAmount() != null) { + lead.setDealAmount(param.getDealAmount()); + lead.setDealTime(LocalDateTime.now()); + } + + return this.updateById(lead); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateLeadStatus(Integer leadId, Integer status, String remarks) { + CustomerLeadEntity lead = this.getById(leadId); + if (lead == null) { + throw new RuntimeException("客资不存在"); + } + + lead.setStatus(status); + if (StringUtils.hasText(remarks)) { + lead.setRemarks(remarks); + } + + // 成交时记录成交时间 + if (status == CustomerLeadEntity.STATUS_DEALED) { + lead.setDealTime(LocalDateTime.now()); + } + + return this.updateById(lead); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean dispatchLead(LeadDispatchParam param) { + CustomerLeadEntity lead = this.getById(param.getLeadId()); + if (lead == null) { + throw new RuntimeException("客资不存在"); + } + + User currentUser = getLoginUser(); + Integer adminId = currentUser != null ? currentUser.getUserId() : null; + + // 更新客资分配信息 + lead.setAssignedUserId(param.getToUserId()); + lead.setDispatchTime(LocalDateTime.now()); + lead.setDispatchAdminId(adminId); + lead.setStatus(CustomerLeadEntity.STATUS_FOLLOWING); // 派单后变为跟进中 + + // 如果有备注,更新备注 + if (StringUtils.hasText(param.getRemarks())) { + String oldRemarks = lead.getRemarks() != null ? lead.getRemarks() + "\n" : ""; + lead.setRemarks(oldRemarks + "【派单备注】" + param.getRemarks()); + } + + this.updateById(lead); + + // 记录派单历史 + LeadDispatch dispatch = new LeadDispatch(); + dispatch.setLeadId(param.getLeadId()); + dispatch.setFromUserId(lead.getAssignedUserId()); // 原分配用户 + dispatch.setToUserId(param.getToUserId()); + dispatch.setAdminId(adminId); + dispatch.setDispatchRemarks(param.getRemarks()); + dispatch.setDispatchType(param.getDispatchType() != null ? param.getDispatchType() : LeadDispatch.TYPE_NEW); + dispatch.setCreateTime(LocalDateTime.now()); + leadDispatchMapper.insert(dispatch); + + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map batchDispatchLeads(LeadDispatchParam param) { + Map result = new HashMap<>(); + int successCount = 0; + int failCount = 0; + List errors = new ArrayList<>(); + + if (param.getLeadIds() == null || param.getLeadIds().length == 0) { + throw new RuntimeException("请选择要派单的客资"); + } + + User currentUser = getLoginUser(); + Integer adminId = currentUser != null ? currentUser.getUserId() : null; + + for (Integer leadId : param.getLeadIds()) { + try { + LeadDispatchParam singleParam = new LeadDispatchParam(); + singleParam.setLeadId(leadId); + singleParam.setToUserId(param.getToUserId()); + singleParam.setRemarks(param.getRemarks()); + singleParam.setDispatchType(LeadDispatch.TYPE_NEW); + + this.dispatchLead(singleParam); + successCount++; + } catch (Exception e) { + failCount++; + errors.add("客资ID[" + leadId + "]: " + e.getMessage()); + } + } + + result.put("successCount", successCount); + result.put("failCount", failCount); + result.put("errors", errors); + + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addFollowLog(LeadFollowParam param) { + CustomerLeadEntity lead = this.getById(param.getLeadId()); + if (lead == null) { + throw new RuntimeException("客资不存在"); + } + + User currentUser = getLoginUser(); + Integer userId = currentUser != null ? currentUser.getUserId() : null; + + // 添加跟进记录 + LeadFollowLog followLog = new LeadFollowLog(); + followLog.setLeadId(param.getLeadId()); + followLog.setUserId(userId); + followLog.setFollowType(param.getFollowType()); + followLog.setFollowContent(param.getFollowContent()); + followLog.setNextFollowPlan(param.getNextFollowPlan()); + followLog.setAttachmentUrls(param.getAttachmentUrls()); + followLog.setCreateTime(LocalDateTime.now()); + + if (StringUtils.hasText(param.getNextFollowTime())) { + followLog.setNextFollowTime(LocalDateTime.parse(param.getNextFollowTime().replace(" ", "T"))); + } + + leadFollowLogMapper.insert(followLog); + + // 更新客资跟进信息 + lead.setFollowCount(lead.getFollowCount() != null ? lead.getFollowCount() + 1 : 1); + lead.setLastFollowTime(LocalDateTime.now()); + + // 如果需要更新状态 + if (Boolean.TRUE.equals(param.getUpdateStatus()) && param.getNewStatus() != null) { + lead.setStatus(param.getNewStatus()); + } + + // 如果是成交 + if (param.getNewStatus() != null && param.getNewStatus() == CustomerLeadEntity.STATUS_DEALED) { + lead.setDealTime(LocalDateTime.now()); + } + + return this.updateById(lead); + } + + @Override + public List> getFollowHistory(Integer leadId) { + List logs = leadFollowLogMapper.selectFollowHistory(leadId); + List> result = new ArrayList<>(); + + for (LeadFollowLog log : logs) { + Map item = new HashMap<>(); + item.put("followId", log.getFollowId()); + item.put("followType", log.getFollowType()); + item.put("followTypeText", log.getFollowTypeText()); + item.put("followContent", log.getFollowContent()); + item.put("nextFollowTime", log.getNextFollowTime()); + item.put("nextFollowPlan", log.getNextFollowPlan()); + item.put("userName", log.getUserName()); + item.put("createTime", log.getCreateTime()); + result.add(item); + } + + return result; + } + + @Override + public Map getStatistics(CustomerLeadParam param) { + Map stats = new HashMap<>(); + + // 基础统计 + Map basicStats = customerLeadMapper.selectStatistics( + param.getStartDate(), param.getEndDate(), param.getSalesmanId()); + stats.put("basic", basicStats); + + // 按状态统计 + stats.put("byStatus", customerLeadMapper.selectStatusStatistics(param.getStartDate(), param.getEndDate())); + + // 按来源统计 + stats.put("bySource", customerLeadMapper.selectSourceStatistics(param.getStartDate(), param.getEndDate())); + + // 每日趋势 + stats.put("dailyTrend", customerLeadMapper.selectDailyTrend(param.getStartDate(), param.getEndDate())); + + return stats; + } + + @Override + public List> exportLeads(CustomerLeadParam param) { + // 设置不分页,导出全部 + param.setPageNum(1); + param.setPageSize(10000); + + List leads = customerLeadMapper.selectLeadPage(param); + List> result = new ArrayList<>(); + + for (CustomerLeadEntity lead : leads) { + Map item = new HashMap<>(); + item.put("客户姓名", lead.getName()); + item.put("联系电话", lead.getPhone()); + item.put("公司名称", lead.getCompany()); + item.put("需求描述", lead.getNeed()); + item.put("状态", lead.getStatusText()); + item.put("来源", lead.getSourceType()); + item.put("业务员", lead.getAssignedUserName()); + item.put("推荐人", lead.getReferrerName()); + item.put("跟进次数", lead.getFollowCount()); + item.put("成交金额", lead.getDealAmount()); + item.put("创建时间", lead.getCreateTime()); + item.put("派单时间", lead.getDispatchTime()); + item.put("成交时间", lead.getDealTime()); + item.put("备注", lead.getRemarks()); + result.add(item); + } + + return result; + } + + @Override + public List getUnassignedLeads() { + return customerLeadMapper.selectUnassignedLeads(); + } + + @Override + public Map getSalesmanLeadStats(Integer salesmanId) { + Map stats = new HashMap<>(); + + stats.put("total", customerLeadMapper.countBySalesman(salesmanId, null)); + stats.put("pending", customerLeadMapper.countBySalesman(salesmanId, CustomerLeadEntity.STATUS_PENDING)); + stats.put("following", customerLeadMapper.countBySalesman(salesmanId, CustomerLeadEntity.STATUS_FOLLOWING)); + stats.put("dealed", customerLeadMapper.countBySalesman(salesmanId, CustomerLeadEntity.STATUS_DEALED)); + stats.put("invalid", customerLeadMapper.countBySalesman(salesmanId, CustomerLeadEntity.STATUS_INVALID)); + + return stats; + } +} diff --git a/src/main/java/com/gxwebsoft/cms/service/impl/LeadReferralServiceImpl.java b/src/main/java/com/gxwebsoft/cms/service/impl/LeadReferralServiceImpl.java new file mode 100644 index 0000000..354713e --- /dev/null +++ b/src/main/java/com/gxwebsoft/cms/service/impl/LeadReferralServiceImpl.java @@ -0,0 +1,305 @@ +package com.gxwebsoft.cms.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.cms.entity.CustomerLeadEntity; +import com.gxwebsoft.cms.entity.LeadReferral; +import com.gxwebsoft.cms.mapper.CustomerLeadMapper; +import com.gxwebsoft.cms.mapper.LeadReferralMapper; +import com.gxwebsoft.cms.param.CustomerLeadParam; +import com.gxwebsoft.cms.param.LeadReferralParam; +import com.gxwebsoft.cms.service.CustomerLeadService; +import com.gxwebsoft.cms.service.LeadReferralService; +import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 推荐人服务实现(全民推荐) + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Service +public class LeadReferralServiceImpl extends ServiceImpl implements LeadReferralService { + + @Autowired + private LeadReferralMapper leadReferralMapper; + + @Autowired + private CustomerLeadMapper customerLeadMapper; + + @Autowired + private CustomerLeadService customerLeadService; + + @Autowired + private UserService userService; + + // 默认推荐费比例 5% + private static final BigDecimal DEFAULT_REFERRAL_RATE = new BigDecimal("0.05"); + + /** + * 获取当前登录用户(与 BaseController.getLoginUser() 逻辑一致) + */ + private User getLoginUser() { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof User) { + return (User) principal; + } + } + } catch (Exception ignored) { + } + return null; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public LeadReferral anonymousReferral(LeadReferralParam param) { + // 1. 如果有推荐码,先查询推荐人 + Integer referrerUserId = null; + if (StringUtils.hasText(param.getReferralCode())) { + LeadReferral referral = leadReferralMapper.selectByCode(param.getReferralCode()); + if (referral != null) { + referrerUserId = referral.getReferrerUserId(); + } + } + + // 2. 创建客资 + CustomerLeadEntity lead = new CustomerLeadEntity(); + lead.setName(param.getCustomerName()); + lead.setPhone(param.getCustomerPhone()); + lead.setCompany(param.getCustomerCompany()); + lead.setNeed(param.getRequirement()); + lead.setSourceType(CustomerLeadEntity.SOURCE_REFERRAL); + lead.setStatus(CustomerLeadEntity.STATUS_PENDING); + lead.setReferrerUserId(referrerUserId); + lead.setFollowCount(0); + lead.setReferralFee(BigDecimal.ZERO); + lead.setReferralFeePaid(0); + + if (StringUtils.hasText(param.getAppointmentTime())) { + lead.setAppointmentTime(LocalDateTime.parse(param.getAppointmentTime().replace(" ", "T"))); + } + + customerLeadMapper.insert(lead); + + // 3. 创建推荐关系 + LeadReferral referral = new LeadReferral(); + referral.setReferrerUserId(referrerUserId); + referral.setReferredLeadId(lead.getLeadId()); + referral.setReferralCode(StringUtils.hasText(param.getReferralCode()) ? param.getReferralCode() : null); + referral.setReferralFee(param.getReferralFee() != null ? param.getReferralFee() : BigDecimal.ZERO); + referral.setReferralStatus(LeadReferral.STATUS_PENDING); // 待确认 + referral.setCustomerName(param.getCustomerName()); + referral.setCustomerPhone(param.getCustomerPhone()); + referral.setCreateTime(LocalDateTime.now()); + + leadReferralMapper.insert(referral); + + // 4. 更新客资的推荐费 + lead.setReferralFee(referral.getReferralFee()); + customerLeadMapper.updateById(lead); + + return referral; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public LeadReferral userReferral(LeadReferralParam param) { + User currentUser = getLoginUser(); + if (currentUser == null) { + throw new RuntimeException("请先登录"); + } + + // 1. 创建客资 + CustomerLeadEntity lead = new CustomerLeadEntity(); + lead.setName(param.getCustomerName()); + lead.setPhone(param.getCustomerPhone()); + lead.setCompany(param.getCustomerCompany()); + lead.setNeed(param.getRequirement()); + lead.setSourceType(CustomerLeadEntity.SOURCE_REFERRAL); + lead.setStatus(CustomerLeadEntity.STATUS_PENDING); + lead.setReferrerUserId(currentUser.getUserId()); + lead.setFollowCount(0); + lead.setReferralFee(param.getReferralFee() != null ? param.getReferralFee() : BigDecimal.ZERO); + lead.setReferralFeePaid(0); + + if (StringUtils.hasText(param.getAppointmentTime())) { + lead.setAppointmentTime(LocalDateTime.parse(param.getAppointmentTime().replace(" ", "T"))); + } + + customerLeadMapper.insert(lead); + + // 2. 创建推荐关系 + LeadReferral referral = new LeadReferral(); + referral.setReferrerUserId(currentUser.getUserId()); + referral.setReferredLeadId(lead.getLeadId()); + referral.setReferralCode(this.generateReferralCode()); // 生成推荐码 + referral.setReferralFee(param.getReferralFee() != null ? param.getReferralFee() : BigDecimal.ZERO); + referral.setReferralStatus(LeadReferral.STATUS_PENDING); + referral.setCustomerName(param.getCustomerName()); + referral.setCustomerPhone(param.getCustomerPhone()); + referral.setCreateTime(LocalDateTime.now()); + + leadReferralMapper.insert(referral); + + return referral; + } + + @Override + public Map getReferralPage(CustomerLeadParam param) { + Page page = new Page<>(param.getPageNum(), param.getPageSize()); + List list = leadReferralMapper.selectReferralPage(param); + + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", page.getTotal()); + result.put("pageNum", param.getPageNum()); + result.put("pageSize", param.getPageSize()); + + return result; + } + + @Override + public Map getReferralStats(Integer userId) { + Map stats = new HashMap<>(); + + stats.put("totalCount", leadReferralMapper.countUserReferrals(userId, null)); + stats.put("pendingCount", leadReferralMapper.countUserReferrals(userId, LeadReferral.STATUS_PENDING)); + stats.put("validCount", leadReferralMapper.countUserReferrals(userId, LeadReferral.STATUS_VALID)); + stats.put("settledCount", leadReferralMapper.countUserReferrals(userId, LeadReferral.STATUS_SETTLED)); + stats.put("pendingAmount", leadReferralMapper.sumPendingFee(userId)); + + return stats; + } + + @Override + public String generateReferralCode() { + return "RF" + System.currentTimeMillis() + (int)(Math.random() * 100); + } + + @Override + public Map getReferrerByCode(String referralCode) { + LeadReferral referral = leadReferralMapper.selectByCode(referralCode); + if (referral == null || referral.getReferrerUserId() == null) { + return null; + } + + User referrer = userService.getByIdRel(referral.getReferrerUserId()); + if (referrer == null) { + return null; + } + + Map result = new HashMap<>(); + result.put("referrerId", referrer.getUserId()); + result.put("referrerName", referrer.getNickname()); + result.put("referrerPhone", referrer.getPhone()); + result.put("referralCode", referralCode); + + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean confirmReferral(Integer referralId) { + LeadReferral referral = this.getById(referralId); + if (referral == null) { + throw new RuntimeException("推荐记录不存在"); + } + + referral.setReferralStatus(LeadReferral.STATUS_VALID); + return this.updateById(referral); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean invalidateReferral(Integer referralId, String reason) { + LeadReferral referral = this.getById(referralId); + if (referral == null) { + throw new RuntimeException("推荐记录不存在"); + } + + referral.setReferralStatus(LeadReferral.STATUS_INVALID); + + // 同步更新客资推荐费状态 + CustomerLeadEntity lead = customerLeadMapper.selectById(referral.getReferredLeadId()); + if (lead != null) { + lead.setReferralFeePaid(-1); // 标记为已作废 + customerLeadMapper.updateById(lead); + } + + return this.updateById(referral); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean settleReferral(Integer referralId) { + LeadReferral referral = this.getById(referralId); + if (referral == null) { + throw new RuntimeException("推荐记录不存在"); + } + + if (referral.getReferralStatus() != LeadReferral.STATUS_VALID) { + throw new RuntimeException("只有有效的推荐才能结算"); + } + + referral.setReferralStatus(LeadReferral.STATUS_SETTLED); + referral.setSettlementTime(LocalDateTime.now()); + + // 同步更新客资推荐费状态 + CustomerLeadEntity lead = customerLeadMapper.selectById(referral.getReferredLeadId()); + if (lead != null) { + lead.setReferralFeePaid(1); // 已支付 + customerLeadMapper.updateById(lead); + } + + return this.updateById(referral); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map batchSettleReferrals(Integer[] referralIds) { + Map result = new HashMap<>(); + int successCount = 0; + int failCount = 0; + List errors = new ArrayList<>(); + + for (Integer id : referralIds) { + try { + this.settleReferral(id); + successCount++; + } catch (Exception e) { + failCount++; + errors.add("ID[" + id + "]: " + e.getMessage()); + } + } + + result.put("successCount", successCount); + result.put("failCount", failCount); + result.put("errors", errors); + + return result; + } + + @Override + public BigDecimal calculateReferralFee(BigDecimal dealAmount) { + if (dealAmount == null || dealAmount.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return dealAmount.multiply(DEFAULT_REFERRAL_RATE).setScale(2, RoundingMode.HALF_UP); + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/entity/UserRoleExtend.java b/src/main/java/com/gxwebsoft/common/system/entity/UserRoleExtend.java new file mode 100644 index 0000000..092973c --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/entity/UserRoleExtend.java @@ -0,0 +1,87 @@ +package com.gxwebsoft.common.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 用户角色扩展实体(支持多角色) + * + * @author 科技小王子 + * @since 2026-04-14 + */ +@Data +@TableName("sys_user_role_extend") +@Schema(name = "UserRoleExtend", description = "用户角色扩展") +public class UserRoleExtend implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "ID") + @TableId(type = IdType.AUTO) + private Integer id; + + @Schema(description = "用户ID") + private Integer userId; + + @Schema(description = "角色类型: admin管理员 salesman业务员 referrer推荐人") + private String roleType; + + @Schema(description = "是否主角色 0否 1是") + private Integer isPrimary; + + @Schema(description = "权限配置(JSON)") + private String permissions; + + @Schema(description = "最大客资分配数(业务员)") + private Integer maxLeads; + + @Schema(description = "佣金比例%(业务员)") + private BigDecimal commissionRate; + + @Schema(description = "推荐奖金%(推荐人)") + private BigDecimal referralBonus; + + @Schema(description = "状态: 0禁用 1启用") + private Integer status; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + + // ========== 关联信息(非数据库字段)========== + @Schema(description = "用户昵称") + private String userName; + + @Schema(description = "用户电话") + private String userPhone; + + @Schema(description = "当前分配的客资数") + private Integer currentLeads; + + // ========== 角色类型常量 ========== + public static final String ROLE_ADMIN = "admin"; // 管理员 + public static final String ROLE_SALESMAN = "salesman"; // 业务员 + public static final String ROLE_REFERRER = "referrer"; // 推荐人 + + public String getRoleTypeText() { + if (this.roleType == null) return ""; + return switch (this.roleType) { + case ROLE_ADMIN -> "管理员"; + case ROLE_SALESMAN -> "业务员"; + case ROLE_REFERRER -> "推荐人"; + default -> this.roleType; + }; + } +}