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