Day22 名片系統 — ERP 的第一步

https://image01.fishmind.org/cnc/ironman-2026-day22-01.png

名片是 ERP CRM 的開端。

先有資料,才有 CRM。名片是第一筆資料。

先開一台機器

在虛擬化主機上開一台新的機器:Ubuntu 24.04,4 vCPU、8G 記憶體,內網位址 192.168.0.50。這台不裝任何模型。名片系統只跑三個容器 container:web 是 nginx,端出前端頁面,順便把 /api 轉給後端;api 是 FastAPI;db 是 PostgreSQL 16。三個用 docker compose 一起起來。

開一台新的 VM:Ubuntu 24.04,4 vCPU / 8G,IP 192.168.0.50,取名 namecard。
在上面用 docker compose 起名片系統,三個容器:
- web:nginx,端前端頁面,/api 轉給 api
- api:FastAPI
- db:PostgreSQL 16
名片原圖與人頭照存 data/uploads,bind mount 進 api;資料庫只記路徑,⛔ 圖片不准進 DB。
OCR 打 ocr.iron30.com 的視覺模型,Chat 打 llm.iron30.com,兩個都是 openai_compatible,
端點寫進資料庫的設定表,⛔ 不准寫死在程式裡。
🔴 資料庫密碼、JWT secret 放 .env,不進 git;缺了停下來問我,⛔ 不准自己編一個先跑。
驗收:三個容器都 healthy,機器上 curl http://localhost/health 回 200。

名片原圖與人頭照不進資料庫,存在機器的檔案系統上,用 bind mount 掛進 api 容器;資料庫只記路徑。資料庫密碼與 JWT secret 放 .env,不進 git。這是前面每一篇都立過的規則。認字與對話兩件事都不在這台做:OCR 打 Day 11 那台 ocr.iron30.com 的視覺模型,對話打 llm.iron30.com。只有 api 容器在內網呼叫它們,模型機永遠不對外。

系統功能:

名片系統的頁面照這個蓋,網頁版與手機 App 共用同一套 API:
- 登入:帳號密碼,JWT access 15 分鐘 + refresh 30 天;⛔ 不做註冊頁,帳號只能由管理員建
- 首頁:四格,名片、上傳名片、搜尋 Chat、管理;一般使用者看不到管理那格
- 名片:我的名片 / 公司名片庫 兩個分頁,關鍵字搜尋、排序、分頁;
  詳情頁要有名片原圖、人頭照、電話分六種(公司、分機、傳真、手機、私人、其他)、關係標籤、加入日期;圖片可以放大
- 上傳名片:正面必填、反面選填;正面 OCR 填中文欄位、反面 OCR 填英文欄位;
  OCR 結果只是草稿,人確認存檔才入庫;OCR 掛了要能全手填,原圖照存
- 搜尋 Chat:用自然語言問;模型只負責把問題變成一個結構化查詢,SQL 由 server 跑、權限在 SQL 層過濾;
  查詢不合法就退回關鍵字搜尋
- 管理(只有 admin 看得到):使用者、公司名片庫、關係標籤、稽核紀錄、LLM 設定(OCR 與 Chat 分開,存 DB)、佈景主題
權限:私有卡只有 owner 與 admin 看得到;共享卡全員可讀、owner 與 admin 可改、⛔ 只有 admin 能刪或取消共享。
🔴 權限一律在 server 端判斷,不信任 client;每一個 API 的權限規則都要有測試。
🔴 稽核:登入、登出、登入失敗、重設密碼、名片的每一次新增、修改、刪除、共享都要記。
UI 全繁體中文,技術名詞留英文。

給它一個名字:namecard.iron30.com

對外的名字是 namecard.iron30.com。做法跟 Day 15 一樣:在反向代理那台加一條 proxy host,指到 192.168.0.50 的 80 埠,開 Force SSL,憑證用 Let’s Encrypt 自動簽。機器上只有 web 容器的 80 對外,api 的 8000 只在內網。除了登入與健康檢查兩個路徑,其他每一個 API 都要 JWT 驗證;權限判斷全部在 server 端做,不信任 client。

到反向代理那台,加一條 proxy host:namecard.iron30.com → http://192.168.0.50:80,
開 Force SSL,憑證用 Let's Encrypt 自動簽。
⛔ api 的 8000 不准對外開。
驗收:https://namecard.iron30.com 打開是登入頁;
/api/v1/cards 不帶 token 要回 401,不能回資料。

這一條是有原因的:前面那台反向代理曾經因為一個沒有認證的服務被掃到、被實打。所以沒登入什麼都不能碰。

規則:對外只開一個埠,而且那個埠後面第一件事是驗證。

登入

登入頁:帳號密碼,沒有註冊;下面那個「下載 Android App」是明天的事

https://image01.fishmind.org/cnc/ironman-2026-day22-02.png

這是一個封閉系統:沒有註冊頁,帳號一律由管理員建。登入成功拿到一組 JWT:access token 十五分鐘,refresh token 三十天,會輪替。手機 App 與網頁用同一套。登入失敗只回 401,不告訴你帳號存不存在。登入頁下面有一個「下載 Android App」,那是明天的事。

規則:內部工具不要有註冊頁。多一個入口就多一個要防的地方。

首頁只有四格

登入之後的首頁:名片、上傳名片、搜尋 Chat、管理,一般使用者看不到最後那格

https://image01.fishmind.org/cnc/ironman-2026-day22-03.png

登入之後首頁就四格:名片、上傳名片、搜尋 Chat、管理。一般使用者看不到管理那格。四格對應這一篇接下來的四段路:先看管理,把系統設定好;再看名片列表與詳情;再看上傳;最後看用講的找人。

規則:首頁放的是「你會去做的事」,不是「系統有的功能」。

管理:使用者

使用者管理:三個帳號,兩個管理員、一個一般使用者;每一列都有編輯、重設密碼、刪除

https://image01.fishmind.org/cnc/ironman-2026-day22-04.png

管理的第一頁是使用者。建帳號、指定角色、停用、重設密碼、刪除,都在這裡。角色只有兩種:管理員 admin 與一般使用者 user。管理員多的是使用者管理、LLM 與主題設定、以及共享名片的刪除與取消共享。現在有三個帳號:兩個管理員、一個一般使用者。帳號被停用之後,手上的 token 下一次驗證就會被拒絕,不用等它過期。使用者被刪掉,他建的名片不會跟著消失,只是擁有者標成已停用的使用者。

規則:資料的壽命要比帳號長。刪人不刪資料。

管理:公司名片庫

公司名片庫管理:每一張共享卡標著擁有者,只有管理員能在這裡取消共享或刪除

https://image01.fishmind.org/cnc/ironman-2026-day22-05.png

名片預設是私有的,只有自己與管理員看得到。使用者可以把一張名片勾成共享,它就進公司名片庫,全公司都看得到,而且看得到是誰建的。共享之後擁有者不能自己收回。共享卡擁有者與管理員可以改,但只有管理員可以刪、可以取消共享。管理頁這一張表就是給管理員收拾用的:誰共享了什麼、要不要留。這個規則第一次聽會覺得嚴:為什麼我自己共享的不能自己收回?因為別人已經在用了。

規則:進了公共空間的東西,撤回要經過管理員。

管理:關係標籤

關係標籤管理:客戶、供應商、廠商……每一個有排序值,裝好時就有一組預設的

https://image01.fishmind.org/cnc/ironman-2026-day22-06.png

每一張名片可以貼關係標籤:客戶、供應商、廠商、同學、同事、長官這一類。標籤是管理員在這一頁維護的,有排序值,前面的先出現。系統裝好時已經有一組預設的。一張名片可以貼多個標籤。一個都沒貼,系統會顯示待定。標籤一開始就要定,因為後面用講的找人的時候,「台中的供應商」這種問法就是靠它。

規則:分類這種事在資料進來之前定好。

管理:稽核紀錄

稽核紀錄:一串 login_failed 之後接著 login 與 reset_password,那是我自己換密碼的痕跡

https://image01.fishmind.org/cnc/ironman-2026-day22-07.png

每一次登入、登出、登入失敗、重設密碼,以及名片的新增、修改、刪除、共享、取消共享,全部記下來:時間、操作者、動作、對象、摘要。登入失敗也記帳號。看到同一個帳號連續失敗好幾次,就知道有人在猜。前幾天實際看到一串 login_failed,接著是 login、reset_password 兩筆:那是我自己換密碼的痕跡。系統把我做的事記得比我清楚。每頁 20 筆,可以調。

規則:登入失敗一定要記。它是最便宜的入侵偵測。

管理:LLM 設定

LLM 設定:OCR 服務與 Chat 服務分開,類型、Base URL、模型、API Key;內網模型機不用金鑰

https://image01.fishmind.org/cnc/ironman-2026-day22-08.png

認字與對話是兩個服務,分開設定:OCR 服務一欄、Chat 服務一欄。每一欄三樣東西:類型、Base URL、模型名稱,再加一個 API Key。類型是 openai_compatible,也就是任何相容 OpenAI 格式的端點都接得上。兩邊現在都指向自己內網的模型機,內網不用金鑰,所以 API Key 空著。要換成雲端的模型,填網址、模型、金鑰,按儲存,不用改程式、不用重啟。設定存在資料庫,不是寫死在程式裡。

規則:模型端點放設定,不放程式。哪天要換模型,改設定就好。

管理:佈景主題

佈景主題:六個顏色,選了全站一起變;淺色深色是每個人自己在右上角切

https://image01.fishmind.org/cnc/ironman-2026-day22-09.png

主題色是全站的,管理員選一個,所有人下一次載入頁面就變。現在有六個顏色,用的是綠。淺色深色是另一回事,那是個人偏好,每個人自己在右上角切,不受這裡影響。這一頁沒什麼技術含量,但它是「這是我們公司的系統」跟「這是一個不知道哪來的網頁」的差別。

規則:全站的設定與個人的偏好分開放。管理員不該決定別人的深色模式。

名片的兩個分頁

我的名片:自己建的兩張,搜尋欄、排序、每頁 15,右上角是上傳名片與新增名片

https://image01.fishmind.org/cnc/ironman-2026-day22-10.png

名片頁有兩個分頁:我的名片、公司名片庫。我的名片是自己建的,不管有沒有共享。公司名片庫是所有人共享出來的,每一張標著由誰建立。搜尋欄吃姓名、公司、職稱、關係、學經歷或備註,就是一般的關鍵字過濾。可以排序,每頁 15 張。右上角兩顆按鈕:上傳名片、新增名片。上傳走 OCR,新增是純手填。

公司名片庫:所有人共享出來的,每一張標著由誰建立

https://image01.fishmind.org/cnc/ironman-2026-day22-11.png

規則:關鍵字搜尋要先做好。用講的找人是加在它上面的,不是取代它。

一張名片的全部

名片詳情:正面圖與人頭照在上面,電話分六格,加入日期與共享時間都在;這張是假名片

https://image01.fishmind.org/cnc/ironman-2026-day22-12.png

點進一張名片:上面是名片正面的圖與人頭照,下面是欄位。電話分六種:公司、分機、傳真、手機、私人、其他。名片上的電話從來不只一支,硬塞進一格會弄丟。還有中文地址與英文地址、國別、關係標籤、加入日期、共享於什麼時候。圖片可以點開放大:滾輪縮放、拖曳平移、雙擊重設。名片上那些小字,OCR 認錯的時候要能對回原圖。示範用的那張「郝明遍,硬名片股份有限公司」是一張假名片,電話地址都是編的。

圖片點開放大:滾輪縮放、拖曳平移、雙擊重設,OCR 認錯的時候對回原圖用的

https://image01.fishmind.org/cnc/ironman-2026-day22-13.png

規則:原圖一定要留。結構化資料是從原圖來的,原圖才是證據。

中文一欄,英文一欄

編輯表單:左邊中文、右邊英文,姓名旁邊多一格頭銜別名;國別下拉,關係是一排可以多選的 chip

https://image01.fishmind.org/cnc/ironman-2026-day22-14.png

編輯表單左邊中文、右邊英文:姓名、公司、職稱、地址各一份。台灣的名片多半正面中文、反面英文,兩邊都要有位置。姓名旁邊有一格頭銜別名,放博士、教授、英文小名這一類。這一格是後來加的:OCR 一開始會把「王小明博士」整個塞進姓名,搜尋就找不到人。國別是下拉,預設台灣,OCR 會猜,猜錯可以改。關係標籤是一排可以多選的 chip。

規則:名字就是名字。頭銜另外放,不然搜尋會吃虧。

正面必填,反面選填

上傳名片:正面必填、反面選填,拖進來或點選檔案

https://image01.fishmind.org/cnc/ironman-2026-day22-15.png

上傳頁兩個框:正面必填,反面選填。正面跑 OCR 填中文欄位,反面也跑 OCR,填英文欄位。server 會先自動裁切,手機直拍歪一點沒關係。OCR 認出來的東西只是草稿,會出現在確認表單上,我看過、改過、按存檔,它才是正式資料。認錯的欄位當場改。OCR 服務掛了怎麼辦:回一個明確的錯誤,表單還是可以全手填,原圖照存。有一次拿一張反光很嚴重的名片試,OCR 認出來的欄位幾乎是空的,表單照樣可以填,圖也在。這就是「草稿」的意思。

規則:機器認出來的東西一律當草稿。人按下存檔那一下才算數。

用講的找人

問一句「台中 供應商」:找到 1 筆相關名片,底下附那張名片的連結

https://image01.fishmind.org/cnc/ironman-2026-day22-16.png

搜尋 Chat 那一頁可以用自然語言問:「上次在台北認識做半導體的那個人」。試了一句「台中 供應商」,它回:找到 1 筆相關名片,底下附那張名片的連結,點了就進詳情頁。它背後只做一件事:模型把我的問題變成一個結構化的查詢,server 拿去跑資料庫,結果回填給模型整理成一句話。模型不會自己多走一步。權限在資料庫那一層過濾:找得到的只有我自己的名片與公司名片庫,不是全部。模型產出的查詢不合法,就退回一般的關鍵字搜尋;模型機整台掛了,搜尋框照樣能用。

搜尋 Chat 的空白頁:提示句就是這個功能的用法,「上次在台北認識做半導體的那個人」

https://image01.fishmind.org/cnc/ironman-2026-day22-17.png

規則:讓模型只做「把話翻成查詢」這一步。資料由 server 拿,權限由 server 管。

一張名片到一筆聯絡人

到這裡,名片系統的網頁版走完了:一台機器、三個容器、一個對外名字、管理六頁、名片三頁、可以用講的找人。它現在還缺最重要的那一段:在現場,掏出手機,拍一張,當場入庫。明天拿手機來拍。


本文為 iThome 2026 鐵人賽「一個人的機房」系列第 22 篇,同步發表於 iT 邦幫忙

發佈留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料