CRM系統(tǒng)DeskcommCRM的設(shè)計(jì)與部署實(shí)踐)
前陣子我一直在折騰一個(gè)叫DeskcommCRM的項(xiàng)目它是面向桌面辦公和內(nèi)外溝通場(chǎng)景的輕量級(jí)客戶關(guān)系管理系統(tǒng)。簡(jiǎn)單說(shuō)就是把你平時(shí)跟客戶打交道的聊天記錄、郵件往來(lái)、跟進(jìn)提醒、待辦任務(wù)全部收攏到一個(gè)地方跟客戶檔案串成一條線再自動(dòng)生成幾張能看的統(tǒng)計(jì)報(bào)表。它適合銷售、客服、自由職業(yè)者也適合那種“客戶信息還躺在微信聊天記錄里”的小團(tuán)隊(duì)。這篇文章我會(huì)完整記錄我從設(shè)計(jì)思路、模塊拆分、部署配置到上線運(yùn)行踩坑的全過(guò)程盡量把能抄作業(yè)的步驟都擺出來(lái)方便想搭一套自用或團(tuán)隊(duì)內(nèi)部 CRM 的朋友參考。1. 項(xiàng)目定位與整體設(shè)計(jì)DeskcommCRM 到底要解決什么問(wèn)題1.1 從“客戶信息散落在聊天記錄里”談起先說(shuō)一個(gè)我觀察到的普遍現(xiàn)象。很多小團(tuán)隊(duì)不是沒有客戶管理意識(shí)而是壓根沒時(shí)間把客戶信息錄入一套正經(jīng)的 CRM 系統(tǒng)。業(yè)務(wù)員加了客戶微信談過(guò)什么全在聊天記錄里客戶打過(guò)電話說(shuō)了什么可能只存在于記憶里報(bào)價(jià)單發(fā)了幾版、最后定了哪個(gè)價(jià)Excel 表里可能都沒有。等客戶突然來(lái)問(wèn)“上次那個(gè)方案怎么樣了”你得翻半天聊天記錄還不一定翻得到。DeskcommCRM 想解決的就是這個(gè)問(wèn)題。它不像大廠那種重營(yíng)銷自動(dòng)化、帶一堆復(fù)雜權(quán)限和流程引擎的 CRM而是把重點(diǎn)放在“溝通即記錄”上。每當(dāng)一條消息進(jìn)來(lái)、一個(gè)任務(wù)到期、一個(gè)報(bào)價(jià)單被確認(rèn)系統(tǒng)都會(huì)自動(dòng)把信息掛到對(duì)應(yīng)客戶的名下。這樣即使業(yè)務(wù)員休假別人接手時(shí)也能快速看懂客戶聊到哪一步了。我在設(shè)計(jì)初期給這個(gè)項(xiàng)目定了三個(gè)原則數(shù)據(jù)先沉淀、記錄要自動(dòng)、查詢要夠快。數(shù)據(jù)先沉淀指的是客戶資料和溝通記錄必須統(tǒng)一落庫(kù)記錄要自動(dòng)是說(shuō)能通過(guò)接口或?qū)肽玫降南⒉灰屓耸止ぬ畈樵円獕蚩焓钦f(shuō)客戶檔案打開后所有歷史往來(lái)應(yīng)該幾秒內(nèi)就能拉出來(lái)而不是等報(bào)表跑半天。1.2 整體架構(gòu)與核心模塊劃分DeskcommCRM 的整體架構(gòu)并不復(fù)雜我把它分成四個(gè)部分桌面客戶端、管理后臺(tái)、后端服務(wù)、數(shù)據(jù)存儲(chǔ)。桌面客戶端提供快捷的客戶查詢和消息速記能力管理后臺(tái)負(fù)責(zé)完整的數(shù)據(jù)管理和報(bào)表查看后端服務(wù)統(tǒng)一暴露 API數(shù)據(jù)存儲(chǔ)使用 PostgreSQL 加 Redis。這四個(gè)部分選型時(shí)都有各自的理由。桌面端我沒有選擇純網(wǎng)頁(yè)方案而是用了能直接跑在 Windows 和 macOS 上的桌面殼這樣業(yè)務(wù)員可以像開微信一樣隨手打開不用每次先登錄瀏覽器也不容易開一堆標(biāo)簽頁(yè)導(dǎo)致客戶信息被淹沒。管理后臺(tái)做成網(wǎng)頁(yè)是考慮到老板看報(bào)表和配置字段時(shí)用瀏覽器更舒服。后端服務(wù)的選型上我用了 Node.js 的 NestJS原因是它模塊化程度高開發(fā)效率快而且團(tuán)隊(duì)內(nèi)部更熟悉 JavaScript 技術(shù)棧。數(shù)據(jù)庫(kù)選了 PostgreSQL主要是看中它的 JSON 字段能力方便存儲(chǔ)靈活擴(kuò)展的客戶屬性。核心模塊劃分成五塊聯(lián)系人管理、溝通記錄、任務(wù)跟進(jìn)、工單反饋、數(shù)據(jù)看板。聯(lián)系人管理管的是客戶檔案包括個(gè)人、企業(yè)、標(biāo)簽和自定義字段溝通記錄是對(duì)接郵件、企業(yè)微信、聊天記錄導(dǎo)入和電話備注的統(tǒng)一時(shí)間線任務(wù)跟進(jìn)負(fù)責(zé)創(chuàng)建待辦、設(shè)置提醒、追蹤跟進(jìn)周期工單反饋適合售后和客服場(chǎng)景記錄客戶報(bào)障、處理進(jìn)度和滿意度數(shù)據(jù)看板則把新增客戶數(shù)、跟進(jìn)次數(shù)、成交轉(zhuǎn)化率這類指標(biāo)統(tǒng)計(jì)出來(lái)。這套模塊劃分的底層邏輯是“客戶生命周期”。從線索進(jìn)來(lái)到跟進(jìn)中到成交再到售后每個(gè)階段都有對(duì)應(yīng)的模塊承載數(shù)據(jù)。線索進(jìn)來(lái)的那一刻聯(lián)系人模塊先建檔被銷售聯(lián)系溝通時(shí)溝通記錄模塊在工作銷售承諾了回訪時(shí)間任務(wù)跟進(jìn)模塊會(huì)提醒客戶出了產(chǎn)品問(wèn)題工單模塊會(huì)記錄處理過(guò)程最終管理層要看整體經(jīng)營(yíng)情況數(shù)據(jù)看板給結(jié)論。1.3 針對(duì)典型用戶的取舍做這類系統(tǒng)最大的坑是想一口氣滿足所有人。我一開始也列了一堆需求包括營(yíng)銷郵件批量發(fā)送、客戶公海分配、銷售業(yè)績(jī)排名等等。但實(shí)際調(diào)研了一圈身邊需要 CRM 的團(tuán)隊(duì)發(fā)現(xiàn)最有痛點(diǎn)的反而是那些最簡(jiǎn)單的場(chǎng)景。于是我把功能目標(biāo)收斂成了“四個(gè)人都能用起來(lái)”銷售能快速記客戶客服能看清歷史負(fù)責(zé)人能安排任務(wù)管理層能看到數(shù)據(jù)。這個(gè)收斂過(guò)程做了不少取舍。比如營(yíng)銷自動(dòng)化我最終只保留了簡(jiǎn)單的分組群發(fā)郵件功能而且必須手動(dòng)觸發(fā)不會(huì)自動(dòng)給所有客戶發(fā)促銷。原因是小團(tuán)隊(duì)使用自動(dòng)營(yíng)銷的頻次很低卻對(duì)系統(tǒng)穩(wěn)定性和反垃圾要求很高投入產(chǎn)出比不劃算。又比如公海分配我最初設(shè)計(jì)了一大套規(guī)則后來(lái)砍成了一個(gè)簡(jiǎn)單的狀態(tài)字段空閑中、跟進(jìn)中、已成交。有需要時(shí)團(tuán)隊(duì)負(fù)責(zé)人手動(dòng)重新指派即可沒必要一上來(lái)就做復(fù)雜的規(guī)則引擎。這些取舍讓我意識(shí)到輕量系統(tǒng)的核心價(jià)值不是“功能多”而是“維護(hù)成本低”。小團(tuán)隊(duì)未必有專職的 IT 管理員系統(tǒng)太復(fù)雜出了故障沒人會(huì)修自然就被棄用了。所以我在設(shè)計(jì)每個(gè)功能前都會(huì)問(wèn)一句這個(gè)功能如果讓一個(gè)不懂?dāng)?shù)據(jù)庫(kù)的人用他會(huì)不會(huì)用不會(huì)用就簡(jiǎn)化。2. 核心技術(shù)點(diǎn)與功能模塊拆解2.1 聯(lián)系人檔案與完整體檢聯(lián)系人模塊不是簡(jiǎn)單的一張表而是一套多層級(jí)的檔案結(jié)構(gòu)。客戶可以是個(gè)人也可以掛在企業(yè)下面還可以有多個(gè)聯(lián)系人和多個(gè)標(biāo)簽。我在數(shù)據(jù)庫(kù)里設(shè)計(jì)了企業(yè)表、聯(lián)系人表、聯(lián)系人標(biāo)簽表這三個(gè)主要實(shí)體。企業(yè)表存儲(chǔ)公司名稱、行業(yè)、規(guī)模、地址聯(lián)系人表存儲(chǔ)姓名、電話、郵箱、職務(wù)、所屬企業(yè)標(biāo)簽表負(fù)責(zé)打標(biāo)比如“高意向”“已簽約”“需要續(xù)費(fèi)”。這套模型的初期設(shè)計(jì)踩了一個(gè)坑就是標(biāo)簽到底存成單獨(dú)表還是直接用一個(gè)字符串字段。字符串字段確實(shí)簡(jiǎn)單但要統(tǒng)計(jì)“所有高意向客戶”時(shí)必須用模糊查詢數(shù)據(jù)量大了性能會(huì)下降。單獨(dú)的標(biāo)簽表需要多維護(hù)一張關(guān)聯(lián)表但查詢時(shí)可以走索引速度穩(wěn)定。最終我選了單獨(dú)標(biāo)簽表并且限制一個(gè)聯(lián)系人最多打 20 個(gè)標(biāo)簽防止濫用導(dǎo)致查詢膨脹。在搜索體驗(yàn)上我做了全局搜輸入關(guān)鍵詞能同時(shí)匹配聯(lián)系人姓名、電話、郵箱、企業(yè)名和備注內(nèi)容。這個(gè)功能的實(shí)現(xiàn)關(guān)鍵是在 PostgreSQL 里建了帶pg_trgm擴(kuò)展的 GIN 索引。pg_trgm可以支持模糊匹配的加速數(shù)據(jù)量到幾萬(wàn)條聯(lián)系人時(shí)搜索響應(yīng)依然能穩(wěn)定在 300 毫秒內(nèi)。第一次配置時(shí)我沒啟用這個(gè)擴(kuò)展結(jié)果搜一個(gè)名字要全表掃描客戶多了以后明顯卡頓后來(lái)才加上。字段設(shè)計(jì)上我保留了八個(gè)默認(rèn)字段加若干自定義字段。自定義字段支持文本、日期、下拉選項(xiàng)三種類型足夠覆蓋大多數(shù)場(chǎng)景。不建議一上來(lái)就添加大量自定義字段因?yàn)槊總€(gè)自定義字段都要考慮檢索、展示、導(dǎo)入導(dǎo)出映射維護(hù)成本是線性增加的。我最后只在一個(gè)客戶檔案里用了兩個(gè)自定義字段客戶來(lái)源渠道和預(yù)計(jì)成交日期其他信息全部放進(jìn)備注里反正支持全文檢索。2.2 溝通記錄與消息集成溝通記錄模塊是本系統(tǒng)最重要的數(shù)據(jù)來(lái)源它解決的是“客戶到底跟你聊了什么”。我設(shè)計(jì)了一個(gè)統(tǒng)一時(shí)間線的數(shù)據(jù)模型把郵件、聊天記錄、電話備注、見面紀(jì)要全部以消息記錄的形式掛到聯(lián)系人時(shí)間線上。每條記錄包含發(fā)送方、接收方、正文、附件鏈接、發(fā)生時(shí)間、關(guān)聯(lián)的待辦任務(wù)。從接入方式看郵件是最容易打通的。我用 IMAP 協(xié)議監(jiān)聽指定郵箱賬號(hào)的收件箱通過(guò)郵件頭里的 Message-ID 做去重再按發(fā)件人郵箱地址匹配到系統(tǒng)內(nèi)的聯(lián)系人。如果發(fā)件人的郵箱不在聯(lián)系人庫(kù)里我會(huì)先自動(dòng)創(chuàng)建一條“待確認(rèn)”狀態(tài)的臨時(shí)聯(lián)系人等有人確認(rèn)后再正式建檔。這一步很實(shí)用可以減少錄入成本也避免一堆垃圾郵件自動(dòng)污染聯(lián)系人庫(kù)。聊天記錄的接入比郵件復(fù)雜一些。我實(shí)際使用的是企業(yè)微信的會(huì)話存檔接口和聊天記錄導(dǎo)出文件這兩種方式。會(huì)話存檔接口要額外購(gòu)買企業(yè)的會(huì)話存檔服務(wù)不過(guò)拿到的數(shù)據(jù)很完整消息類型、發(fā)送人、接收人、時(shí)間戳都有。如果是外部聊天工具比如個(gè)人微信我不建議做任何自動(dòng)抓取合規(guī)風(fēng)險(xiǎn)太大最穩(wěn)妥的是支持 CSV 或 HTML 導(dǎo)出后手動(dòng)導(dǎo)入。自動(dòng)導(dǎo)入的痛點(diǎn)主要是數(shù)據(jù)格式不統(tǒng)一我在導(dǎo)入模塊里寫了一個(gè)字段映射界面讓用戶把源文件里的列名拖拽對(duì)應(yīng)到系統(tǒng)字段。比如源文件里有“客戶姓名”“客戶手機(jī)號(hào)”“最近聊天內(nèi)容”就手動(dòng)映射到聯(lián)系人姓名、手機(jī)號(hào)、最近一條消息。這樣導(dǎo)入模塊可以兼容任何來(lái)源的數(shù)據(jù)不需要為每種格式單獨(dú)寫代碼。為了信息安全和合規(guī)我還在消息正文的展示層做了一個(gè)脫敏選項(xiàng)。對(duì)于身份證號(hào)、銀行卡號(hào)這類敏感信息系統(tǒng)默認(rèn)用*號(hào)遮蔽只有擁有管理員權(quán)限的人點(diǎn)擊后才能查看原文。這個(gè)功能雖然實(shí)現(xiàn)起來(lái)不難但對(duì)面向外部客戶展示的場(chǎng)合很關(guān)鍵防止銷售在投屏演示時(shí)把客戶隱私一并暴露出去。2.3 跟進(jìn)任務(wù)與提醒機(jī)制任務(wù)跟進(jìn)模塊的設(shè)計(jì)目標(biāo)很單純不讓任何一條承諾落空。業(yè)務(wù)員對(duì)客戶承諾“明天給您回電話”“下周給方案”這些承諾都該變成任務(wù)。我在這套系統(tǒng)里把任務(wù)分成兩類普通待辦和跟進(jìn)任務(wù)。普通待辦是自己安排的活兒比如“整理產(chǎn)品資料”跟進(jìn)任務(wù)則必須關(guān)聯(lián)到聯(lián)系人上并且有明確的截止時(shí)間和提醒方式。任務(wù)創(chuàng)建時(shí)的關(guān)鍵字段不多執(zhí)行人、關(guān)聯(lián)聯(lián)系人、任務(wù)標(biāo)題、詳細(xì)描述、截止時(shí)間、提醒時(shí)間。提醒時(shí)間支持提前 5 分鐘、30 分鐘、1 小時(shí)、1 天多個(gè)檔位也可以自定義。這里最重要的是任務(wù)狀態(tài)機(jī)待處理、處理中、已完成、已逾期、已取消。狀態(tài)不能隨意跳轉(zhuǎn)處理中的任務(wù)不能直接標(biāo)記為已完成必須先有處理結(jié)果備注否則系統(tǒng)會(huì)攔截并提示“請(qǐng)先填寫跟進(jìn)記錄”。提醒機(jī)制的實(shí)現(xiàn)我一開始用了個(gè)非常樸素的方案每五分鐘掃描一次數(shù)據(jù)庫(kù)里所有截止時(shí)間在三分鐘內(nèi)的任務(wù)然后發(fā)站內(nèi)信和郵件。這個(gè)方案在小數(shù)據(jù)量下沒有問(wèn)題但任務(wù)量到了幾萬(wàn)條時(shí)每五分鐘一次全表掃描還是有點(diǎn)浪費(fèi)。后來(lái)我改成了 Redis 延遲隊(duì)列任務(wù)創(chuàng)建時(shí)把提醒時(shí)間戳推入 ZSet由單獨(dú)的 worker 定時(shí)拉取到期的任務(wù) ID再執(zhí)行通知發(fā)送。這樣掃描的壓力被分散到消息隊(duì)列數(shù)據(jù)庫(kù)幾乎不會(huì)因?yàn)檫@個(gè)邏輯產(chǎn)生慢查詢。提醒渠道上站內(nèi)信是基本盤郵件和企業(yè)微信機(jī)器人作為擴(kuò)展。站內(nèi)信的優(yōu)勢(shì)是快打開客戶端就能看到數(shù)字角標(biāo)郵件適合正式場(chǎng)合比如給管理員發(fā)送日?qǐng)?bào)企業(yè)微信群機(jī)器人適合團(tuán)隊(duì)協(xié)作場(chǎng)景任務(wù)分配后群里立刻有人認(rèn)領(lǐng)減少互相催促。配置企業(yè)微信機(jī)器人只需要一個(gè) Webhook 地址每條消息以 JSON 格式 POST 過(guò)去即可5 分鐘就能接通。還有一類任務(wù)要特別小心就是周期性跟進(jìn)任務(wù)??蛻羰情L(zhǎng)期經(jīng)營(yíng)的單純的單次待辦會(huì)讓銷售人員忘記定期回訪。我加了“任務(wù)模板”功能管理員可以配置一個(gè)任務(wù)模板比如“每?jī)芍芑卦L一次已成交客戶”系統(tǒng)在每次任務(wù)完成后自動(dòng)創(chuàng)建下一次任務(wù)。這個(gè)邏輯不復(fù)雜麻煩的是避免重復(fù)創(chuàng)建我在模板表里加了last_triggered_at字段保證同一模板同一時(shí)間只會(huì)生成一個(gè)任務(wù)。2.4 可視化數(shù)據(jù)看板一個(gè) CRM 如果只有信息記錄而沒有統(tǒng)計(jì)那它就是通信錄或記事本。數(shù)據(jù)看板是我花了不少心思做的板塊但我也很清楚小團(tuán)隊(duì)不需要太多花哨的圖表核心看三塊客戶增長(zhǎng)情況、跟進(jìn)執(zhí)行情況、成交轉(zhuǎn)化情況??蛻粼鲩L(zhǎng)情況對(duì)應(yīng)的圖表很簡(jiǎn)單就是按天/按周統(tǒng)計(jì)新增客戶數(shù)同時(shí)附上累計(jì)客戶數(shù)。為了支持這個(gè)圖表我不需要在查詢時(shí)實(shí)時(shí)寫復(fù)雜的 group by只需要在聯(lián)系人表里維護(hù)created_at字段然后做日期維度聚合即可。數(shù)據(jù)量不大時(shí)直接 SQL 聚合完全夠用。但要注意時(shí)區(qū)問(wèn)題我在統(tǒng)計(jì)時(shí)強(qiáng)制按應(yīng)用配置的時(shí)區(qū)做to_timestamp和date_trunc否則北京時(shí)間和 UTC 時(shí)間會(huì)差 8 小時(shí)每天統(tǒng)計(jì)數(shù)看起來(lái)都會(huì)偏一天。跟進(jìn)執(zhí)行情況看的是任務(wù)完成率。這里我關(guān)心兩個(gè)指標(biāo)完成率和逾期率。完成率等于已完成任務(wù)數(shù)除以已分配任務(wù)數(shù)逾期率等于逾期未完成任務(wù)數(shù)除以應(yīng)完成任務(wù)數(shù)。指標(biāo)計(jì)算本身不復(fù)雜難點(diǎn)在于定義“逾期”我使用的規(guī)則是截止時(shí)間超過(guò)當(dāng)前時(shí)間且狀態(tài)不是已完成或已取消才算逾期。如果一個(gè)任務(wù)被推遲過(guò)系統(tǒng)記錄due_at的最新時(shí)間取最新時(shí)間判斷。否則會(huì)出現(xiàn)“改了個(gè)截止日期統(tǒng)計(jì)時(shí)還算逾期”的尷尬。成交轉(zhuǎn)化率我按照銷售漏斗來(lái)做從“線索”到“初步溝通”到“方案報(bào)價(jià)”到“成交”四層。每個(gè)聯(lián)系人擁有一個(gè)階段字段統(tǒng)計(jì)時(shí)數(shù)出當(dāng)前處于各階段的人數(shù)。漏斗圖的實(shí)現(xiàn)用餅圖其實(shí)就行不需要復(fù)雜模型。真正要注意的是階段流轉(zhuǎn)歷史如果只是修改當(dāng)前階段字段以后想復(fù)盤“這個(gè)客戶在報(bào)價(jià)階段停留了多久”就會(huì)無(wú)從下手。所以我增加了一張階段變更歷史表每次階段變化都記錄原階段、新階段、操作人、操作時(shí)間這樣漏斗圖就不只能看當(dāng)前分布還能分析轉(zhuǎn)化速度和停滯時(shí)長(zhǎng)。為了保證看板加載不卡我沒有讓前端實(shí)時(shí)去查統(tǒng)計(jì)接口而是先在后端做了聚合查詢接口再在前端用 ECharts 渲染。當(dāng)前數(shù)據(jù)量小聚合查詢都能在 1 秒內(nèi)完成。當(dāng)數(shù)據(jù)量翻了幾倍以后我打算用一個(gè)簡(jiǎn)單的定時(shí)任務(wù)把每天的關(guān)鍵指標(biāo)寫入一張匯總表前端直接讀匯總表實(shí)現(xiàn)毫秒級(jí)打開。3. 實(shí)操部署與配置全程記錄3.1 環(huán)境準(zhǔn)備與依賴清單先交代一下我實(shí)際使用的部署環(huán)境方便你對(duì)照。服務(wù)器是一臺(tái) 4 核 8G 內(nèi)存的普通 Linux 云主機(jī)操作系統(tǒng)是 Ubuntu 22.04 LTS。這套配置跑 PostgreSQL、Redis、Node.js 服務(wù)端和一個(gè) Nginx 反向代理日常使用非常寬裕。如果用 Docker Compose 部署內(nèi)存最低建議 4G否則 PostgreSQL 和 Node.js 同時(shí)啟動(dòng)時(shí)可能內(nèi)存告急。依賴組件清單如下PostgreSQL 16主數(shù)據(jù)庫(kù)Redis 7緩存和延遲隊(duì)列Node.js 20后端服務(wù)Nginx 1.24反向代理和靜態(tài)文件服務(wù)可選SMTP 郵件服務(wù)用于郵件提醒和郵件接入我推薦用 Docker Compose 統(tǒng)一管理這些組件因?yàn)榭梢砸粭l命令拉起整套環(huán)境也方便備份和遷移。服務(wù)器上先安裝 Docker 和 Docker Compose 插件然后準(zhǔn)備項(xiàng)目目錄結(jié)構(gòu)。我的目錄結(jié)構(gòu)是這樣的/opt/deskcomm ├── docker-compose.yml ├── .env ├── nginx │ └── conf.d │ └── deskcomm.conf ├── backend │ ├── dist │ └── package.json └── frontend └── htmldocker-compose.yml里定義了四個(gè)服務(wù)db、redis、backend、nginx。數(shù)據(jù)庫(kù)和 Redis 使用官方鏡像數(shù)據(jù)目錄掛載到宿主機(jī)上的./data/pg和./data/redis。后端服務(wù)使用自己構(gòu)建的鏡像Dockerfile基于node:20-slim把編譯好的dist目錄拷進(jìn)去后用node dist/main.js啟動(dòng)。前端構(gòu)建完把靜態(tài)文件復(fù)制到./frontend/html交給 Nginx 服務(wù)。3.2 初始化數(shù)據(jù)庫(kù)與配置項(xiàng)環(huán)境變量配置是啟動(dòng)前最關(guān)鍵的一步。我習(xí)慣將所有敏感配置放進(jìn).env文件中并在docker-compose.yml里通過(guò)env_file引用避免把密碼和密鑰寫死在代碼里。核心的配置項(xiàng)有這幾類數(shù)據(jù)庫(kù)相關(guān)POSTGRES_HOSTdb POSTGRES_PORT5432 POSTGRES_PASSWORDyour_strong_password POSTGRES_DBdeskcomm后臺(tái)應(yīng)用相關(guān)JWT_SECRETplease_change_to_a_random_long_string APP_TIMEZONEAsia/Shanghai DEFAULT_ADMIN_EMAILadminexample.com DEFAULT_ADMIN_PASSWORDchange_me郵件和提醒相關(guān)MAIL_HOSTsmtp.example.com MAIL_PORT465 MAIL_USERno-replyexample.com MAIL_PASSWORDsmtp_password WEBHOOK_QIYEWEIXINhttps://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyyour_key數(shù)據(jù)庫(kù)初始化我用了遷移腳本的方式?jīng)]有直接用 ORM 的自動(dòng)建表。這是因?yàn)樽詣?dòng)建表雖然方便但后續(xù)加字段、改索引時(shí)很難維護(hù)版本變更。我在backend/migrations目錄下放了一系列按時(shí)間戳命名的 SQL 文件啟動(dòng)時(shí)由遷移工具依次執(zhí)行執(zhí)行過(guò)的文件會(huì)被記錄到migration_log表里。這樣無(wú)論從哪個(gè)版本升級(jí)到新版本只需要把新遷移腳本放進(jìn)目錄里重啟服務(wù)即可完成升級(jí)。管理員賬號(hào)我建議用初始化腳本創(chuàng)建而不是寫死在數(shù)據(jù)庫(kù)遷移里。因?yàn)槊艽a明文寫在 SQL 文件里很不安全也很容易被提交到 Git 倉(cāng)庫(kù)。初始化腳本會(huì)在服務(wù)啟動(dòng)后檢查是否已有管理員賬號(hào)如果沒有就從環(huán)境變量讀取初始密碼經(jīng)過(guò) bcrypt 哈希后寫入數(shù)據(jù)庫(kù)。首次啟動(dòng)后應(yīng)該立刻登錄后臺(tái)修改密碼。3.3 啟動(dòng)服務(wù)與驗(yàn)證可用性配置完成后的啟動(dòng)命令很簡(jiǎn)單cd /opt/deskcomm docker compose up -d第一次啟動(dòng)會(huì)自動(dòng)拉取鏡像并構(gòu)建后端容器耗時(shí)取決于網(wǎng)絡(luò)和機(jī)器性能一般在五到十五分鐘之間。啟動(dòng)后我用下面幾步驗(yàn)證環(huán)境是否正常第一步檢查所有容器狀態(tài)docker compose ps所有服務(wù)的狀態(tài)應(yīng)該是Up如果backend容器反復(fù)重啟多半是環(huán)境變量配置錯(cuò)誤查看日志定位即可docker compose logs backend第二步檢查健康檢查接口。后端服務(wù)里我加了一個(gè)簡(jiǎn)單的/health接口會(huì)同時(shí)檢查數(shù)據(jù)庫(kù)連接和 Redis 連接返回{ status: ok }。如果這個(gè)接口返回 200說(shuō)明應(yīng)用依賴的環(huán)境基本正常。第三步訪問(wèn)前端頁(yè)面并登錄。Nginx 把根路徑/代理到前端靜態(tài)文件把/api前綴代理到后端服務(wù)。所以用戶在瀏覽器里只需要打開http://服務(wù)器IP就能進(jìn)入登錄頁(yè)。注意我第一次部署時(shí)忘記配置 Nginx 對(duì) WebSocket 的支持導(dǎo)致桌面端的消息提醒推送一直是灰色的后來(lái)加了Upgrade和Connection請(qǐng)求頭轉(zhuǎn)發(fā)才解決。驗(yàn)證完成后我還習(xí)慣做一輪樣例數(shù)據(jù)測(cè)試。建一個(gè)測(cè)試企業(yè)、一個(gè)測(cè)試聯(lián)系人、一條溝通記錄和一個(gè)任務(wù)確認(rèn)每個(gè)功能模塊都能正常寫入和查詢。如果所有操作都正常再開啟 SMTP 配置和 Webhook 配置把真實(shí)通知鏈路跑通。這個(gè)步驟別跳過(guò)因?yàn)楹芏鄦?wèn)題只有在真實(shí)連接外部服務(wù)時(shí)才會(huì)暴露。4. 上線運(yùn)行后的常見問(wèn)題與排查記錄4.1 問(wèn)題速查表系統(tǒng)上線這幾周我遇到過(guò)的、以及社區(qū)朋友問(wèn)過(guò)的問(wèn)題基本都集中在下面這張表里問(wèn)題可能原因解決辦法登錄后一直提示“賬號(hào)或密碼錯(cuò)誤”管理員初始密碼未修改或 JWT 密鑰變更導(dǎo)致舊 token 失效執(zhí)行初始化腳本重置密碼重新生成 JWT 密鑰并重啟郵件接入后收不到客戶郵件IMAP 賬號(hào)密碼錯(cuò)誤或郵件服務(wù)器限制了第三方客戶端檢查郵箱設(shè)置開啟“應(yīng)用專用密碼”任務(wù)提醒遲遲不來(lái)時(shí)區(qū)配置錯(cuò)誤或 Redis 隊(duì)列 worker 未運(yùn)行檢查APP_TIMEZONE重啟 worker 并查看隊(duì)列積壓數(shù)量正則導(dǎo)入 CSV 后中文亂碼CSV 文件編碼不是 UTF-8用記事本另存為 UTF-8 或轉(zhuǎn)換編碼后重試看板統(tǒng)計(jì)數(shù)字對(duì)不上時(shí)區(qū)偏差或任務(wù)狀態(tài)歷史更新方式不對(duì)統(tǒng)一按應(yīng)用時(shí)區(qū)聚合確認(rèn)狀態(tài)變更寫入了歷史表客戶端連不上服務(wù)器Nginx 未配置 WebSocket 轉(zhuǎn)發(fā)在 Nginx 配置中增加 WebSocket 支持這張表我保存到了項(xiàng)目 Wiki 里每次有新人接手部署時(shí)先讓他們看一遍能省掉很多基礎(chǔ)咨詢。4.2 幾個(gè)值得注意的深坑第一個(gè)深坑是消息 Webhook 重復(fù)回調(diào)。企業(yè)微信的 Webhook 在極端情況下會(huì)重復(fù)推送同一條消息如果沒有冪等處理數(shù)據(jù)庫(kù)里會(huì)出現(xiàn)兩條一模一樣的溝通記錄。我處理的方式是在消息表上建立一個(gè)唯一索引(source_type, external_message_id)以后每條消息寫入時(shí)如果重復(fù)則執(zhí)行更新而不是新增。這個(gè)索引建好后重復(fù)回調(diào)的問(wèn)題從根源上隔絕了。第二個(gè)深坑是數(shù)據(jù)庫(kù)連接池耗盡。剛開始我用的是默認(rèn)連接池大小后端服務(wù)同時(shí)處理大量請(qǐng)求時(shí)會(huì)報(bào)too many connections。排查下來(lái)發(fā)現(xiàn)并不是數(shù)據(jù)庫(kù)配置不夠而是有些接口在事務(wù)里做了長(zhǎng)時(shí)間查詢一直占著連接不釋放。后來(lái)我調(diào)整了連接池上限到 20并給所有 API 加了超時(shí)時(shí)間超時(shí)的請(qǐng)求及時(shí)中斷。針對(duì)慢查詢給created_at、contact_id、remind_at這些頻繁查詢的字段都加了索引問(wèn)題才徹底緩解。第三個(gè)深坑是忘記設(shè)置 Docker 容器的時(shí)區(qū)。容器里的默認(rèn)時(shí)區(qū)是 UTC而業(yè)務(wù)方在中國(guó)所有時(shí)間提醒都比真實(shí)時(shí)間晚了 8 小時(shí)。任務(wù)定在下午三點(diǎn)提醒實(shí)際到晚上十一點(diǎn)才推。這個(gè)坑很隱蔽因?yàn)閺膽?yīng)用日志看時(shí)間都是連續(xù)的很難一眼發(fā)現(xiàn)問(wèn)題。解決方案是在docker-compose.yml里給容器設(shè)置環(huán)境變量TZAsia/Shanghai并且應(yīng)用內(nèi)的時(shí)間處理全部基于這個(gè)時(shí)區(qū)。第四個(gè)深坑是數(shù)據(jù)庫(kù)備份恢復(fù)后外鍵報(bào)錯(cuò)。我一開始的備份腳本只備份了業(yè)務(wù)數(shù)據(jù)表沒有備份序列的自增計(jì)數(shù)器?;謴?fù)后新增聯(lián)系人時(shí)主鍵沖突是小事麻煩的是有些關(guān)聯(lián)表引用了錯(cuò)誤的 ID。后來(lái)我在備份腳本里使用pg_dump --clean --if-exists并明確指定全庫(kù)導(dǎo)出模式恢復(fù)完還要執(zhí)行一次ANALYZE更新統(tǒng)計(jì)信息確保數(shù)據(jù)庫(kù)執(zhí)行計(jì)劃不跑偏。4.3 性能與備份建議再怎么說(shuō)這也是一個(gè)長(zhǎng)期運(yùn)行的系統(tǒng)備份和監(jiān)控一定要做好。我用的備份策略是每天凌晨?jī)牲c(diǎn)執(zhí)行一次pg_dump全量備份同時(shí)把備份文件保留 14 天每天中午額外執(zhí)行一次增量邏輯把當(dāng)天的溝通記錄和任務(wù)變更記錄同步到一個(gè)歸檔表。之所以保留歸檔表是為了以后做數(shù)據(jù)分析時(shí)不用去翻巨大的主業(yè)務(wù)表。監(jiān)控方面我沒有引入重型的監(jiān)控系統(tǒng)只寫了一個(gè)簡(jiǎn)單的腳本每五分鐘檢查三個(gè)指標(biāo)后端接口響應(yīng)時(shí)間、Redis 隊(duì)列積壓數(shù)量、磁盤剩余空間。如果響應(yīng)時(shí)間超過(guò)兩秒或磁盤剩余空間低于 20%就往企業(yè)微信群里推送告警。這套輕量監(jiān)控已經(jīng)幫我抓到了兩次磁盤寫滿前的問(wèn)題都是日志文件沒有做輪轉(zhuǎn)導(dǎo)致的。順便一提Nginx 的 access 日志默認(rèn)無(wú)限增長(zhǎng)我設(shè)置了按天切分并保留七天再配合 crontab 清理磁盤壓力就小了很多。對(duì)于數(shù)據(jù)庫(kù)性能我建議小團(tuán)隊(duì)先不用考慮分表老老實(shí)實(shí)把索引建好。當(dāng)前系統(tǒng)里查詢最頻繁的是聯(lián)系人表和時(shí)間線記錄表這兩張表數(shù)據(jù)量在兩三萬(wàn)條時(shí)正確建索引后查詢都是毫秒級(jí)。真要到了幾十萬(wàn)條數(shù)據(jù)再考慮按時(shí)間分區(qū)或做只讀從庫(kù)不過(guò)對(duì)于大多數(shù)小團(tuán)隊(duì)來(lái)說(shuō)那已經(jīng)是幾年以后的事了。5. 使用經(jīng)驗(yàn)與下一步騰挪空間5.1 這套系統(tǒng)幫我節(jié)省了什么說(shuō)說(shuō)真實(shí)的使用感受。原來(lái)我自己的客戶信息管理非常粗放微信置頂十幾個(gè)聊天窗口Excel 記錄報(bào)價(jià)和合同郵箱里躺著一堆方案文檔。有用但亂找東西全靠記憶。用 DeskcommCRM 之后變化最明顯的不是“有什么高級(jí)功能”而是“找東西變快了”客戶打電話來(lái)打開桌面端輸入手機(jī)號(hào)后四位所有歷史記錄都在那不用臨時(shí)翻手機(jī)聊天記錄。對(duì)團(tuán)隊(duì)來(lái)說(shuō)最有價(jià)值的其實(shí)是標(biāo)準(zhǔn)的“跟進(jìn)記錄”習(xí)慣。系統(tǒng)不會(huì)強(qiáng)迫你寫日?qǐng)?bào)但每次任務(wù)完成時(shí)必須填寫處理結(jié)果備注這就養(yǎng)成了隨手記錄的習(xí)慣。月底看數(shù)據(jù)時(shí)負(fù)責(zé)人不再憑借印象評(píng)價(jià)誰(shuí)辛苦而是能看到每個(gè)人跟進(jìn)客戶的數(shù)量和轉(zhuǎn)化率這些數(shù)據(jù)對(duì)績(jī)效復(fù)盤和資源分配很有幫助。另外我個(gè)人比較受益的是 “防止客戶流失”。以前有些客戶談著談著就沒了聲息其實(shí)很多時(shí)候是銷售太忙忘了回訪。系統(tǒng)里逾期任務(wù)會(huì)一直亮紅每天打開客戶端第一眼看到的就是“逾期未跟進(jìn)”的列表那種無(wú)形的壓力會(huì)自動(dòng)推著銷售去把承諾推完。幾次體驗(yàn)下來(lái)客戶的續(xù)費(fèi)率確實(shí)比以前好了一些。5.2 想繼續(xù)擴(kuò)展的方向DeskcommCRM 目前已經(jīng)能穩(wěn)定跑在日常業(yè)務(wù)里但我心里也列了一張擴(kuò)展清單算是下一階段的規(guī)劃。第一個(gè)想補(bǔ)的是移動(dòng)端適配?,F(xiàn)在桌面端和 Web 端都做得不錯(cuò)但業(yè)務(wù)員出門拜訪客戶時(shí)在手機(jī)上快速查一下歷史記錄還是很有價(jià)值的。我不打算單獨(dú)開發(fā)原生 App優(yōu)先做一套響應(yīng)式的移動(dòng)頁(yè)面保證在手機(jī)上能看客戶檔案、記溝通記錄、處理待辦任務(wù)即可。第二個(gè)想補(bǔ)的是日歷集成。目前任務(wù)的提醒是按時(shí)觸發(fā)的但如果能和 Google Calendar 或 Outlook 日歷打通把跟進(jìn)任務(wù)自動(dòng)同步到日歷上銷售的時(shí)間管理就能更順。技術(shù)上不難主要是 OAuth 對(duì)接和雙向同步的業(yè)務(wù)邏輯要仔細(xì)處理。第三個(gè)想補(bǔ)的是輕量 BI。雖然現(xiàn)在看板提供了核心指標(biāo)但用戶還想自定義統(tǒng)計(jì)維度比如按產(chǎn)品線統(tǒng)計(jì)成交額、按地區(qū)分布客戶。我計(jì)劃把聚合查詢能力開放出來(lái)讓管理員可以選擇維度、指標(biāo)和時(shí)間范圍用簡(jiǎn)單的拖拽配置生成自定義報(bào)表而不需要每次提需求都改代碼。第四個(gè)想補(bǔ)的是多租戶模式。目前系統(tǒng)是單團(tuán)隊(duì)部署如果想做成 SaaS 版本對(duì)外提供需要把企業(yè)維度加入所有數(shù)據(jù)模型并處理好數(shù)據(jù)隔離和資源配額。這是一個(gè)比較大的改造我目前還在設(shè)計(jì)方案優(yōu)先級(jí)排在比較后面。最后再說(shuō)一句個(gè)人的體會(huì)。做這類內(nèi)部工具最重要的不是技術(shù)棧多新、架構(gòu)多復(fù)雜而是能否真正解決使用者的日常問(wèn)題。DeskcommCRM 我砍了很多計(jì)劃中的功能只留最常用的核心鏈路結(jié)果反而使用率很高。如果你也在猶豫要不要自建一套客戶管理系統(tǒng)我建議先從最小的閉環(huán)開始跑通一個(gè)季度再逐步豐富你會(huì)發(fā)現(xiàn)輕量系統(tǒng)的長(zhǎng)期價(jià)值遠(yuǎn)超想象。