CRM系統(tǒng)設(shè)計(jì)與實(shí)踐)
一個(gè)叫 DeskcommCRM 的項(xiàng)目聽(tīng)起來(lái)有點(diǎn)像某個(gè)內(nèi)部系統(tǒng)的代號(hào)但實(shí)際上它代表了一類(lèi)特別值得聊的東西以“溝通”為核心的客戶(hù)關(guān)系管理工具。我一開(kāi)始接觸這個(gè)項(xiàng)目名的時(shí)候第一反應(yīng)是“Deskcomm”可能是 Desk Communication 的縮寫(xiě)也就是說(shuō)這套系統(tǒng)從一開(kāi)始就不是奔著“管客戶(hù)檔案”去的而是奔著“讓每一次跟客戶(hù)打交道的溝通都沉淀下來(lái)”這個(gè)目標(biāo)去的。這篇就把這個(gè)項(xiàng)目的完整拆解寫(xiě)出來(lái)從產(chǎn)品定位、技術(shù)選型到實(shí)操落地把我自己踩過(guò)的坑和驗(yàn)證過(guò)的方案都攤開(kāi)講。如果你正在猶豫要不要自建一套 CRM或者你手里正好有個(gè)類(lèi)似 DeskcommCRM 的半成品項(xiàng)目想盤(pán)活這篇非常值得往下看。我不會(huì)給你講那種聽(tīng)起來(lái)很美、落地就廢的“大廠架構(gòu)”我講的是小團(tuán)隊(duì)、30人以下規(guī)模、一年能維護(hù)得起、真能天天用起來(lái)的東西。1. 項(xiàng)目定位為什么叫 DeskcommCRM它到底要解決什么問(wèn)題1.1 核心需求拆解溝通記錄才是客戶(hù)管理的命脈很多團(tuán)隊(duì)對(duì) CRM 的理解就是“客戶(hù)信息表”能錄入公司名、聯(lián)系人、電話(huà)、地址頂多再加個(gè)跟進(jìn)階段。但實(shí)際用過(guò)就知道這類(lèi)表只有在填的時(shí)候有存在感填完之后就成了數(shù)據(jù)墳?zāi)箾](méi)人看也沒(méi)人更新。DeskcommCRM 這個(gè)名字里藏著它的核心主張客戶(hù)的真實(shí)狀態(tài)不在靜態(tài)字段里而在動(dòng)態(tài)溝通里。比如你今天跟一個(gè)客戶(hù)打了20分鐘電話(huà)聊到預(yù)算審批卡在財(cái)務(wù)環(huán)節(jié)這個(gè)信息如果不記下來(lái)三天后換個(gè)人跟進(jìn)又要從“您好我是……”重新開(kāi)始。溝通記錄一旦斷裂客戶(hù)體驗(yàn)就瞬間掉回“你們到底有沒(méi)有在認(rèn)真對(duì)待我”的階段。所以這個(gè)項(xiàng)目的出發(fā)點(diǎn)非常明確圍繞溝通來(lái)組織所有客戶(hù)數(shù)據(jù)把電話(huà)、郵件、線(xiàn)下拜訪(fǎng)、在線(xiàn)聊天全部匯到一條時(shí)間線(xiàn)上讓每個(gè)客戶(hù)的歷史狀態(tài)一目了然。這個(gè)定位直接決定了后面所有功能模塊的設(shè)計(jì)優(yōu)先級(jí)——不是先做統(tǒng)計(jì)報(bào)表而是先做記錄字段和時(shí)間線(xiàn)展示。1.2 適用場(chǎng)景與目標(biāo)用戶(hù)什么樣的小團(tuán)隊(duì)適合自建DeskcommCRM 這種輕量級(jí)、溝通優(yōu)先的 CRM尤其適合下面幾類(lèi)場(chǎng)景銷(xiāo)售團(tuán)隊(duì)規(guī)模在3到50人之間用免費(fèi)表格工具已經(jīng)覺(jué)得亂買(mǎi)大型 CRM 又用不滿(mǎn)也付不起費(fèi)用。業(yè)務(wù)方式以電話(huà)、微信、線(xiàn)下見(jiàn)面為主客戶(hù)接觸點(diǎn)分散在手機(jī)、郵件、聊天軟件里需要一個(gè)地方把零散信息聚合起來(lái)。企業(yè)服務(wù)、咨詢(xún)、外貿(mào)、項(xiàng)目制交付這類(lèi)行業(yè)客戶(hù)周期長(zhǎng)、參與決策的人多光靠“銷(xiāo)售漏斗”管不了那么長(zhǎng)的鏈路需要按客戶(hù)對(duì)象完整回顧上下文。團(tuán)隊(duì)里面有人懂一點(diǎn)技術(shù)愿意折騰開(kāi)源方案或低代碼平臺(tái)不想把客戶(hù)數(shù)據(jù)托管到第三方商業(yè)平臺(tái)。說(shuō)實(shí)話(huà)如果團(tuán)隊(duì)規(guī)模超過(guò)100人或者業(yè)務(wù)是純粹的快消型在線(xiàn)零售我并不建議自己折騰這類(lèi)系統(tǒng)。那種場(chǎng)景需要的是標(biāo)準(zhǔn)化的訂單流和營(yíng)銷(xiāo)自動(dòng)化直接用成熟的商業(yè)化產(chǎn)品更劃算。但對(duì)上面四個(gè)畫(huà)像來(lái)說(shuō)DeskcommCRM 的模式剛好卡在一個(gè)很舒服的位置不重、不貴、可定制。1.3 為什么不做“大而全”先有溝通深度再談業(yè)務(wù)寬度項(xiàng)目開(kāi)過(guò)幾次評(píng)審會(huì)之后最容易被需求淹沒(méi)。今天銷(xiāo)售提要做業(yè)績(jī)排行明天客服提要做工單分派后天老板說(shuō)要上 BI 大屏。每一條聽(tīng)起來(lái)都對(duì)但真要全做三個(gè)月連個(gè)能穩(wěn)定記錄客戶(hù)跟進(jìn)版本都未必能上線(xiàn)。所以 DeskcommCRM 從一開(kāi)始劃定了一條邊界只做四件事。第一沉淀客戶(hù)與聯(lián)系人的完整檔案第二記錄所有形式的溝通第三用任務(wù)和提醒把溝通變成行動(dòng)第四用最簡(jiǎn)的看板讓管理者看清楚大家到底有沒(méi)有在跟客戶(hù)保持聯(lián)系。至于復(fù)雜的工作流審批、深度的銷(xiāo)售預(yù)測(cè)算法、多渠道營(yíng)銷(xiāo)自動(dòng)化統(tǒng)統(tǒng)不做留接口等需要的時(shí)候再接。這個(gè)克制在實(shí)操中非常關(guān)鍵。一個(gè)小團(tuán)隊(duì)無(wú)論自研還是定制最怕的不是功能少而是上線(xiàn)后沒(méi)人用。功能設(shè)計(jì)得越聚焦學(xué)習(xí)成本越低用戶(hù)越容易養(yǎng)成“每次溝通完順手記一條”的習(xí)慣而讓人愿意持續(xù)記錄恰恰是 CRM 項(xiàng)目成功與否的分水嶺。2. 技術(shù)選型DeskcommCRM 這類(lèi)系統(tǒng)用什么技術(shù)棧最穩(wěn)妥2.1 后端與數(shù)據(jù)庫(kù)PostgreSQL 加一套自己熟悉的 Web 框架技術(shù)選型沒(méi)有銀彈只有“當(dāng)前團(tuán)隊(duì)最熟且修得起”的方案。我給 DeskcommCRM 定技術(shù)方案的時(shí)候核心原則就兩條生態(tài)成熟、資料多故障排查難度低。后端如果團(tuán)隊(duì)是 Python 背景建議直接用 Django 或 FastAPI。Django 自帶的 Admin 后臺(tái)在項(xiàng)目早期太有用了客戶(hù)數(shù)據(jù)還沒(méi)導(dǎo)出報(bào)表之前直接拿 Admin 頁(yè)面頂一陣子完全夠用。FastAPI 則更輕便接口開(kāi)發(fā)效率高適合前后端分離的結(jié)構(gòu)配合 SQLAlchemy 操作 PostgreSQL 很順手。數(shù)據(jù)庫(kù)推薦 PostgreSQL理由倒不是因?yàn)椤氨?MySQL 高級(jí)”而是 PG 在幾個(gè) CRM 核心場(chǎng)景里確實(shí)少踩坑聯(lián)系人、客戶(hù)、標(biāo)簽之間復(fù)雜關(guān)聯(lián)查詢(xún)時(shí)PG 的 JSONB 字段可以很靈活地存動(dòng)態(tài)屬性不需要頻繁改表結(jié)構(gòu)。時(shí)間線(xiàn)類(lèi)查詢(xún)通常伴隨多條件過(guò)濾和排序PG 的索引優(yōu)化空間大數(shù)據(jù)量大了也能扛。后面如果要加全文搜索或者地理位置篩選PG 內(nèi)置支持不需要額外接 ES 或者單獨(dú)的搜索服務(wù)。2.2 前端方案管理后臺(tái)類(lèi)應(yīng)用優(yōu)先考慮成熟組件庫(kù)DeskcommCRM 剛啟動(dòng)時(shí)團(tuán)隊(duì)糾結(jié)過(guò)要不要上 React后來(lái)實(shí)際做下來(lái)發(fā)現(xiàn)管理后臺(tái)類(lèi)型的系統(tǒng)最怕的不是框架不夠高級(jí)而是組件細(xì)節(jié)磨死人。今天一個(gè)日期選擇器要漢化明天一個(gè)表格要固定列后天一個(gè)富文本編輯器要調(diào)樣式用成熟組件庫(kù)能省掉大量重復(fù)造輪子時(shí)間。我這邊實(shí)測(cè)下來(lái)比較順的組合是 Vue 3 Element Plus 或者 React Ant Design兩者選一套團(tuán)隊(duì)熟悉的就行。關(guān)鍵技巧是不要一上來(lái)就做 20 個(gè)頁(yè)面先做三個(gè)核心頁(yè)面反復(fù)打磨客戶(hù)列表頁(yè)、客戶(hù)詳情頁(yè)時(shí)間線(xiàn)在里面、任務(wù)看板頁(yè)。這三個(gè)頁(yè)面撐起了 CRM 的日常高頻操作把這三條鏈路做順項(xiàng)目就已經(jīng)成功一大半。前端和后端之間用 JSON 接口對(duì)接鑒權(quán)用 JWT 就行。不要聽(tīng)人忽悠直接上 OAuth2.0 多服務(wù)網(wǎng)關(guān)30人的團(tuán)隊(duì)壓根用不到那套復(fù)雜度。等哪天真的要從釘釘或企業(yè)微信自動(dòng)登錄了再在 JWT 前面掛一個(gè) OIDC 代理也不遲。2.3 部署與文件存儲(chǔ)Docker 化是成本最低的運(yùn)維方案關(guān)于部署方案我特別想提醒一點(diǎn)CRM 里的附件存儲(chǔ)問(wèn)題比代碼本身更容易讓項(xiàng)目翻車(chē)。客戶(hù)發(fā)來(lái)的合同 PDF、溝通截圖、報(bào)價(jià)單如果不能穩(wěn)定地上傳、預(yù)覽、歸檔用戶(hù)很快會(huì)對(duì)系統(tǒng)失去信任。對(duì)于多數(shù)小團(tuán)隊(duì)文件直接存本地磁盤(pán)是能接受的但一定要做到統(tǒng)一封裝接口不要讓業(yè)務(wù)代碼直接寫(xiě)本地路徑。本地磁盤(pán)方案是成本最低的選擇數(shù)據(jù)量小的時(shí)候完全夠用。如果后續(xù)文件多了可以遷移到 S3 或國(guó)內(nèi)云的對(duì)象存儲(chǔ)業(yè)務(wù)代碼不用大改。其實(shí)一般體量的團(tuán)隊(duì)一年撐死存幾十 GB 的文件真不必在這個(gè)問(wèn)題上過(guò)度設(shè)計(jì)。部署上只要三步走就行后端打 Docker 鏡像、前端跑 Nginx 容器、數(shù)據(jù)庫(kù)掛 volume 持久化。用 Docker Compose 把三個(gè)服務(wù)串起來(lái)一套腳本搞定。別學(xué)大廠上 Kubernetes一臺(tái)單機(jī)加定期備份腳本穩(wěn)定性和維護(hù)成本對(duì)一個(gè)小團(tuán)隊(duì)來(lái)說(shuō)剛剛好。3. 核心功能與業(yè)務(wù)模型DeskcommCRM 的數(shù)據(jù)庫(kù)設(shè)計(jì)思路3.1 客戶(hù)與聯(lián)系人一對(duì)多模型最容易犯的錯(cuò)CRM 里最基礎(chǔ)也最容易設(shè)計(jì)錯(cuò)的就是“客戶(hù)”和“聯(lián)系人”的關(guān)系。不少系統(tǒng)偷懶把聯(lián)系人的手機(jī)號(hào)直接存在客戶(hù)表里導(dǎo)致一個(gè)客戶(hù)公司里三個(gè)不同決策人時(shí)數(shù)據(jù)庫(kù)里恨不得建三條“客戶(hù)”。這給后續(xù)跟進(jìn)埋下巨坑。DeskcommCRM 采用的是標(biāo)準(zhǔn)的一對(duì)多模型客戶(hù)表customer存公司主體信息企業(yè)名稱(chēng)、行業(yè)、規(guī)模、地址、來(lái)源渠道、所有者。聯(lián)系人表contact存?zhèn)€體信息姓名、職位、電話(huà)、微信、郵箱、備注等通過(guò) customer_id 指向所屬客戶(hù)。這樣設(shè)計(jì)有幾個(gè)直接好處??蛻?hù)名稱(chēng)是唯一的數(shù)據(jù)統(tǒng)計(jì)不會(huì)扭曲聯(lián)系人是活的一個(gè)人跳槽到另一個(gè)公司歷史溝通記錄還能跟著人走而不丟失對(duì)原客戶(hù)的記憶跟進(jìn)時(shí)可以分別指定“聯(lián)系誰(shuí)”和“關(guān)聯(lián)哪個(gè)客戶(hù)”場(chǎng)景更清晰。我甚至建議在聯(lián)系人表里加一個(gè)“離職狀態(tài)”字段搞個(gè)軟刪除。這樣如果銷(xiāo)售人員離職或者客戶(hù)舊聯(lián)系人離開(kāi)公司系統(tǒng)里還留著記錄不會(huì)因?yàn)閯h除操作把歷史時(shí)間線(xiàn)連帶毀掉。3.2 跟進(jìn)記錄與時(shí)間線(xiàn)數(shù)據(jù)要不能被改要只能追加這個(gè)模塊是整個(gè) DeskcommCRM 的核心設(shè)計(jì)思路上要參考的是“記賬本”而不是“記事本”。記賬本意味著每一條跟進(jìn)記錄一旦創(chuàng)建就只能追加補(bǔ)充不允許修改或刪除內(nèi)容??蛻?hù)溝通和財(cái)務(wù)賬目有個(gè)共通點(diǎn)——它們都是有時(shí)間先后邏輯和證據(jù)價(jià)值的。如果允許銷(xiāo)售隨意把“客戶(hù)明確說(shuō)預(yù)算不足”改成“客戶(hù)非常有興趣”那這個(gè)系統(tǒng)里數(shù)據(jù)的可信度就沒(méi)了管理者再也不敢拿它來(lái)做判斷。跟進(jìn)記錄表至少要包含這些字段關(guān)聯(lián)客戶(hù)與聯(lián)系人溝通類(lèi)型電話(huà)、微信、郵件、拜訪(fǎng)、線(xiàn)上會(huì)議溝通方向呼出/呼入溝通時(shí)間精確到分鐘內(nèi)容正文純文本加少量格式拒絕大段 HTML跟進(jìn)人下一步動(dòng)作的關(guān)聯(lián)任務(wù)可選時(shí)間線(xiàn)展示時(shí)按照“客戶(hù) - 聯(lián)系人 - 溝通記錄”的層級(jí)把所有內(nèi)容倒序鋪開(kāi)。順手做一個(gè)“全天時(shí)間線(xiàn)”視圖讓每個(gè)人看到自己今天所有跟進(jìn)記錄這個(gè)功能對(duì)團(tuán)隊(duì)養(yǎng)成記錄習(xí)慣特別有幫助。3.3 任務(wù)與提醒功能CRM 能不能用起來(lái)就靠這一關(guān)再好的溝通記錄如果“記完就完了”而沒(méi)有后續(xù)行動(dòng)那 CRM 很快就會(huì)淪為閑置系統(tǒng)。DeskcommCRM 里任務(wù)模塊的存在意義就是把一次溝通的終點(diǎn)變成下一次溝通的起點(diǎn)。每條跟進(jìn)記錄都可以順手創(chuàng)建一個(gè)任務(wù)比如“下周二跟進(jìn)報(bào)價(jià)反饋”、“三天后確認(rèn)合同郵寄地址”。任務(wù)表設(shè)計(jì)上建議包含這幾個(gè)字段任務(wù)標(biāo)題、描述關(guān)聯(lián)客戶(hù)、聯(lián)系人負(fù)責(zé)人截止日期優(yōu)先級(jí)狀態(tài)待辦、進(jìn)行中、已完成、已取消任務(wù)視圖我強(qiáng)烈建議直接做成“今日待辦”模式每天打開(kāi)系統(tǒng)第一眼就是今天該干的事。用完一個(gè)劃掉一個(gè)視覺(jué)效果和 To-Do App 一樣銷(xiāo)售沒(méi)有心理負(fù)擔(dān)更容易堅(jiān)持下來(lái)。3.4 標(biāo)簽與篩選動(dòng)態(tài)適配業(yè)務(wù)而不是靠二次開(kāi)發(fā)小團(tuán)隊(duì)的客戶(hù)管理方式千奇百怪有的按行業(yè)分、有的按城市分、有的按購(gòu)買(mǎi)意向分。如果把這些維度全坐成系統(tǒng)字段業(yè)務(wù)一變就要改表開(kāi)發(fā)人員被拖死在修改字段的路上。DeskcommCRM 采用的是標(biāo)簽系統(tǒng)方案系統(tǒng)內(nèi)置幾個(gè)基礎(chǔ)標(biāo)簽比如“高意向”“已簽約”“流失風(fēng)險(xiǎn)”同時(shí)允許每個(gè)用戶(hù)自己新增標(biāo)簽然后通過(guò)標(biāo)簽自由組合篩選客戶(hù)。比如可以快速篩選出“標(biāo)簽含高意向且昨天有跟進(jìn)記錄”的客戶(hù)。數(shù)據(jù)庫(kù)里加一張標(biāo)簽表和一張關(guān)聯(lián)表本質(zhì)上就是多對(duì)多關(guān)系。這個(gè)方案彈性和擴(kuò)展性很強(qiáng)既不要預(yù)知業(yè)務(wù)未來(lái)怎么變又能在后期幾乎不改代碼的情況下適配新場(chǎng)景。4. 實(shí)操過(guò)程從零把 DeskcommCRM 跑起來(lái)的核心步驟4.1 環(huán)境初始化用 Docker Compose 把基礎(chǔ)服務(wù)拉起來(lái)實(shí)操部分我直接把示例技術(shù)棧定為 FastAPI Vue 3 PostgreSQL。這套組合對(duì)于 Python 或 JS 開(kāi)發(fā)者都相對(duì)友好。第一步是寫(xiě)項(xiàng)目根目錄的 docker-compose.yml把 PostgreSQL 和后續(xù)的后端服務(wù)一塊編排起來(lái)。核心配置文件可以這么寫(xiě)version: 3.8 services: db: image: postgres:14 container_name: deskcomm_db restart: unless-stopped environment: POSTGRES_USER: deskcomm POSTGRES_PASSWORD: deskcomm_password POSTGRES_DB: deskcomm_crm volumes: - ./pg_data:/var/lib/postgresql/data ports: - 5432:5432 backend: build: ./backend container_name: deskcomm_api restart: unless-stopped depends_on: - db environment: DATABASE_URL: postgresql://deskcomm:deskcomm_passworddb:5432/deskcomm_crm ports: - 8000:8000 volumes: - ./backend:/app frontend: build: ./frontend container_name: deskcomm_web restart: unless-stopped ports: - 8080:80 depends_on: - backend可能有經(jīng)驗(yàn)的人會(huì)注意到backend 直接把代碼目錄掛載進(jìn)去是開(kāi)發(fā)模式的做法方便熱更新。正式部署時(shí)去掉這行 volume 掛載編譯產(chǎn)物打進(jìn)鏡像里更干凈。4.2 建表與遷移從客戶(hù)表和跟進(jìn)記錄表起步項(xiàng)目一開(kāi)始不要“建全套表”先把核心的三張表建清楚就夠跑了customer客戶(hù)表、contact聯(lián)系人表、follow_up跟進(jìn)記錄表。后續(xù)再加 task 和 tag 都來(lái)得及。建表語(yǔ)句我用 SQLAlchemy 模型來(lái)寫(xiě)因?yàn)楹竺嬉詣?dòng)生成遷移腳本。客戶(hù)表的模型大致為from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, func from sqlalchemy.orm import declarative_base, relationship Base declarative_base() class Customer(Base): __tablename__ customer id Column(Integer, primary_keyTrue) name Column(String(200), uniqueTrue, nullableFalse, indexTrue) industry Column(String(100)) source Column(String(50)) address Column(String(255)) owner_id Column(Integer, indexTrue) # 跟進(jìn)人關(guān)聯(lián)用戶(hù)表 created_at Column(DateTime, server_defaultfunc.now()) updated_at Column(DateTime, onupdatefunc.now()) contacts relationship(Contact, back_populatescustomer) class Contact(Base): __tablename__ contact id Column(Integer, primary_keyTrue) customer_id Column(Integer, ForeignKey(customer.id), nullableFalse, indexTrue) name Column(String(100), nullableFalse) title Column(String(100)) phone Column(String(50)) wechat Column(String(100)) email Column(String(100)) notes Column(Text) customer relationship(Customer, back_populatescontacts) class FollowUp(Base): __tablename__ follow_up id Column(Integer, primary_keyTrue) customer_id Column(Integer, ForeignKey(customer.id), nullableFalse, indexTrue) contact_id Column(Integer, ForeignKey(contact.id), nullableTrue) owner_id Column(Integer, indexTrue) communication_type Column(String(20)) # phone/wechat/email/visit/meeting direction Column(String(10)) # in/out content Column(Text, nullableFalse) happened_at Column(DateTime, server_defaultfunc.now())執(zhí)行遷移用 Alembic先把初始化腳本alembic revision --autogenerate生成再alembic upgrade head應(yīng)用遷移。這個(gè)習(xí)慣從第一天就養(yǎng)成后面改表結(jié)構(gòu)時(shí)不會(huì)手忙腳亂。4.3 時(shí)間線(xiàn)接口一次查詢(xún)把客戶(hù)對(duì)應(yīng)溝通記錄全帶出來(lái)時(shí)間線(xiàn)是 DeskcommCRM 里最常被調(diào)用的接口設(shè)計(jì)原則是“盡量少地訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)”。一個(gè)客戶(hù)詳情頁(yè)前端只需要調(diào)一個(gè)接口就返回客戶(hù)信息、聯(lián)系人和最近 N 條跟進(jìn)記錄。用 FastAPI 可以直接這樣寫(xiě)from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session, joinedload router APIRouter() router.get(/customers/{customer_id}/timeline) def get_customer_timeline(customer_id: int, db: Session Depends(get_db)): customer ( db.query(Customer) .options(joinedload(Customer.contacts)) .filter(Customer.id customer_id) .first() ) if not customer: raise HTTPException(status_code404, detail客戶(hù)不存在) follow_ups ( db.query(FollowUp) .filter(FollowUp.customer_id customer_id) .order_by(FollowUp.happened_at.desc()) .limit(50) .all() ) return { customer: { id: customer.id, name: customer.name, industry: customer.industry, source: customer.source, }, contacts: [ {id: c.id, name: c.name, title: c.title, phone: c.phone} for c in customer.contacts ], follow_ups: [ { id: f.id, type: f.communication_type, direction: f.direction, content: f.content, happened_at: f.happened_at.isoformat(), owner_id: f.owner_id, } for f in follow_ups ], }這里用 joinedload 一次性把聯(lián)系人查出來(lái)避免 N1 查詢(xún)。跟進(jìn)記錄條數(shù)取最近 50 條前端做滾動(dòng)加載時(shí)就傳個(gè) offset 參數(shù)繼續(xù)翻。4.4 數(shù)據(jù)權(quán)限控制越簡(jiǎn)單越不容易漏CRM 里最要命的問(wèn)題是權(quán)限混亂。銷(xiāo)售不希望別的同事看到自己的客戶(hù)但老板希望看到所有人的進(jìn)展。兩邊都是真實(shí)需求處理不好系統(tǒng)就沒(méi)人敢用了。DeskcommCRM 用了一個(gè)折中且好落地的方案數(shù)據(jù)層面不做復(fù)雜的行級(jí)權(quán)限而是通過(guò)“歸屬人”加“可見(jiàn)范圍”兩層控制。每個(gè)客戶(hù)都有 owner_id 字段。默認(rèn)情況下普通用戶(hù)只能看到自己是歸屬人的客戶(hù)管理者角色通過(guò) user 表的 role 字段區(qū)分可以看到全量的客戶(hù)列表跨團(tuán)隊(duì)共享需求用“共享客戶(hù)”功能解決給客戶(hù)表加一張 share_customer 關(guān)聯(lián)表誰(shuí)分享了、分享給了誰(shuí)、什么時(shí)候分享的都能留痕。這套方案不需要引入任何權(quán)限框架純靠 SQL 查詢(xún)條件控制理解和維護(hù)成本都極低。等團(tuán)隊(duì)真有一天需要“部門(mén)只能看部門(mén)客戶(hù)”的復(fù)雜規(guī)則再遷移到 casbin 或者 opa 也不遲。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄DeskcommCRM 開(kāi)發(fā)中最容易翻車(chē)的點(diǎn)5.1 重復(fù)客戶(hù)數(shù)據(jù)導(dǎo)入時(shí)清洗索引要提前做好CRM 系統(tǒng)上線(xiàn)一段時(shí)間后必然會(huì)遇到重復(fù)客戶(hù)。Excle 導(dǎo)入的數(shù)據(jù)尤其臟同一個(gè)“北京華信科技有限公司”在系統(tǒng)里可能存在“北京華信科技”“華信科技有限公司北京”等多個(gè)變體。不及時(shí)處理后面統(tǒng)計(jì)客戶(hù)數(shù)量時(shí)數(shù)據(jù)完全不能看。我的建議是兩手抓。第一在客戶(hù)表的 name 字段上建唯一索引從數(shù)據(jù)庫(kù)層面兜底第二導(dǎo)入數(shù)據(jù)之前用簡(jiǎn)單的文本清洗規(guī)則做前綴歸一化比如去掉“北京”這類(lèi)括號(hào)后綴、統(tǒng)一公司后綴名稱(chēng)等。清洗規(guī)則不追求完美能把明顯重復(fù)的攔掉一半以上就值了。如果真的遇到少量重復(fù)管理員在客戶(hù)詳情頁(yè)手動(dòng)合并即可。合并本質(zhì)就是事務(wù)操作把備選客戶(hù)的聯(lián)系人、跟進(jìn)記錄全部轉(zhuǎn)移給主線(xiàn)客戶(hù)然后軟刪除備選客戶(hù)。合并按鈕做出來(lái)不難但一定要二次確認(rèn)數(shù)據(jù)丟了很難恢復(fù)。5.2 時(shí)間線(xiàn)順序混亂與時(shí)區(qū)問(wèn)題所有時(shí)間一律存 UTC如果跟進(jìn)記錄的時(shí)間排序忽前忽后那十有八九是時(shí)區(qū)問(wèn)題。用戶(hù)在東八區(qū)錄入晚上8點(diǎn)的記錄存到數(shù)據(jù)庫(kù)卻是中午12點(diǎn)再過(guò)一天排序就會(huì)錯(cuò)亂。這個(gè)問(wèn)題解決起來(lái)很簡(jiǎn)單所有 DateTime 字段在數(shù)據(jù)庫(kù)里一律存 UTC前端展示時(shí)再轉(zhuǎn)成本地時(shí)間。后端返回時(shí)間時(shí)統(tǒng)一用 ISO 8601 格式帶上時(shí)區(qū)偏移量前端用 dayjs 或 moment 做本地格式化。如果你用的是學(xué)習(xí)成本更低的 UTC 統(tǒng)一方案建議角度更進(jìn)一層給每條記錄增加一個(gè)時(shí)間戳字段避免時(shí)區(qū)轉(zhuǎn)換后排序錯(cuò)位。在 Postgres 里一種實(shí)測(cè)有效的方案是使用timestamptz類(lèi)型替代timestamp。它本身就是帶時(shí)區(qū)的時(shí)間類(lèi)型后端傳入帶時(shí)區(qū)的 ISO 字符串也能被正確識(shí)別存儲(chǔ)。這個(gè)調(diào)整不復(fù)雜但能省掉后續(xù)無(wú)數(shù)怨聲載道的時(shí)間錯(cuò)亂反饋。5.3 附件文件丟失定期備份要寫(xiě)進(jìn)計(jì)劃里不能靠腦子記客戶(hù)發(fā)的報(bào)價(jià)單、合同掃描件、聊天截圖這些附件丟一個(gè)都很難找回。有些團(tuán)隊(duì)把所有附件存在 web 服務(wù)器目錄下面結(jié)果哪次重新部署容器時(shí)忘了掛載 volume數(shù)據(jù)就全沒(méi)了。強(qiáng)制要求部署腳本里必須包含數(shù)據(jù)庫(kù)和附件目錄的備份步驟。最少要做到每天晚上用 cron 定時(shí)把 PostgreSQL dump 到另一塊磁盤(pán)或?qū)ο蟠鎯?chǔ)附件目錄用 rsync 同步到備份位置。理想狀態(tài)下備份文件保留最近 30 天。Docker 部署環(huán)境下最簡(jiǎn)單的方式是在宿主機(jī)上配置一條 cron 任務(wù)定時(shí)執(zhí)行docker exec deskcomm_db pg_dump -U deskcomm deskcomm_crm backup_$(date \%F).sql再配合一條 rsync 命令把附件目錄推到云端。這套方案五分鐘能配完卻能讓你在系統(tǒng)運(yùn)行幾個(gè)月之后睡得安穩(wěn)。5.4 用戶(hù)不愛(ài)記錄跟進(jìn)別只怪產(chǎn)品要在體驗(yàn)上做減法很多團(tuán)隊(duì)上線(xiàn) CRM 之后最大的問(wèn)題不是技術(shù)而是“沒(méi)人用”。銷(xiāo)售覺(jué)得填跟進(jìn)記錄耽誤時(shí)間不填的話(huà)系統(tǒng)里又沒(méi)什么數(shù)據(jù)管理者看著空蕩蕩的界面愈發(fā)焦慮于是強(qiáng)制要求填寫(xiě)結(jié)果銷(xiāo)售更抵觸形成惡性循環(huán)。DeskcommCRM 在體驗(yàn)上做了幾個(gè)減法來(lái)改善這個(gè)問(wèn)題跟進(jìn)記錄默認(rèn)只保留一個(gè)文本框和三個(gè)下拉框打開(kāi)頁(yè)面5秒內(nèi)能填完。今天任務(wù)還沒(méi)有完成時(shí)打開(kāi)任務(wù)看板會(huì)看到紅色的“超時(shí)”標(biāo)記給一點(diǎn)點(diǎn)視覺(jué)壓力但不過(guò)度。每周自動(dòng)生成一份“跟進(jìn)周報(bào)”統(tǒng)計(jì)每個(gè)人本周新增多少條記錄、完成了多少任務(wù)發(fā)到全員郵件里。正向反饋多了記錄習(xí)慣自然就養(yǎng)成了。技術(shù)問(wèn)題通常好解決習(xí)慣問(wèn)題才是長(zhǎng)期仗。這個(gè)道理做 CRM 的人越早想明白項(xiàng)目越早能走上正軌。6. 項(xiàng)目復(fù)盤(pán)與后續(xù)可擴(kuò)展方向DeskcommCRM 這個(gè)項(xiàng)目走過(guò)一整個(gè)從定位到落地的周期之后我最深的感觸是做一套能用的 CRM難度不在于功能多不多而在于能不能克制住沖動(dòng)把核心鏈路做透??蛻?hù)檔案管好、溝通記錄不斷、任務(wù)能閉環(huán)、權(quán)限不失控這四條立住了系統(tǒng)基本就成了。而多數(shù)失敗的自建 CRM都死在想要一口吃成胖子結(jié)果核心鏈路到處漏風(fēng)用戶(hù)用兩天就跑了。后續(xù)想把 DeskcommCRM 做得更完整我會(huì)優(yōu)先考慮兩個(gè)方向而不是繼續(xù)堆功能。第一個(gè)方向是開(kāi)放 API 和 Webhook。CRM 里記錄的溝通和任務(wù)數(shù)據(jù)如果能和企微、釘釘打通用戶(hù)不用在多個(gè)工具之間切來(lái)切去是一個(gè)體感很強(qiáng)的提升。具體實(shí)現(xiàn)上不用太復(fù)雜把跟進(jìn)記錄新增、任務(wù)完成這類(lèi)事件以 Webhook 形式推給外部平臺(tái)事件驅(qū)動(dòng)模型設(shè)計(jì)好了后續(xù)對(duì)接都不費(fèi)勁。第二個(gè)方向是輕量級(jí)的數(shù)據(jù)洞察比如客戶(hù)活躍度評(píng)分、跟進(jìn)間隔提醒。這些功能本質(zhì)上是把現(xiàn)有數(shù)據(jù)進(jìn)行加工不需要引入大數(shù)據(jù)組件用 SQL 查詢(xún)加定時(shí)任務(wù)就能跑起來(lái)。比如可以設(shè)置一個(gè)規(guī)則超過(guò)7天沒(méi)有跟進(jìn)記錄的高意向客戶(hù)自動(dòng)給負(fù)責(zé)人發(fā)一條提醒。這個(gè)功能很多團(tuán)隊(duì)夢(mèng)寐以求實(shí)現(xiàn)上卻只需要一條簡(jiǎn)單的 SQL 和一段定時(shí)任務(wù)腳本。如果你也想基于類(lèi)似思路做一套系統(tǒng)我的建議是先問(wèn)自己一個(gè)問(wèn)題團(tuán)隊(duì)里有幾個(gè)人愿意天天打開(kāi)這個(gè)系統(tǒng)把溝通記下來(lái)如果答案不樂(lè)觀任何技術(shù)選型和功能規(guī)劃都是空中樓閣。先把人這個(gè)變量解決再談代碼怎么寫(xiě)。