從一個傳檔案的爛需求,到一個能掛公網的「瞬傳」:我用 WorkBuddy 把它從 HTML 一路做到了 Java

從一個傳檔案的爛需求,到一個能掛公網的「瞬傳」:我用 WorkBuddy 把它從 HTML 一路做到了 Java

作為一個天天連伺服器的人,我的痛點很具體:跨機器搬運小資料,成本高得離譜

我連生產/測試機大多走向日葵或者遠端桌面。做事沒問題,搬東西是真的折磨。本地一個改好的設定、一段現場報錯日誌、一個臨時打的 jar,要送到那台機器上——微信檔案助手得兩邊各登入一次;走雲端硬碟得先上傳再下載,還留紀錄;rz/sz 在弱網下能卡你到懷疑人生。文字比檔案更糟:遠端桌面裡的剪貼簿時靈時不靈,一段 nginx 設定、一串臨時 token,經常只能對著螢幕一個字一個字手打。

我要的東西其實極簡:一個網頁,丟進去,對面輸個碼就拿到,用完自動銷毀,全程不註冊、不登入、不留歷史。市面上的工具要麼太重、要麼強制登入、要麼把你每一次傳輸都記下來。沒有一個戳中我。

那就自己寫。後端 Java 我沒問題,前端是真不想碰——正好讓 WorkBuddy 陪我從頭走一遍,看看它到底能幫到哪一步。

首頁 / 接收態

第一步:選對賽道

進 WorkBuddy 首頁,頂部擺著幾條主線:日常辦公、程式開發、設計創意。我要寫工程,直接點程式開發

選擇程式開發

它沒急著寫程式,先幫我把需求「解構」了

我沒整理需求文件,就把上面那段抱怨原樣丟進去。它最讓我意外的是:沒有立刻進入寫程式的興奮狀態,而是先把我的痛點抽象成了一句話——「遠端環境下檔案與文字的臨時互傳」,然後直接給了我三條技術路線,還配了一張優缺點對照表:

  • 方案 A・純 Web 檔案中轉:檔案落伺服器,瀏覽器直傳直取。零設定、開箱即用,代價是檔案要在伺服器落地。
  • 方案 B・即時剪貼簿同步(WebSocket):雙端即時,像一個線上剪貼簿。文字體驗極好,但只解決文字。
  • 方案 C・綜合雙通道:文字 + 檔案兩條通道都要,前兩者的優點合併。

它自己傾向 C,並說明了理由。

需求與三套方案

這一步的體驗已經贏了一半。我餵的是一句模糊抱怨,它回的是一個有取捨的技術選型——這才是一個工程師想要的對話方式,而不是一上來就丟兩百行能跑但跑偏的程式碼。

圍繞「安全」反覆對線

我沒拍板,先拋了我最在意的約束:這玩意要掛公網給陌生人用,不能裸奔。它順著這條線把安全模型拆成「使用者側」和「伺服器端側」兩塊,顆粒度細到能直接抄進設計文件:

使用者/存取側——身分認證用一次性存取碼(口令),不建註冊體系,每次存取臨時生成、進獨立空間;分享走帶 token 的邀請連結,連結自帶有效期、過期即失效。上傳側——強制 HTTPS,危險副檔名(exe/sh/php)走黑名單攔截,限單檔大小、限單使用者總份數。下載側——下載連結帶有效期、不可被猜測的 URL,支援閱後即焚(下完一次即刪)。

安全架構思考・使用者與上傳側

伺服器端側——檔案按使用者隔離儲存,互相不可見;文字內容加密落盤(類 AES,伺服器只存密文);統一 TTL 自動清理;同 IP 限頻防刷;稽核日誌只記時間與中繼資料、不記內容

安全架構思考・伺服器端與額外想法

它對安全的敏感度超出我預期。 我只說了「別裸奔」三個字,它把密鑰模型、隔離儲存、限頻、稽核邊界一次性鋪開了。這些點後面幾乎原封不動落進了最終實作。

把功能與流程釘死,順手砍掉一半需求

安全聊透,接著定功能。我把發送方、接收方的流程口述了一遍,它順手畫了張端到端的狀態流轉

發送方頁面 → 上傳檔案/文字 → 產生唯一口令(URL)→ 設定有效期 / 下載次數 / 口令 → 接收方頁面憑連結查看、下載。

基本功能與整體架構圖

然後它把多人房間單獨拆成一個模組:臨時空間走 WebSocket,多人輸同一口令進同一房間收發文字;房間有建立者、有有效期、有人人數上限;建立者可踢人、可銷毀,到期訊息全清。

真正值錢的是收尾那句建議——別一口吃成胖子,按交付價值拆兩期

  1. MVP:單檔案/文字分享 + 連結 + 有效期/下載次數;
  2. 增強版:在 MVP 之上疊房間、QR Code、密碼保護。

「先做 MVP,跑通了再加功能,成本最低。」

拆 MVP / 增強版

我當時正處在「功能我全都要」的上頭狀態,它沒有順著我堆,反而幫我做減法。一個 AI 助手能在你興奮的時候踩一腳煞車、把你拽回 MVP 節奏,這點比寫得快重要得多。後面證明這個拆法是對的。

一張參考圖,它讀出了一份「設計規範」

功能定了,長相還沒譜。我懶得描述,直接截了張看著順眼的風格圖丟過去。它沒瞎誇,而是把這張圖反解成了一份可執行的視覺規範:淺灰底 + 白色卡片、綠色主色(用於按鈕高亮與選中態)、大圓角、低資訊密度、頂部三大入口(接收/發送/房間)、右側資訊面板 + QR Code、底部安全提示。

隨後它定了 MVP 形態,還順嘴問要不要給專案建長期檔案。我說行,起個名叫「一隻牛博」,照這方向開幹。它回:「好,牛博,開始動手。」

給參考圖,定下風格

從一張隨手截的圖到一份結構化設計 token,這一步把「我說不清的審美」翻譯成了「它能落地的規則」,溝通成本直接砍半。

第一版:原生 HTML/JS 先把骨架立起來

第一版來得很快,純原生 HTML/JS,跑在 localhost:3000。骨架已經齊了:頂部三個 tab、中間發送區、有效期選擇、右側資訊卡。粗是粗,但參考圖那個味道出來了。

第一版效果

我讓它直接跑,它把啟動指令一併給全:npm installnpm start,幾行就起來了。它不只交程式碼,連「怎麼把它跑起來」都替你想好了。

詢問是否執行

第二版:換 Vue,順手要個毛玻璃

原生寫著寫著,命令式的 DOM 操作堆起來結構開始發散。我讓它用 Vue 重構一版,順便提了個我一直想要的效果——毛玻璃磨砂。它上了 backdrop-filter: blur() 那一套半透明,整體質感立刻不一樣了。

改用 Vue 重構

第三版:純摳細節,把磨砂透明度調到位

毛玻璃第一版太糊,背景插畫全被磨沒了,白費。我讓它把透明度往回收,這步沒技術含量、純審美來回磨——從很高的透明度一點點壓,大概落在 45% 上下,背景插畫能隱約透出來又不搶前景內容。截圖裡我標了句「有點效果」,就是這版終於看順眼了。

繼續優化透明度

值得一提的是,這種「差一點」的體感調優,它接得很穩:我不給具體數值,只說「太透了/再實一點」,它能順著語意往對的方向收斂,而不是要我報參數。

最後落到 Java:技術棧是被「部署」推著走的

聊到上線,方向變了——而且是合理地變。

這東西要長期掛公網給人用,Node 那套部署還得裝執行環境、起進程、配守護,掛了得拉起。我後端本來就是 Java,乾脆整體落到 Spring Boot 3.3.5 + Java 17:打成單個可執行 jar,配多階段 Dockerfile(maven 先建置、產物塞進 jre 執行映像),docker-compose 一拉即起,前置 Nginx 反向代理 + Let’s Encrypt 自動簽證書

前端反而收了回來:不再背 Vue 的建置鏈,而是回到模組化原生 JS——features / ui / utils / api 分目錄拆清。後端 Java 接管所有重活:儲存、限流、定時清理、口令生成、QR Code(ZXing 直出)。一個 jar 全梭哈,部署這件事一下就乾淨了。它把上線指令也逐條列了出來。

給出部署與最終效果

HTML → Vue → Java 這條看似跳躍的路線,其實有一條暗線:原生用來驗形態,Vue 用來試互動與質感,Java 用來扛部署與安全。 技術棧不是它拍腦袋換的,是跟著「這東西到底怎麼用、怎麼上線」自然長出來的。這種「為約束選型」的判斷力,正是它專業的地方。

到這裡,最終落地的工程能力已經不是一個玩具了。隨手貼幾個我最後定稿裡的真實參數,佐證它給的不是花架子:

  • 端到端加密:前端用 AES-GCM 256bit 加解密,密鑰編碼進 URL 的 #k= 片段、絕不上傳伺服器, 伺服器從頭到尾只摸得到密文。這正是首頁「伺服器只保存密文」那句話的底氣。
  • 滑動視窗限流:同 IP 上傳 3 次/分、30 次/時,口令查詢 30 次/分連續 20 次口令試錯觸發 10 分鐘冷卻——直接掐死暴力猜碼。
  • 並發閘:全域並發上傳 10、單 IP 2,防止有人拿上傳打滿磁碟。
  • 分級下載額度:按體積分檔,≤10MB 給 20 次、≤100MB 給 10 次、更大給 5 次
  • 定時清理60 秒一輪掃過期內容,最長保留 120 分鐘(2 小時)封頂。

翻開程式碼:它寫得到底怎麼樣

參數好看不代表程式碼好。我專門挑了幾段它生成的關鍵實作貼出來——這幾段恰恰是最容易寫爛、最能看出功底的地方

第一段,端到端加密的密鑰處理。 整個 E2E 的命門在於「密鑰到底放哪」。它的做法是:密鑰只編碼進 URL 的 #k= 片段——而 URL fragment 按瀏覽器規範根本不會隨請求發往伺服器,於是伺服器從物理上就拿不到密鑰,只能存密文。

javascript 代碼解讀複製代碼// 加密後,密鑰拼進 URL 的 hash 片段,絕不進入請求體
export function encryptedUrl(url, key) {
  return `${url}#k=${encodeURIComponent(key)}`;
}

// 接收端從 location.hash 裡把密鑰取回來,在本地解密
export function keyFromLocation() {
  const params = new URLSearchParams(location.hash.replace(/^#/, ""));
  return params.get("k") || "";
}

async function encryptBytes(plainBytes, rawKey) {
  const iv = randomBytes(IV_BYTES);                       // 每次隨機 12 位元組 IV
  const key = await importKey(rawKey);
  const cipherBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plainBytes);
  return concatBytes(iv, new Uint8Array(cipherBuffer));   // IV 前置拼進密文,解密時再切出來
}

用 WebCrypto 的 AES-GCM、每次隨機 IV、IV 前置拼接——這是教科書級的正確姿勢,既沒有自己造輪子,也沒有把密鑰誤傳上伺服器。一個非密碼學背景的開發者很容易在這裡翻車,它沒有。

第二段,限流。 它用的是帶時間窗的滑動計數器,而且對計數器物件做了 synchronized,在並發下不會把視窗算錯:

java 代碼解讀複製代碼public void require(String key, int maxCount, Duration window, String message) {
    Instant now = Instant.now();
    WindowCounter counter = counters.computeIfAbsent(key, ignored -> new WindowCounter(now, 0));
    synchronized (counter) {
        if (Duration.between(counter.windowStart, now).compareTo(window) >= 0) {
            counter.windowStart = now;   // 視窗過期,重置
            counter.count = 0;
        }
        if (counter.count >= maxCount) {
            throw new ApiException(HttpStatus.TOO_MANY_REQUESTS, message);
        }
        counter.count++;
    }
}

一個方法靠傳入的 key + window + maxCount 同時服務「上傳按分鐘/按小時」「查詢按分鐘」「訊息按分鐘」等所有場景,沒有為每種限流複製一份邏輯。鎖的粒度也壓在單個計數器物件上、而不是整張表,並發吞吐不會被一把大鎖拖死。

第三段,我最服的一處——上傳並發閘為什麼放在 Filter 層。 它沒把這個保護寫進 Controller,而是單獨做了個 OncePerRequestFilter,並且在註解裡寫清了原因

java 代碼解讀複製代碼/**
 * Acquires upload concurrency permits before Spring parses multipart bodies.
 *
 * <p>Controller-level guards run too late for large uploads because the request
 * body may already be parsed. Keeping this protection at the filter layer
 * limits concurrent body ingestion from the same IP as well as application
 * processing.</p>
 */
@Component
public class UploadConcurrencyFilter extends OncePerRequestFilter {
    // ...
    try (TransferGuardService.Guard ignored = transferGuardService.upload(ClientIpUtil.resolve(request))) {
        filterChain.doFilter(request, response);   // 拿到許可才放行,出了作用域自動釋放
    } catch (ApiException exception) {
        writeApiError(response, exception);
    }
}

「Controller 層的限制對大檔案來說太晚了,因為請求體可能已經被解析」——這是一個踩過坑、真懂 Spring 請求生命週期的人才會寫的註解。配合 try-with-resources 讓許可自動釋放,既擋住了惡意並發上傳打滿磁碟,又不會洩漏許可。這一段單拎出來,放進任何一個生產專案的 Code Review 都挑不出毛病。

第四段,清理排程的克制。 所有過期資料(分享、殭屍上傳、房間、限流計數)的回收,只用一個 @Scheduled 方法串起來,週期還能從設定注入:

java 代碼解讀複製代碼@Scheduled(fixedDelayString = "${app.cleanup-interval-seconds:60}000")
public void cleanup() {
    shareStorageService.cleanupExpired();
    shareStorageService.cleanupStaleUploads();
    roomStorageService.cleanupExpired();
    rateLimitService.cleanup();
}

沒有為每類資料各起一個排程器,也沒把清理邏輯散落到各個 Service 裡偷偷跑——收口到一處、依賴注入、週期可配,該簡單的地方就讓它保持簡單。

把這四段連起來看,它的程式碼不是「能跑就行」的水平:關注點分離清楚、並發與邊界考慮到位、註解寫在真正需要解釋的地方、不重複也不過度設計。說實話,這個品質已經接近一個還不錯的中高階工程師的手筆了。

成品:那些被產品邏輯反推出來的設計

介面我就不逐個念了。我更想說的是,最終這套互動裡有幾個決策,是被「臨時傳輸」這個內核反過來逼出來的——它們看著是 UI,本質是產品判斷。這些點,WorkBuddy 在前面的對話裡基本都替我想到了。

第一個決策:預設落在「接收」,而不是「發送」。 這個選擇我很認同。發送的人是主動的,他知道自己要幹嘛;接收的人是被動的,他多半是被一個口令或 QR Code 引過來的,越早讓他看到「在哪輸碼」越好。 所以首頁一進來就是接收態、一個大口令框直接頂在中央,把最高頻、最沒耐心的那條路徑放在了零點擊的位置。右側那條資訊欄(最長 2 小時、單檔 200MB、無需登入)則在不打擾主流程的前提下,一句話講清了「這是個臨時的東西」。

首頁預設即接收態

第二個決策:把「有效期」和「接收次數」做成一等公民。 普通雲端硬碟的分享,過期是個藏在二級選單裡的進階選項;在這裡,它們是建立流程裡躲不掉的兩個旋鈕——有效期(10 分鐘到 2 小時)直接平鋪成按鈕,接收次數(預設 1 次)擺在顯眼處。這不是堆功能,而是用互動把產品價值觀頂到使用者臉上:這東西生來就是要消失的,你必須為它的「短命」做一次決定。文字框右下即時跳的位元組數和 256KB 上限,也在持續暗示邊界感。

發送端把時效與次數前置

第三個決策:口令、QR Code、連結,三個入口一次性全給。 這是被真實場景逼的——我的原始痛點就是「跨裝置」,而跨裝置意味著沒有統一的複製貼上通道。所以生成結果頁同時吐出 8 位口令(適合念給旁邊的人/手打)、QR Code(適合電腦發手機掃)、帶密鑰的完整連結(適合 IM 裡丟過去),三條路通向同一份內容。值得單獨說的是連結尾巴上那截 #k=E6LWXY1i0Ptt...——它就是前文那段加密程式碼裡「只活在 fragment 裡、永不上送伺服器」的 AES 密鑰。底部「端到端加密・伺服器只保存密文」這句話,到這裡是有程式碼兜底的,不是貼上去好看的。

三入口與可見的 E2E 密鑰

第四個決策:讓安全「可被感知」。 加密這件事,做了但使用者看不見,等於沒做。接收頁每一條內容前面都掛著 E2E 標記,旁邊跟著剩餘次數、位元組數、建立時間,頂上是還在跳的銷毀倒數。使用者不需要懂 AES-GCM,但他需要在那一眼裡相信「這東西是加密的、是會過期的、是只給我看的」——這種把後端保證翻譯成前端可見訊號的處理,是很多工具會省掉、但恰恰最影響信任的一環。

接收頁讓加密與時效可見

第五個決策:銷毀態是被當成一個正經狀態來設計的,不是丟一個錯誤。 大多數應用對「內容沒了」的處理就是一個 404 或一行紅字。但對一個主打「閱後即焚」的產品來說,「已銷毀」恰恰是它最該講好的故事。所以這裡是一塊完整的狀態卡:明確告訴你「接收次數已用完或內容已過期,伺服器已刪除臨時內容」,並直接給出「重新建立」的下一步。關鍵是這塊文案背後是真的——伺服器排程任務把資料實體刪掉了,不是前端藏起來騙你。 我最初要的那句「用完自動沒、不留痕」,在這一屏被兌現了。

銷毀態是認真設計的狀態

第六個決策:增強版的多人房間,真的落地了,而且是行動端優先驗證的。 前面設計階段被單獨拆出去、靠 WebSocket 撐起來的那個「臨時空間」,沒有停在簡報上。手機進房後,頂部直接是 30 人在線 + 01:58:13 銷毀倒數——把「多人」和「臨時」兩個最核心的屬性擺在第一屏,下面才是帶時間戳和位元組數的即時訊息流、QR Code 邀請和退出。一個 AI 協助搭的專案,能把二期功能也照著一期的設計語言完整收尾、並在窄螢幕上保持同樣克制的排版,這個完成度超出我預期。

多人臨時空間在行動端落地

寫在最後

整個專案斷斷續續聊下來,程式碼我幾乎沒自己敲,但說實話也沒省到「動動嘴就行」的程度。真正花時間的是前面那些來回——三套方案選哪條、安全到底要做到哪一層、功能砍到什麼程度算 MVP、參考圖那個調調怎麼落地。這些想清楚了,後面寫程式反倒是最不費勁的部分。

一個「傳檔案傳文字好煩」的爛念頭,就這麼變成了一個能掛公網、掃碼就用、用完自動沒的小站。


原文出處:https://juejin.cn/post/7654808985048416290


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝13   💬2   ❤️1
640
🥈
我愛JS
📝1   ❤️1
69
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登