4S店的客户管理,说起来就是“销售跟进潜客、售后留住老客”这两件事,但真正落地的系统却很少把这套逻辑理清楚。很多店里还在用Excel表格甚至纸质登记本来管客户资料,销售离职带走一批客户,售后保养该提醒了全靠人工打电话,客户体验和门店效率两头都顾不上。这个“基于微信小程序的4S店客户管理系统”项目,目标就是把这些线下流程搬到线上,用小程序作为客户触点,后端统一管理客户档案、跟进记录、预约工单和回访记录,让销售、售后、管理者各自看到自己该看的数据。如果你是做毕设、课设,或者真想给门店做一套轻量级CRM,这篇文章的拆解和实操记录应该能帮你少走不少弯路。
1. 需求拆解:4S店到底需要管什么
1.1 从一条线索到一次交车的完整链路
做系统之前先别急着写代码,得把业务链条捋清楚。4S店的售前端,核心流程是:获取线索、分配销售、电话跟进、邀约到店、试驾、谈单、成交交车。每个环节都有客户状态的变化,比如“新建线索”“跟进中”“已到店”“已试驾”“已成交”“已战败”。
这里最容易踩的坑,是把客户状态设计得太简单。我见过不少项目,给客户表加一个字符串字段叫status,然后就没有然后了。等到写统计报表的时候,想查“本月从试驾到成交的平均周期”,发现根本没有历史状态流转记录,只能干瞪眼。所以设计时要加一张客户状态流转表,记录每次状态变更的时间和操作人,这样后面做销售漏斗分析才有数据基础。
另外线索分配也值得单独设计。现实场景里,新线索进来后一般由销售经理手动分配给某个销售顾问,或者按展厅排班轮流分配。系统里要支持“待分配池”和“我的客户”两个视图,销售经理可以把池子里的线索批量指派给顾问,也可以回收长期未跟进的客户。
1.2 售后端:保养、维修、回访的闭环逻辑
售后端的核心不是“录个工单就完事”,而是要把车辆维保数据和客户关怀串起来。一台车什么时候该保养、上次换过什么配件、有没有保险快到期了,这些信息如果散落在不同人手里,售后顾问就没办法在客户进店时快速给出建议。
所以车辆档案表非常关键,它要跟客户表做一对多关联。也就是说,一个客户名下可以有多台车,每台车独立记录品牌、型号、VIN码、车牌号、购车日期、上次保养里程、下次保养时间。这部分数据一来方便做保养提醒,二来客户换车后再来做保养,系统能直接拉出历史维保记录,体验完全不一样。
回访管理这块,很多课设项目会直接省掉,但恰恰是4S店特别在意的功能。厂家会考核客户满意度,店里也有交车回访、保养回访、维修回访这些固定流程。系统里应该给每台成交车辆生成一个回访任务列表,销售或客服完成后填写回访结果,有投诉则自动升级到管理层处理。
1.3 为什么选微信小程序而不是App
选型的时候一定会纠结,为什么不用App或者H5?我当时的判断很简单:第一,4S店客户基本都有微信,小程序扫码即用,不需要下载安装,获客门槛低很多;第二,小程序有微信消息订阅能力,保养提醒、优惠活动通知可以直接触达客户;第三,开发成本比App低得多,一套代码前后端分离,管理后台用Web,客户和员工用小程序,省事。
但小程序也有局限,最典型的是包体积限制和用户信息授权规则。比如用户昵称和头像,以前wx.getUserInfo就能拿到,现在改成了只能拿到默认头像和“微信用户”这个默认昵称,必须让用户主动填写或者通过其他方式获取。这类边界问题开发前就要有预期,不然做到一半卡住了很影响节奏。
2. 系统架构与技术选型
2.1 整体架构:小程序端加Spring Boot后端
这个项目我采用的是标准的前后端分离结构。小程序端用原生微信小程序开发,后端用Spring Boot搭建RESTful API,数据库用MySQL,鉴权用Token机制,文件存储用本地目录加Nginx映射。整套技术栈没有引入太重的东西,对课设和实际落地都比较友好。
后端项目结构我是这样组织的:
src/main/java/com/example/crm ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // MyBatis数据访问层 ├── entity // 数据库实体 ├── dto // 接口传输对象 ├── config // 配置类(拦截器、跨域、微信参数) ├── utils // 工具类(JWT、时间处理) └── common // 统一返回结果、异常处理这里想特别说一下统一返回结果。刚开始我偷懒,每个接口返回什么结构随手写,结果前端对接的时候校验状态码得写一大堆分支。后来改成统一的Result对象,所有接口都返回{code: 0, message: "success", data: xxx},前端只用判断code是否为0,清爽很多。
数据库连接池用Druid,分页用PageHelper,ORM选MyBatis-Plus。为什么不用JPA?说实话MyBatis-Plus在复杂的多表查询和SQL调优上更可控,而且生成CRUD代码快,适合这种业务逻辑比较重的管理系统。
2.2 角色权限:四种身份四种视图
权限设计是管理系统绕不开的环节。这个项目主要有四类用户:系统管理员、销售顾问、售后专员、C端客户。管理员在小程序端也要能做审批操作,所以小程序端实际上是一个多角色入口,根据登录人身份渲染不同的菜单。
我用的方案很简单:用户表加一个role字段,后端用拦截器校验Token里的角色码,前端根据角色码控制页面显示。小程序端没有复杂的路由守卫,就在每个页面的onLoad里检查一下全局storage里的角色信息,不符合的直接跳转首页并提示无权限。
下拉菜单的权限控制粒度到按钮级别,比如销售顾问只能看到“新增跟进”“编辑客户”按钮,经理能看到“查看全部客户”“重新分配”按钮。这部分用简单的v-if就能实现,不用上太重的权限框架。
2.3 数据模型设计:五张核心表
整个系统我实际建了十几张表,但核心的业务表可以浓缩为五张:客户表、车辆表、跟进记录表、服务预约表、回访记录表。建表的时候有几点提醒:
客户表不建议把所有信息塞在一张表里。联系方式、身份信息、地址备注这类字段可能有多个,比如一个客户家里两台车、两套联系方式,分开建关联表更灵活。另外客户表和用户表(微信openid绑定表)要做区分,用户表管登录,客户表管业务,两者通过user_id关联。
时间字段统一用datetime类型,不要用varchar存字符串。之前接手过一个老系统,日期全用字符串存,排序和区间查询的时候一个头两个大,全是坑。
状态字段建议用整数枚举而不是字符串,比如状态值用0、1、2,配合注释说明含义。字符串看起来直观,但一有拼写错误就查不出来,而且不利于做统计聚合。
3. 核心功能实现与实操细节
3.1 微信登录与手机号绑定
小程序登录流程现在是固定套路:wx.login拿到code,传给后端,后端用code去微信接口换openid和session_key,再用openid查用户是否已注册。已注册就颁发Token;没注册就返回“未绑定”标记,前端引导用户走手机号授权流程。
这里要特别注意,手机号获取并不是直接调接口返回手机号,而是通过button组件的open-type="getPhoneNumber"让用户点击授权,拿到code后由后端调用微信接口换手机号。这个code是一次性的,而且有效期很短,建议拿到后马上处理。
实际开发中我遇到过一个问题:用户拒接手机号授权后,前端要能优雅处理,不能直接白屏。我的做法是在授权页加一个“暂不绑定,先逛逛”的按钮,非敏感功能允许游客浏览,等用户主动下单或预约时才强制绑定。
登录Token用的是JWT,有效时间设置为7天。小程序端每次请求在header里带Authorization字段,后端拦截器统一校验。Token过期时返回特定错误码,前端全局处理跳转登录页。
3.2 预约试驾与维保服务
预约是这个系统的高频操作,我用一张appointment表统一管理试驾、保养、维修三种预约类型。前端页面上,用户先选服务类型,再选预约日期和门店,接着选时间段,最后提交。
时间段的设计有个细节。直接用字符串存"2025-06-29 14:00-15:00"虽然直观,但是没法做冲突检测。我后来改用整型字段存储,用0到47表示一天中的48个半小时槽位,比如14:00-14:30对应28。后端校验时查一下目标日期和目标门店下这个槽位是否已满,满了就提示用户换时间。
每个预约单生成后,状态流是:待确认、已确认、已完成、已取消。客户提交预约后,门店员工端会收到新预约通知,确认后客户会收到微信订阅消息。这里要提一下微信订阅消息的限制:只能给用户推送他们主动订阅过的消息,而且一次性订阅只能推一次。所以我在用户提交预约成功后,主动弹出一个订阅授权,让用户“允许”接收预约结果通知,这样门店确认后才有推送资格。
// 预约时间冲突检测的核心逻辑(简化版) private boolean isSlotAvailable(Integer shopId, String date, Integer slot) { LambdaQueryWrapper<Appointment> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Appointment::getShopId, shopId) .eq(Appointment::getAppointmentDate, date) .eq(Appointment::getAppointmentSlot, slot) .in(Appointment::getStatus, Arrays.asList(0, 1)); // 待确认+已确认 return baseMapper.selectCount(wrapper) < SHOP_SLOT_LIMIT; }这个接口在项目里还配了一个Redis锁,防止并发预约同一天同一槽位导致超卖。虽然单店场景下并发达不到很高,但养成这个习惯没坏处。
3.3 跟进记录与客户状态管理
跟进功能是销售最常用的模块,它要解决的痛点是:客户聊到哪一步了、上次联系是什么时候、下次什么时候再跟进。我在跟进记录表里存了跟进时间、跟进方式、跟进内容、约见时间、下次跟进时间。
前端的小程序端有一个待办列表,把“今天该跟进的客户”按时间排序展示。逻辑很简单,就是查follow_record表里next_follow_time小于等于今天的记录,按客户分组取最新一条。这个功能做出来后,销售顾问粘性很高,因为它替代了人手记事的流程。
客户状态管理我建议用状态机而不是自由文本。系统预置固定状态流转规则,比如“新建线索”只能流转到“跟进中”,“跟进中”可以流转到“已到店”或“已战败”,“已到店”可以流转到“已试驾”或“待跟单”。这样可以防止销售乱填,也方便后续做转化率统计。
表格设计上,客户状态字段冗余在customer表里,但状态变更流水另外存一张表。查询客户列表时直接查冗余字段,速度快;分析漏斗时查流水表,数据全。这就是典型的空间换时间思路。
3.4 员工端视图与经营数据看板
C端客户用小程序查预约、查维保记录,但真正高频使用的是员工端。销售顾问登录后能看到自己的客户池、今日待办、本月业绩;售后专员看到的是预约工单列表;经理和管理员看到的是全店看板。
数据看板我是用ECharts在小程序端渲染的,主要展示四个指标:本月新增线索数、线索转化率、预约到店率、客户满意度评分。这些数据在后端用SQL聚合,前端直接渲染不用算。
统计接口的SQL我贴一段供参考:
-- 月度线索转化率统计 SELECT COUNT(DISTINCT CASE WHEN status = 4 THEN id END) AS deal_count, COUNT(DISTINCT id) AS total_count, ROUND(COUNT(DISTINCT CASE WHEN status = 4 THEN id END) / COUNT(DISTINCT id) * 100, 2) AS deal_rate FROM t_customer WHERE create_time BETWEEN #{startTime} AND #{endTime} AND owner_id = #{employeeId}用DISTINCT是因为客户可能有多条跟进记录,必须去重用客户ID聚合。这个细节如果忘了,统计数字会虚高,查问题能查一天。
4. 实际开发中踩过的坑与排查思路
4.1 接口联调:域名校验与真机预览
小程序开发有个让新手抓狂的限制:真机上request请求的域名必须是HTTPS且在后台配置过白名单。开发阶段可以在开发者工具里勾选“不校验合法域名”暂时绕过,但真机预览时这个选项不生效。
我第一次做的时候没注意,开发者工具里一切正常,一用手机扫码就是“request:fail”。排查了半天,最后发现是没开HTTPS。后来老老实实给后端配了Nginx加SSL证书,并在小程序后台把公网域名加进request合法域名,问题才解决。
这里给个建议:如果局域网调试,可以在开发者工具中开启“不校验合法域名”,然后用电脑的局域网IP访问后端接口,开发效率会高很多。到正式部署时再换成公网HTTPS域名。
4.2 用户信息的授权规则变化
前几年小程序可以直接调wx.getUserInfo弹窗获取用户昵称头像,现在这个接口返回的已经是匿名信息了。头像是一张灰色默认图,昵称统一是“微信用户”。如果业务上需要真实昵称头像,只能使用新的头像昵称填写能力,在页面上放一个button,引导用户手动选头像填昵称。
我的处理方式比较务实:不强制用户填昵称,直接用微信手机号绑定后,把手机号作为默认称呼显示。昵称只作为选填字段,在个人中心里用户可以自己改。对4S店来说,手机号比昵称重要一百倍,销售打电话联系客户才是正经事。
4.3 自定义导航栏与安全区适配
小程序页面的导航栏,默认样式在不同机型上表现还算统一,但如果你想做沉浸式头图,就得自己配导航栏。一旦开启了自定义导航栏,就必须要考虑状态栏高度。
获取状态栏高度用wx.getWindowInfo(),胶囊按钮位置用wx.getMenuButtonBoundingClientRect(),然后动态计算导航栏高度。网上很多代码直接写死一个高度,在iPhone上没问题,一到安卓全面屏就错位。最好统一封装一个工具方法,每次进页面先调用,动态设置样式。
我实际测试下来,iPhone 13 Pro和某安卓全面屏机型的胶囊位置差了将近20像素,如果写死,自定义导航栏要么叠字,要么按钮点不到。这个适配逻辑建议放到公共组件里,所有页面复用。
4.4 下拉刷新与列表分页的配合
客户列表和预约列表都用到了分页加载。一开始我图省事,下拉刷新直接重新请求第一页数据,然后替换整个列表。结果快速滑动时会看到列表闪烁,体验很差。
后来改成用wx.startPullDownRefresh刷新时保留原数据,把新数据和原数据做合并去重,然后按排序字段重新排。虽然代码量多了一点,但体验确实顺滑很多。另外一个隐藏问题是分页加载后页数要往上加,不然翻到第二页后下拉刷新,只会显示第一页数据。
5. 项目心得与后续扩展方向
5.1 这个项目的核心设计教训
做完这个系统,我最大的体会是:业务理解比技术选型重要。技术栈再新,如果客户状态的流转逻辑没理清,统计口径对不上,系统做出来也只是个录数据的电子表格。反而是业务链路想透了,表和接口的设计水到渠成。
第二个教训是,涉及到时间、金额、状态这类字段,一定要做约束和校验。比如预约时间不能选过去的日期,金额不能为负数,状态变更要写日志。这些防御性编程看起来很琐碎,但正是它们决定了系统能不能真正投入使用。
5.2 后续可以扩展的方向
这套系统目前已经覆盖了销售、售后、预约、回访几个核心场景,真要拿到4S店场景里应用,还能扩展不少内容:
- 加装微信支付后,客户可以直接在线支付维修保养费用,省去前台排队结账环节。
- 增加会员积分体系,签到、评价、推荐朋友都能获得积分,积分可以抵扣保养工时费。
- 对接地图API,预约成功后给客户推送门店导航卡片,减少找不到路的客诉。
- 管理后台做更细的数据分析,比如单车产值、进店频次、流失预警,帮助门店发现经营问题。
在没接触这个项目之前,我总以为客户管理系统就是简单的增删改查。真做下来才明白,业务规则和细节处理才是真正的难点。希望这篇文章能帮到正在做类似项目的朋友,少踩几个我已经替你踩过的坑。如果你在开发过程中遇到具体问题,欢迎在评论区留言交流。