
简介这份CRM客户关系管理解决方案PPT面向企业信息化从业者、销售与市场管理者以及需要理解客户关系管理体系的学习者可用于内部培训、方案汇报或数字化选型参考。内容以Oracle CRM为主线系统梳理客户关系管理的商业驱动力、系统定义、客户满意度与忠诚度、客户保持与客户生命周期价值等模块并重点展开Oracle CRM在客户全景视图、实时数据分析、个性化互动、协作与移动访问、客户智能等方面的优势同时对比CRM与ERP在内部资源优化与客户导向上的分工与协同帮助读者建立从理论到落地的整体框架。资源为单个PPT文件压缩包约156.05MB篇幅与图表较充实适合直接用于讲解或二次编辑。目前已有572人学习下载可作为入门认知与方案参考的实用素材。1. CRM客户关系管理解决方案的技术骨架我见过不少团队做 CRM客户关系管理解决方案.ppt第一版就被打回来问题几乎不在配色而在第 3 页的架构图里找不到「租户边界」——销售总监关心数据谁看得见运维关心私有部署之后谁来背可用性IT 负责人关心这套东西能不能跟现有的账号体系对接。这三件事如果不在 PPT 的前五页讲清楚后面几十页的界面截图再漂亮也白搭。CRM 客户关系管理解决方案本质上是一套「以客户主数据为中心、以跟进流水为证据链、以权限切分为交付形态」的业务系统它要解决的是线索从哪来、商机卡在哪一步、销售离职后客户跟谁走这三个具体问题。这份材料适合三类人要给甲方做方案汇报的售前与架构师、准备用 RuoYi 系脚手架自建的研发负责人、以及评估免费 CRM 与私有部署差异的中小团队管理者。2. CRM解决方案的领域模型与库表设计方案 PPT 里最容易画虚的就是数据模型这一页。画三个方框写「客户—商机—订单」谁都会但真正决定这套系统能不能跑三年的是字段的取舍和索引的落位。2.1 客户、联系人、商机三张主表的字段取舍先把实体边界定下来常见做法是四张核心表加两张流水表表名承载对象关键字段是否多租户crm_customer客户主体公司name、industry、level、owner_id是crm_contact联系人人customer_id、phone、position是crm_lead线索未验证source、status、convert_time是crm_opportunity商机stage、amount、expect_close_date是crm_follow_up跟进流水biz_type、biz_id、content、next_time是crm_operate_log操作审计action、before_json、after_json是两个坑值得提前在 PPT 里标注。第一客户和线索不要合成一张表用status字段区分「线索/正式客户」看似省事实际会让线索池的回收规则、去重规则和客户归属规则纠缠在一起后期想拆分要做数据迁移。第二联系人必须独立成表并允许一个客户挂多条因为 B2B 场景里对接人换岗是常态把联系人压成客户表上的contact_name字段半年后就没法回溯「当时是谁签的字」。2.2 建表 DDL 与索引策略下面这段是可直接执行的 MySQL 8.0 建表语句客户表和商机表都带上了租户隔离键CREATE TABLE crm_customer ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, tenant_id BIGINT UNSIGNED NOT NULL COMMENT 租户ID多租户隔离键, name VARCHAR(128) NOT NULL COMMENT 客户名称, industry VARCHAR(64) DEFAULT NULL COMMENT 所属行业, level TINYINT NOT NULL DEFAULT 3 COMMENT 1重点 2普通 3观察, owner_id BIGINT UNSIGNED NOT NULL COMMENT 归属销售离职转交靠这个字段, dept_id BIGINT UNSIGNED NOT NULL COMMENT 归属部门用于部门数据权限, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0公海 2已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_tenant_name (tenant_id, name) COMMENT 同租户内客户名唯一防重复录入, KEY idx_tenant_owner (tenant_id, owner_id, status) COMMENT 我的客户列表主查询路径, KEY idx_tenant_dept (tenant_id, dept_id, status) COMMENT 部门视图查询路径 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户主表; CREATE TABLE crm_opportunity ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, tenant_id BIGINT UNSIGNED NOT NULL, customer_id BIGINT UNSIGNED NOT NULL, name VARCHAR(128) NOT NULL COMMENT 商机名称, stage VARCHAR(32) NOT NULL DEFAULT INIT COMMENT INIT/NEED/QUOTE/NEGO/WON/LOST, amount DECIMAL(14,2) NOT NULL DEFAULT 0.00, expect_close_date DATE DEFAULT NULL, owner_id BIGINT UNSIGNED NOT NULL, PRIMARY KEY (id), KEY idx_tenant_stage (tenant_id, stage, expect_close_date) COMMENT 漏斗图与预测报表都走它 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商机表;索引设计上有两点说明。uk_tenant_name把tenant_id放在最左列是因为唯一性约束必须限定在租户内部——两个不同租户都有「杭州某某科技」是完全正常的。idx_tenant_stage的列顺序按「等值列在前、范围列在后」排漏斗图和销售预测这两类报表都是先固定租户和阶段、再按预计成交日期排序走这个联合索引可以避免 filesort。至于owner_id和dept_id双写是为了支持两种数据权限模型按人、按部门如果贵司只用其中一种可以砍掉另一个索引。2.3 多租户字段的落位与查询代价tenant_id放在每张业务表的第二列而不是靠customer_id关联推导理由是避免跨表 JOIN 才能做隔离。代价是每条 SQL 都必须带tenant_id ?漏一处就是数据越权。工程上的通用解法是在 MyBatis 拦截器或 JPA 的Filter里统一注入而不是靠开发自觉手写在每个 Mapper 上。私有部署的客户往往只有一个租户这时tenant_id恒为 1索引的选择性会变差可以让 DBA 评估是否需要加WHERE tenant_id1的冗余条件或改用其他区分列。这也是「免费 CRM 与私有化部署」在数据层最实在的差别之一SaaS 版必须为多租户付出索引和维护成本私有版可以砍掉但将来想扩成多组织就又要加回来。3. CRM核心模块的可运行实现线索转商机与跟进流水架构图画完PPT 里通常还需要一页「关键流程」最好是能落到代码的那种。3.1 技术选型RuoYi 系脚手架与轻量服务的取舍国内中小团队自建 CRM常见做法是拿 RuoYi 系脚手架起步社区里有专门做办公与客户管理的衍生版本权限、部门、字典、代码生成器都是现成的能省掉两三周的基础工作量。它的代价是技术栈被锁定在 Spring Boot Vue前端模板偏后台管理风格客户看到的界面会比较「像内部系统」。另一条路是用 Flask/FastAPI 这类轻量框架自己搭接口灵活、迭代快但部门树、数据权限、操作日志这些都要自己写。选型时把这几项列进 PPT 的对比表格比只写「技术先进」有说服力基础权限与部门树脚手架自带 / 需自研代码生成器脚手架自带 / 无前后端分离部署成本中等 / 低定制化上限受模板约束 / 不受限适合团队规模20 人以上研发 / 5 人以内3.2 线索转商机的状态机与接口实现线索转化是 CRM 里唯一不能出错的写操作它同时改crm_lead、插crm_customer、可能再插crm_opportunity必须放在一个事务里。下面这段 FastAPI 代码给出可运行的最小实现from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel, Field from datetime import date router APIRouter(prefix/api/crm) # 允许的状态迁移写成白名单而不是 if-else 链 LEAD_TRANSITION { NEW: {FOLLOWING, INVALID}, FOLLOWING: {CONVERTED, INVALID}, CONVERTED: set(), # 终态不可再流转 INVALID: set(), } class ConvertReq(BaseModel): lead_id: int customer_name: str Field(min_length2, max_length128) amount: float Field(default0.0, ge0, le10**10) expect_close_date: date | None None router.post(/lead/convert) def convert_lead(req: ConvertReq, ctx: dict Depends(get_login_ctx)): tenant_id, user_id ctx[tenant_id], ctx[user_id] # 行锁防止同一个线索被两个销售同时转化 lead db.query_one( SELECT id, status FROM crm_lead WHERE id%s AND tenant_id%s FOR UPDATE, (req.lead_id, tenant_id), ) if not lead: raise HTTPException(404, 线索不存在或无权访问) if CONVERTED not in LEAD_TRANSITION.get(lead[status], set()): raise HTTPException(409, f当前状态 {lead[status]} 不允许转化) with db.transaction(): customer_id db.insert( INSERT INTO crm_customer(tenant_id, name, owner_id, dept_id, level, status) VALUES(%s,%s,%s,%s,3,1), (tenant_id, req.customer_name, user_id, ctx[dept_id]), ) db.execute( UPDATE crm_lead SET statusCONVERTED, convert_timeNOW(), customer_id%s WHERE id%s AND tenant_id%s, (customer_id, req.lead_id, tenant_id), ) if req.amount 0: db.insert( INSERT INTO crm_opportunity(tenant_id, customer_id, name, stage, amount, expect_close_date, owner_id) VALUES(%s,%s,%s,INIT,%s,%s,%s), (tenant_id, customer_id, req.customer_name 首单, req.amount, req.expect_close_date, user_id), ) return {customer_id: customer_id, stage: INIT}几个关键点解释一下。FOR UPDATE是必须的销售在列表页快速双击「转化」按钮就会并发触发两次请求没有行锁就会建出两个客户。LEAD_TRANSITION用白名单字典而不是 if-else 链好处是新增状态时只改配置也方便直接导出成 PPT 里的状态流转图。amount用ge0和le10**10做双边界校验防止误填把商机金额写成天文数字导致预测报表失真。dept_id从登录上下文取、不允许前端传否则用户可以伪造部门来绕过部门级数据权限。如果要做「飞鱼crm怎么邀请员工」这类成员管理的对应功能思路一致邀请动作本身是「在sys_user建账号 绑定租户 分配角色」三件事同样要事务化和幂等处理邀请链接建议带一次性 token 而不是明文用户 ID。3.3 跟进记录的幂等写入与提醒调度跟进流水是 CRM 里写入量最大的表也是最容易出问题的地方。移动端网络抖动导致重复提交很常见处理办法是前端生成client_uuid后端在crm_follow_up上加UNIQUE KEY uk_client_uuid (tenant_id, client_uuid)重复插入直接捕获唯一键冲突返回成功而不是报错。提醒调度常见做法是用定时任务扫next_time落在未来 30 分钟内的记录推送到站内信或企业微信扫描任务要加分布式锁否则多实例部署会重复推送。顺手一提PPT 里讲「永久在线的crm网站」这类可用性承诺时别只写 99.9%把定时任务的补偿机制、消息重试次数、失败告警通道写出来运维方才知道要不要额外投人。4. 把CRM方案做成PPTpython-pptx自动生成技术方案书标题里带.ppt说明交付物最终是一份材料。与其手动画二十页不如把可结构化的部分脚本化。4.1 先定信息骨架再谈排版一份能过评审的 CRM 客户关系管理解决方案材料常见骨架是五段业务现状与痛点1 页、总体架构与部署形态2 页、核心流程与数据模型4 页、权限与安全2 页、实施计划与报价2 页。前四段是技术内容第五段才是商务。很多技术同学栽在第三段画得太细把 ER 图放大到看不清评审席上的业务方直接走神正确做法是 ER 图只留六张核心表字段放进备注或附页。4.2 python-pptx 生成架构页与指标页下面脚本从数据库读实时指标再写进 PPT 的表格页避免出现「材料里的客户数还是上个月的」这种尴尬from pptx import Presentation from pptx.util import Inches, Pt from pptx.enum.text import PP_ALIGN prs Presentation() # 用默认模板也可传自己的模板路径 slide prs.slides.add_slide(prs.slide_layouts[5]) # 5 仅标题版式 slide.shapes.title.text 平台运行指标数据截至出稿当日 # 从数据库取数避免手工维护造成数字过期 rows db.query( SELECT tenant_id, COUNT(*) AS c, SUM(status1) AS active FROM crm_customer GROUP BY tenant_id ORDER BY c DESC LIMIT 5 ) table slide.shapes.add_table(6, 4, Inches(0.8), Inches(1.6), Inches(8.4), Inches(3.0)).table headers [租户, 客户总数, 有效客户, 有效率] for col, text in enumerate(headers): cell table.cell(0, col) cell.text text cell.text_frame.paragraphs[0].font.size Pt(14) cell.text_frame.paragraphs[0].font.bold True for i, r in enumerate(rows, start1): rate f{r[active] / r[c] * 100:.1f}% if r[c] else - for col, val in enumerate([r[tenant_id], r[c], r[active], rate]): table.cell(i, col).text str(val) table.cell(i, col).text_frame.paragraphs[0].font.size Pt(12) prs.save(crm_solution.pptx)参数上要注意三点。slide_layouts[5]是默认模板里带标题的纯内容版式如果你用公司模板版式索引会变稳妥做法是先for i, l in enumerate(prs.slide_layouts): print(i, l.name)打印一遍再选。add_table的宽高单位是 EMU 换算的 Inches行高会自动分配内容超过单元格宽度时不会自动换行撑高所以客户名这类长文本要么截断要么降字号。中文显示依赖系统字体服务器上跑脚本生成后如果出现方框需要在文本 run 上显式设置font.name 微软雅黑并且同时设置rPr的ea属性。4.3 让数字有出处图表页的取数原则凡是 PPT 里出现的数字都应当在附注里写明口径和取数时间。客户总数是「未软删除的所有记录」还是「剔除公海后的记录」差别可能有三成。我一般会在脚本里把口径写进标题例如「有效客户status1不含公海」评审时被追问直接翻到附页 SQL 就够了。若材料需要漏斗图用add_chart配合XL_CHART_TYPE.BAR_CLUSTERED数据同样从idx_tenant_stage那条查询里来不要在 PPT 里手工改数。5. 进阶租户数据权限校验与交付前自检方案讲到最后一页真正决定项目成败的是「权限漏洞有没有」。数据越权在 CRM 里比功能缺失严重得多一次跨部门看到别人的客户名单就足以让整个项目被叫停。最有效的验证手段是写一组自动化用例用不同角色的 token 去打同一个接口断言返回集合互不相交def test_cross_tenant_isolation(client_a, client_b): # A 租户建 3 个客户 for i in range(3): client_a.post(/api/crm/customer, json{name: fA客户{i}}) # B 租户只应看到自己的数据 ids_b [r[id] for r in client_b.get(/api/crm/customer).json()[data]] ids_a [r[id] for r in client_a.get(/api/crm/customer).json()[data]] assert set(ids_a) set(ids_b) set(), 存在跨租户数据泄漏 def test_dept_scope(client_sales_x, client_sales_y): # 同租户不同部门互相看不到对方的客户 ids_x {r[id] for r in client_sales_x.get(/api/crm/customer).json()[data]} ids_y {r[id] for r in client_sales_y.get(/api/crm/customer).json()[data]} assert ids_x ids_y set()这两条用例应该进 CI每次改 Mapper 或拦截器都跑一遍。踩过的坑里最常见的是「报表接口忘了加租户条件」因为报表往往用原生 SQL 绕过 ORM 的自动过滤是最容易漏的地方。交付前可以照下面这张表逐项过一遍任何一项打不了勾就先别定评审时间。检查项判定标准常见失分点租户隔离全部接口带 tenant_id 且有用例覆盖报表原生 SQL 漏传部门数据权限按人、按部门两种模型至少验证一种前端传 dept_id 可伪造客户归属转交销售离职后客户可批量转移只改 owner 不改 dept商机状态机终态不可回退有并发用例双击产生重复商机跟进流水幂等client_uuid 唯一键生效移动端重试写两条材料数字口径每个数字标注口径与取数时间手工维护导致过期可用性承诺定时任务有锁、有重试、有告警只写百分比不写机制最后提一个容易被忽略的技巧把这份自检表直接做成 PPT 的附录页。评审时如果技术方被质疑「你们怎么保证数据不串」翻到这一页逐条念比口头承诺有用得多也顺手把交付范围里的测试工作量写清楚了。本文还有配套的精品资源点击获取