醫境媒合平台 · 內部技術分析
問題 2(名稱顯示):功能已上線,回報畫面確認為舊版;但名稱在自助註冊的帳號上會全面失效——因為名稱欄位根本沒被填入。
問題 1(證照上傳):管線四層全通,但手機拍的照片多為 HEIC 格式,會被格式白名單當場擋下——這很可能就是你實際撞到的狀況。
回報症狀:撮合記錄頁的「診所彙總」與「醫師」欄位顯示 6a55f496d145549b341c1f60 這類 24 碼 ObjectId,難以辨識是誰。
名稱顯示功能(計畫 29)已於 2026-07-19 由 commit 435b448 實作完成,並隨同一次 push 部署至 production。但目前的實作在自助註冊的帳號上會全面 fallback 回 ID——因為那些帳號的名稱欄位根本是空的或無意義。
function shortId(id: string): string {
return id.length > 6 ? `…${id.slice(-6)}` : id;
}
畫面渲染的是「名稱(或替代文字)+ …41c1f60 尾 6 碼 + 複製按鈕」。完整 ID 只存在於 title 屬性與剪貼簿,永遠不會成為可見內文。回報畫面顯示完整 24 碼、無替代文字、無複製鈕,因此不是這版程式碼的輸出。
對 https://yijingmatch.com/ 取得的 HTML 中 <title> 為「醫境媒合平台」,即品牌變更 commit c75eacd。該 commit 與名稱 enrich 的 435b448 屬同一次 push,且時序在其之後——因此 enrich 必然已一併上線。
TODO.md 與 16.上線前檢查表-簽核.md 均記載:2026-07-17 冒煙測試後資料已清除,dcm_prod 僅剩 admin 帳號 1 筆。而回報畫面顯示「撮合記錄 3 筆」與三列醫師資料——現行 production 無法產生該畫面。
2026-07-01 那一輪 claude-in-chrome 人工驗收(記錄於 23.claude-in-chrome-手動驗收測試項目.md)。該時間點同時早於 R2 物件儲存開通(07-09)與計畫 29 名稱顯示(07-19)——單一張舊畫面恰好可以同時解釋這次回報的兩個症狀。
服務層以批次 $in 查詢回填顯示名稱,屬 additive 欄位,不改變既有回應結構:
enrichMatchRecordViews() 併行查兩張表match-record.service.ts:222–235lookupDoctorNames() 查 DoctorProfile.fullName;lookupClinicNames() 查 Clinic.name,皆帶 deletedAt: null 過濾match-record.service.ts:188–220透過 gitnexus 的 call-graph 確認,enrichMatchRecordViews 的呼叫端確實涵蓋 listMatchRecords、createMatchRecordsForJobPost、updateMatchRecord 三者;診所彙總另走 lookupClinicNames(:456)。四條路徑全數接上,沒有遺漏——包含最初審查時擔心的「按更新後名稱消失」問題已被涵蓋。
這是本次分析最重要的發現。註冊流程建立角色殼時:
libs/auth/src/lib/register.ts:182(醫師)const profile = await DoctorProfileModel.create({ accountId });
// ↑ 完全沒有 fullName
libs/auth/src/lib/register.ts:119, 192(診所)
const displayName = email.split('@')[0]; // ← email 帳號前綴
const clinic = await ClinicModel.create({ accountId, name: displayName });
因此在真實使用情境下:
| 角色 | 註冊後的名稱欄位 | admin 撮合頁實際顯示 | 可用性 |
|---|---|---|---|
| 醫師(剛註冊,未建檔) | fullName 為 空 |
(未建檔名稱)+ …1c1f6a |
無法辨識 |
| 醫師(已完成 profile 建檔) | fullName = 本人填寫 |
真實姓名 | 正常 |
| 診所(自助註冊) | name = email 前綴,如 smoke-clinic |
顯示 smoke-clinic |
有值但非真實診所名 |
| 已軟刪除的醫師/診所 | 被 deletedAt: null 濾除 |
替代文字+短 ID | 刻意設計 |
名稱顯示這個「管線」已經接好了,但管線兩端的水源是空的。醫師必須自己到 /profile 完成建檔才會有姓名;診所則從註冊那一刻起就只有一個從 email 推導出來的代號,平台目前沒有任何介面讓診所填寫真實名稱。所以就算你現在重新測試,看到的很可能仍是「(未建檔名稱)」或 smoke-clinic 這種無意義字串——症狀跟你原本回報的幾乎一樣,但根因完全不同。
回報症狀:找不到/無法使用證照照片上傳功能。
證照上傳功能從前端輸入元件、API 端點、簽章服務到 Cloudflare R2 儲存桶,四層全部已實作並已於 2026-07-09 完成實機驗證。
但這裡必須誠實說明證據強度:問題 2 有三條獨立證據可證明回報畫面是舊版,問題 1 沒有——你提供的截圖是撮合記錄頁,不是上傳頁面。因此對於「不能上傳」,以下是待確認的假設,而非已證實的結論。而最可能的那個假設,是一個現在仍會發生的失敗路徑:
你說的是「上傳照片」。iPhone 自 iOS 11 起,拍照預設儲存格式為 HEIC(非 JPEG)。而系統的格式白名單只允許 application/pdf、image/jpeg、image/png(signed-upload.ts:20–24),前端在送出前就會擋下並顯示「證照僅限 PDF/JPG/PNG 文件,請勿夾帶其他檔案。」(profile-form.tsx:104–107)。
這不是舊版問題,是現在就會發生的。既有的 e2e 測試用的是 PNG/PDF,因此不會涵蓋這個情境。
附帶說明:iOS Safari 在某些情況下會於 file input 自動把 HEIC 轉成 JPEG,所以並非百分之百會撞到——這也是為什麼需要你確認實際看到的訊息。
當時你嘗試上傳時,是——
三者的修法完全不同,所以值得先確認。
| 層 | 檔案位置 | 職責 | 狀態 |
|---|---|---|---|
| 前端輸入 | libs/ui/src/lib/doctor-profile-form.tsx:709 |
<input type="file">,accept 白名單 |
已實作 |
| 前端串接 | apps/web/app/(doctor)/profile/profile-form.tsx:101–142 |
取簽章 URL → PUT 檔案 → 回填 documentRef |
已實作 |
| API 端點 | apps/web/app/api/doctor-profiles/me/license-upload-url/route.ts |
withRole(['doctor']) 守門 → 產生 presigned PUT URL |
已實作 |
| 服務層 | libs/doctor/src/lib/license-upload.service.ts |
content-type 白名單 + key 以 accountId 命名空間化 | 已實作 |
| 簽章實作 | libs/doctor/src/lib/s3-signed-upload.ts |
S3 相容簽章(不耦合於 route/service) | 已實作 |
| 物件儲存 | Cloudflare R2 dcm-license-docs |
私有桶(匿名 GET 拒絕、r2.dev 已停用) | 2026-07-09 開通 |
| e2e 測試 | apps/web-e2e/src/m06-license-upload.spec.ts |
簽章 200 → PUT 實檔 → documentRef 回存 → 物件實存 | 已通過 |
採用「presigned URL 直傳」設計——檔案不經過平台伺服器,由瀏覽器直接 PUT 到 R2,平台只負責簽發一把限時鑰匙:
/profile 的證照列選擇檔案doctor-profile-form.tsx:708–718POST /api/doctor-profiles/me/license-upload-url,帶 contentTypeprofile-form.tsx:109licenses/{accountId}/{uuid}——以帳號命名空間化,防跨人覆寫與列舉license-upload.service.ts:34documentRefprofile-form.tsx:128–137| 可能原因 | 你會看到什麼 | 如何確認 |
|---|---|---|
| HEIC 照片被格式白名單擋下 首要假設 |
「證照僅限 PDF/JPG/PNG 文件,請勿夾帶其他檔案。」 | iPhone 拍照預設為 HEIC。現在仍會發生,e2e 未涵蓋此情境 |
| 沒先新增證照列 | 頁面上完全找不到上傳欄位 | 上傳控制項在證照列內部,必須先按「新增證照」把一列加出來才會出現 |
| 測試時間早於 2026-07-09 | 「證照上傳服務尚未開通,請稍後再試」 | 即 API 回 503;R2 環境變數當時尚未設定,現已修復 |
| 檔案超過 10 MB | 「證照檔案上傳失敗」 | 高解析度手機拍照可能接近此上限 |
| 簽章 URL 逾時 | 「證照檔案上傳失敗」 | 選檔後閒置超過 5 分鐘才實際上傳 |
只允許 PDF/JPG/PNG、限 10 MB、限時 300 秒、私有桶不可公開讀,是為了守住專案鐵律第 5 條——絕不蒐集特種個資。把上傳欄位限死在文件類型,是為了避免醫師誤傳病歷、健檢報告等敏感資料進入平台。設計註記見 signed-upload.ts:8–11。
admin 端的簽章下載(讓營運者查驗證照檔案)程式碼已完成(SignedDownloadProvider + GET /api/admin/doctor-profiles/:id/licenses + /admin/credentials 區塊),但 TODO.md 明載此路徑尚未在具備真實 R2 環境變數的環境跑過一次驗證(本機環境變數為空會自動跳過測試)。也就是說:上傳已實機驗證,下載查驗還沒有。這是一個已知的驗證缺口。
把「已完成」與「實際仍有缺口」分開,避免把已完工的東西重做一遍。
| 項目 | 判定 | 建議動作 |
|---|---|---|
| HEIC 照片無法上傳 | 首要待查 | 先確認你當時看到的訊息。若是格式錯誤 → 需決定:擴充白名單支援 HEIC(後端需轉檔)、或在 UI 明確提示「iPhone 請先改存 JPEG」 |
| 證照上傳管線本體 | 已完成 | 四層皆通、已實機驗證。管線不需重做 |
| 撮合 UI 名稱 enrich 管線 | 已完成 | 不需重做。四條路徑皆已接上 |
| 診所無法填寫真實名稱 | 真缺口 | 目前診所名稱永遠是 email 前綴。需補「診所基本資料編輯」介面,或由 admin 後台代填 |
| 醫師未建檔即無姓名 | 體驗缺口 | 撮合發生時醫師通常已建檔;但可考慮在 admin 頁提示「該醫師尚未完成建檔」 |
| admin 簽章下載未實機驗證 | 驗證缺口 | 在具備 R2 環境變數的環境跑一次 m06-license-upload.spec.ts |
| 上傳入口不明顯(藏在證照列內) | 體驗缺口 | 若確認是「找不到」而非格式錯誤,可考慮在無證照列時顯示引導提示 |
這幾項都不是阻斷性問題,也都不影響目前卡在「等律師回覆」的上線主線。建議優先序:① 先確認上傳當下看到的錯誤訊息(一句話就能分辨是格式問題還是找不到入口,決定要不要動 HEIC)> ② 診所真實名稱(影響 admin 日常辨識,且會隨真實診所加入而愈發明顯)> ③ 簽章下載實機驗證(補完既有驗證缺口)。
本次分析採雙知識圖譜交叉查詢,再回讀真實檔案驗證——圖譜只用於定位,所有結論皆以實際檔案內容與 git 實況為準。
license-upload.service.ts、signed-upload.ts、計畫 26 批次 3(M-06)等節點enrichMatchRecordViews 的三個呼叫端與兩個被呼叫端,證明四條路徑全接上。註:該索引的全文檢索索引缺失,關鍵字查詢降級,故僅採用精確 symbol 查詢結果檔案:行號 均經實際讀取確認736db3c = origin/main,工作區乾淨yijingmatch.com 取 HTML 確認部署版本