[{"data":1,"prerenderedAt":5218},["ShallowReactive",2],{"navigation":3,"index":34,"index-blogs":162,"mdc--p12pnv-key":5189,"mdc--x9ibd0-key":5200,"mdc-zaetw1-key":5209},[4],{"title":5,"path":6,"stem":7,"children":8,"page":33},"Articles","\u002Farticles","articles",[9,13,17,21,25,29],{"title":10,"path":11,"stem":12},"用 Daily Snapshot 提升統計查詢速度","\u002Farticles\u002Fdaily-snapshot","articles\u002Fdaily-snapshot",{"title":14,"path":15,"stem":16},"GKE 部署","\u002Farticles\u002Fgke-deployment","articles\u002Fgke-deployment",{"title":18,"path":19,"stem":20},"從 LLM 到 Agent：打通底層邏輯","\u002Farticles\u002Fllm-to-agent","articles\u002Fllm-to-agent",{"title":22,"path":23,"stem":24},"資訊安全實踐","\u002Farticles\u002Fsecurity-best-practices","articles\u002Fsecurity-best-practices",{"title":26,"path":27,"stem":28},"單機架構的性能優化","\u002Farticles\u002Fsingle-machine-performance","articles\u002Fsingle-machine-performance",{"title":30,"path":31,"stem":32},"伺服器渲染 SSR","\u002Farticles\u002Fssr","articles\u002Fssr",false,{"id":35,"title":36,"about":37,"blog":40,"body":43,"description":44,"experience":45,"extension":72,"faq":73,"hero":112,"meta":129,"navigation":130,"path":131,"seo":132,"stem":135,"testimonials":136,"__hash__":161},"index\u002Findex.yml","嗨，我是 Gary\u003Cbr>一位後端工程師",{"title":38,"description":39},"關於我","擁有 6 年建置分散式系統與雲端原生應用程式經驗的後端工程師。\n專精於 Go、Kubernetes，以及設計團隊在生產環境中依賴的可擴展 API。\n",{"title":41,"description":42},"最新文章","最近的一些想法。",null,"我打造穩定、可擴展的後端系統，維護正式環境運行。熟悉分散式系統、微服務架構與 AI 工具。",{"title":46,"items":47},"工作經歷",[48,55,60,66],{"position":49,"date":50,"company":51},"後端工程師 @","2026 - 現在",{"name":52,"url":53,"color":54},"鑽誠科技","https:\u002F\u002Fnuxt.com","#fdcb6e",{"position":49,"date":56,"company":57},"2023 - 2026",{"name":58,"url":53,"color":59},"太禾科技","#00DC82",{"position":49,"date":61,"company":62},"2021 - 2023",{"name":63,"url":64,"color":65},"睿實科技","https:\u002F\u002Fraycast.com","#FF6363",{"position":49,"date":67,"company":68},"2020 - 2021",{"name":69,"url":70,"color":71},"華苓科技","https:\u002F\u002Flinear.app","#5E6AD2","yml",{"title":74,"description":75,"categories":76},"常見問題","關於我的工作流程與服務的常見問題解答。",[77,89,104],{"title":78,"questions":79},"服務與流程",[80,83,86],{"label":81,"content":82},"你提供哪些服務？","我專精於後端工程與雲端基礎設施。包含設計與建置 REST\u002FgRPC API、分散式系統架構、資料庫 schema 設計與優化、Kubernetes 叢集建置與維運、CI\u002FCD 流程自動化，以及為擴展後端的團隊提供技術顧問服務。\n",{"label":84,"content":85},"你的工程流程是什麼樣的？","我從需求收集與系統設計開始，在寫下第一行程式碼之前先產出架構文件。接著以小而經過充分測試的增量進行迭代——整合測試、壓力測試，以及從第一天起就內建的可觀測性。我與產品和前端團隊密切合作，確保介面乾淨且有版本管理。\n",{"label":87,"content":88},"你接新創公司的案子嗎？","當然。早期新創公司能從務實的架構選擇中受益，這些選擇可以擴展而不需要完全重寫。我幫助團隊快速推進，同時不累積沉重的技術債。\n",{"title":90,"questions":91},"定價與時程",[92,95,98,101],{"label":93,"content":94},"一個專案通常要多少錢？","視範圍而定。單一功能或 API 模組起價約 NT$3 萬～10 萬；完整後端系統（含資料庫設計、部署、CI\u002FCD）通常在 NT$15 萬～50 萬之間。持續顧問或技術審查則以 NT$15,000～30,000 \u002F 天計費。\n",{"label":96,"content":97},"付款條件是什麼？","通常需要 30%～50% 訂金才能啟動專案，其餘於交付時付清。接受銀行轉帳或其他雙方同意的方式。\n",{"label":99,"content":100},"一個典型的專案需要多久？","單一 API 或功能模組約 1–3 週。完整後端系統建置通常需要 1–3 個月，視需求複雜度與溝通頻率而定。\n",{"label":102,"content":103},"你提供顧問保留金或持續支援嗎？","可以。如果需要長期技術諮詢、系統維護或功能迭代，歡迎討論月費合作方式。\n",{"title":38,"questions":105},[106,109],{"label":107,"content":108},"你最享受工作的哪個部分？","看到實做的系統能穩定面對大流量，這件事本身就很有成就感。\n",{"label":110,"content":111},"工作之外你有什麼愛好？","打電動、看技術文章、偶爾研究一些有趣的開源工具，最近在玩 AI 相關的東西。\n",{"images":113},[114,117,120,123,126],{"src":115,"alt":116},"\u002Fhero\u002F111.jpg","Hero 1",{"src":118,"alt":119},"\u002Fhero\u002F222.jpg","Hero 2",{"src":121,"alt":122},"\u002Fhero\u002F333.jpg","Hero 3",{"src":124,"alt":125},"\u002Fhero\u002Fhero-4.jpg","Hero 4",{"src":127,"alt":128},"\u002Fhero\u002Fhero-5.jpg","Hero 5",{},true,"\u002F",{"title":133,"description":134},"Gary Portfolio","歡迎來到我的作品集！我是 Gary，一位專精於分散式系統、雲端基礎設施與高效能 API 的後端工程師。","index",[137,145,153],{"quote":138,"author":139},"Gary 從零開始架構了我們整個資料管道。他對分散式系統的深刻理解，以及針對故障模式進行設計的能力，為我們節省了無數的事故應對時間。吞吐量提升了 5 倍，p99 延遲降低了一半。",{"name":140,"description":141,"avatar":142},"Lucas","友人一號",{"src":143,"srcset":144},"https:\u002F\u002Fapi.dicebear.com\u002F9.x\u002Florelei\u002Fsvg?seed=Lucas","https:\u002F\u002Fapi.dicebear.com\u002F9.x\u002Florelei\u002Fsvg?seed=Lucas 2x",{"quote":146,"author":147},"與 Gary 一起重新設計 API 是一次徹底的改變。他不僅交付了乾淨、文件完整的 REST\u002FgRPC 層，還主動反駁了會產生長期技術債的需求。這正是你在關鍵專案中希望擁有的那種資深工程師。",{"name":148,"description":149,"avatar":150},"Jason","友人二號",{"src":151,"srcset":152},"https:\u002F\u002Fapi.dicebear.com\u002F9.x\u002Florelei\u002Fsvg?seed=Jason","https:\u002F\u002Fapi.dicebear.com\u002F9.x\u002Florelei\u002Fsvg?seed=Jason 2x",{"quote":154,"author":155},"Gary 以零停機時間完成了我們從單體架構到微服務的遷移。他的 Kubernetes 專業知識以及對可觀測性的嚴謹態度，讓整個團隊在整個上線過程中都充滿信心。遷移後第一個月，事故數量降至幾乎為零。",{"name":156,"description":157,"avatar":158},"Johnson","友人三號",{"src":159,"srcset":160},"https:\u002F\u002Fapi.dicebear.com\u002F9.x\u002Florelei\u002Fsvg?seed=Johnson","https:\u002F\u002Fapi.dicebear.com\u002F9.x\u002Florelei\u002Fsvg?seed=Johnson 2x","6MvpvVDDb6-LhBHR55E2sRPoeY3toEOuqxtt1nYBZqg",[163,1718,4016],{"id":164,"title":14,"author":165,"body":169,"date":1711,"description":1712,"extension":1713,"externalUrl":43,"image":1714,"meta":1715,"minRead":291,"navigation":130,"path":15,"seo":1716,"stem":16,"__hash__":1717},"blog\u002Farticles\u002Fgke-deployment.md",{"name":166,"avatar":167},"Gary",{"src":168,"alt":166},"\u002Fimages\u002Fselfie.webp",{"type":170,"value":171,"toc":1684},"minimark",[172,176,223,226,229,234,350,357,361,439,443,476,480,542,546,596,598,601,605,623,627,685,689,802,806,886,890,1026,1028,1032,1036,1078,1083,1087,1098,1101,1107,1109,1113,1116,1122,1126,1268,1279,1283,1524,1529,1531,1534,1680],[173,174,175],"h2",{"id":175},"架構",[177,178,179,187,193,199,205,211,217],"ul",{},[180,181,182,186],"li",{},[183,184,185],"strong",{},"gshop-api"," — Node.js\u002FExpress，Port 3001",[180,188,189,192],{},[183,190,191],{},"gshop-dashboard"," — Nuxt.js SSR，Port 3002",[180,194,195,198],{},[183,196,197],{},"gshop-web"," — Nuxt.js SSR，Port 3003",[180,200,201,204],{},[183,202,203],{},"GKE Autopilot Cluster"," — gshop-cluster，asia-east1",[180,206,207,210],{},[183,208,209],{},"Artifact Registry"," — asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop",[180,212,213,216],{},[183,214,215],{},"Database"," — Supabase（Session Pooler）",[180,218,219,222],{},[183,220,221],{},"Domain"," — garydemo.com（Cloudflare）",[224,225],"hr",{},[173,227,228],{"id":228},"部署流程",[230,231,233],"h3",{"id":232},"_1-build-push-image每次更新都要做","1. Build & Push Image（每次更新都要做）",[235,236,241],"pre",{"className":237,"code":238,"language":239,"meta":240,"style":240},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# API\ngcloud builds submit .\u002Fgshop-api \\\n  --tag asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n\n# Dashboard\ngcloud builds submit .\u002Fgshop-dashboard \\\n  --tag asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest\n\n# Web\ngcloud builds submit .\u002Fgshop-web \\\n  --tag asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb:latest\n","bash","",[242,243,244,253,274,283,289,295,309,317,322,328,342],"code",{"__ignoreMap":240},[245,246,249],"span",{"class":247,"line":248},"line",1,[245,250,252],{"class":251},"sHwdD","# API\n",[245,254,256,260,264,267,270],{"class":247,"line":255},2,[245,257,259],{"class":258},"sBMFI","gcloud",[245,261,263],{"class":262},"sfazB"," builds",[245,265,266],{"class":262}," submit",[245,268,269],{"class":262}," .\u002Fgshop-api",[245,271,273],{"class":272},"sTEyZ"," \\\n",[245,275,277,280],{"class":247,"line":276},3,[245,278,279],{"class":262},"  --tag",[245,281,282],{"class":262}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n",[245,284,286],{"class":247,"line":285},4,[245,287,288],{"emptyLinePlaceholder":130},"\n",[245,290,292],{"class":247,"line":291},5,[245,293,294],{"class":251},"# Dashboard\n",[245,296,298,300,302,304,307],{"class":247,"line":297},6,[245,299,259],{"class":258},[245,301,263],{"class":262},[245,303,266],{"class":262},[245,305,306],{"class":262}," .\u002Fgshop-dashboard",[245,308,273],{"class":272},[245,310,312,314],{"class":247,"line":311},7,[245,313,279],{"class":262},[245,315,316],{"class":262}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest\n",[245,318,320],{"class":247,"line":319},8,[245,321,288],{"emptyLinePlaceholder":130},[245,323,325],{"class":247,"line":324},9,[245,326,327],{"class":251},"# Web\n",[245,329,331,333,335,337,340],{"class":247,"line":330},10,[245,332,259],{"class":258},[245,334,263],{"class":262},[245,336,266],{"class":262},[245,338,339],{"class":262}," .\u002Fgshop-web",[245,341,273],{"class":272},[245,343,345,347],{"class":247,"line":344},11,[245,346,279],{"class":262},[245,348,349],{"class":262}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb:latest\n",[351,352,353],"blockquote",{},[354,355,356],"p",{},"Cloud Build 在雲端 build（解決本地 Apple Silicon arm64\u002Famd64 問題）",[230,358,360],{"id":359},"_2-建立-kubernetes-secret","2. 建立 Kubernetes Secret",[235,362,364],{"className":237,"code":363,"language":239,"meta":240,"style":240},"kubectl create secret generic gshop-api-secret \\\n  --from-literal=DATABASE_URL=\"...\" \\\n  --from-literal=JWT_SECRET=\"...\" \\\n  --from-literal=GCS_BUCKET_NAME=\"...\" \\\n  --from-literal=ANTHROPIC_API_KEY=\"...\"\n",[242,365,366,385,401,414,427],{"__ignoreMap":240},[245,367,368,371,374,377,380,383],{"class":247,"line":248},[245,369,370],{"class":258},"kubectl",[245,372,373],{"class":262}," create",[245,375,376],{"class":262}," secret",[245,378,379],{"class":262}," generic",[245,381,382],{"class":262}," gshop-api-secret",[245,384,273],{"class":272},[245,386,387,390,394,397,399],{"class":247,"line":255},[245,388,389],{"class":262},"  --from-literal=DATABASE_URL=",[245,391,393],{"class":392},"sMK4o","\"",[245,395,396],{"class":262},"...",[245,398,393],{"class":392},[245,400,273],{"class":272},[245,402,403,406,408,410,412],{"class":247,"line":276},[245,404,405],{"class":262},"  --from-literal=JWT_SECRET=",[245,407,393],{"class":392},[245,409,396],{"class":262},[245,411,393],{"class":392},[245,413,273],{"class":272},[245,415,416,419,421,423,425],{"class":247,"line":285},[245,417,418],{"class":262},"  --from-literal=GCS_BUCKET_NAME=",[245,420,393],{"class":392},[245,422,396],{"class":262},[245,424,393],{"class":392},[245,426,273],{"class":272},[245,428,429,432,434,436],{"class":247,"line":291},[245,430,431],{"class":262},"  --from-literal=ANTHROPIC_API_KEY=",[245,433,393],{"class":392},[245,435,396],{"class":262},[245,437,438],{"class":392},"\"\n",[230,440,442],{"id":441},"_3-建立-cloudflare-origin-certificate-tls-secret","3. 建立 Cloudflare Origin Certificate TLS Secret",[235,444,446],{"className":237,"code":445,"language":239,"meta":240,"style":240},"kubectl create secret tls cloudflare-origin-cert \\\n  --cert=certificate.pem \\\n  --key=private.key\n",[242,447,448,464,471],{"__ignoreMap":240},[245,449,450,452,454,456,459,462],{"class":247,"line":248},[245,451,370],{"class":258},[245,453,373],{"class":262},[245,455,376],{"class":262},[245,457,458],{"class":262}," tls",[245,460,461],{"class":262}," cloudflare-origin-cert",[245,463,273],{"class":272},[245,465,466,469],{"class":247,"line":255},[245,467,468],{"class":262},"  --cert=certificate.pem",[245,470,273],{"class":272},[245,472,473],{"class":247,"line":276},[245,474,475],{"class":262},"  --key=private.key\n",[230,477,479],{"id":478},"_4-套用-k8s-設定","4. 套用 K8s 設定",[235,481,483],{"className":237,"code":482,"language":239,"meta":240,"style":240},"kubectl apply -f k8s\u002Fbackend-config.yaml\nkubectl apply -f k8s\u002Fapi-deployment.yaml\nkubectl apply -f k8s\u002Fdashboard-deployment.yaml\nkubectl apply -f k8s\u002Fweb-deployment.yaml\nkubectl apply -f k8s\u002Fingress.yaml\n",[242,484,485,498,509,520,531],{"__ignoreMap":240},[245,486,487,489,492,495],{"class":247,"line":248},[245,488,370],{"class":258},[245,490,491],{"class":262}," apply",[245,493,494],{"class":262}," -f",[245,496,497],{"class":262}," k8s\u002Fbackend-config.yaml\n",[245,499,500,502,504,506],{"class":247,"line":255},[245,501,370],{"class":258},[245,503,491],{"class":262},[245,505,494],{"class":262},[245,507,508],{"class":262}," k8s\u002Fapi-deployment.yaml\n",[245,510,511,513,515,517],{"class":247,"line":276},[245,512,370],{"class":258},[245,514,491],{"class":262},[245,516,494],{"class":262},[245,518,519],{"class":262}," k8s\u002Fdashboard-deployment.yaml\n",[245,521,522,524,526,528],{"class":247,"line":285},[245,523,370],{"class":258},[245,525,491],{"class":262},[245,527,494],{"class":262},[245,529,530],{"class":262}," k8s\u002Fweb-deployment.yaml\n",[245,532,533,535,537,539],{"class":247,"line":291},[245,534,370],{"class":258},[245,536,491],{"class":262},[245,538,494],{"class":262},[245,540,541],{"class":262}," k8s\u002Fingress.yaml\n",[230,543,545],{"id":544},"_5-更新部署有新-commit-時","5. 更新部署（有新 commit 時）",[235,547,549],{"className":237,"code":548,"language":239,"meta":240,"style":240},"# 重新 build image\ngcloud builds submit .\u002Fgshop-dashboard \\\n  --tag asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest\n\n# 重啟 pod（Rolling Update，不會 downtime）\nkubectl rollout restart deployment\u002Fgshop-dashboard\n",[242,550,551,556,568,574,578,583],{"__ignoreMap":240},[245,552,553],{"class":247,"line":248},[245,554,555],{"class":251},"# 重新 build image\n",[245,557,558,560,562,564,566],{"class":247,"line":255},[245,559,259],{"class":258},[245,561,263],{"class":262},[245,563,266],{"class":262},[245,565,306],{"class":262},[245,567,273],{"class":272},[245,569,570,572],{"class":247,"line":276},[245,571,279],{"class":262},[245,573,316],{"class":262},[245,575,576],{"class":247,"line":285},[245,577,288],{"emptyLinePlaceholder":130},[245,579,580],{"class":247,"line":291},[245,581,582],{"class":251},"# 重啟 pod（Rolling Update，不會 downtime）\n",[245,584,585,587,590,593],{"class":247,"line":297},[245,586,370],{"class":258},[245,588,589],{"class":262}," rollout",[245,591,592],{"class":262}," restart",[245,594,595],{"class":262}," deployment\u002Fgshop-dashboard\n",[224,597],{},[173,599,600],{"id":600},"遇到的狀況與解決",[230,602,604],{"id":603},"_1-arm64amd64-平台不符","1. arm64\u002Famd64 平台不符",[177,606,607,613],{},[180,608,609,612],{},[183,610,611],{},"問題","：本地 Apple Silicon Mac build 出 arm64 image，GKE 需要 amd64",[180,614,615,618,619,622],{},[183,616,617],{},"解法","：改用 ",[242,620,621],{},"gcloud builds submit","，Cloud Build 在 amd64 機器上 build",[230,624,626],{"id":625},"_2-imagepullbackoff沒權限拉-image","2. ImagePullBackOff（沒權限拉 image）",[177,628,629,634],{},[180,630,631,633],{},[183,632,611],{},"：GKE Service Account 沒有 Artifact Registry 讀取權限",[180,635,636,638,639],{},[183,637,617],{},"：\n",[235,640,642],{"className":237,"code":641,"language":239,"meta":240,"style":240},"gcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:620172615694-compute@developer.gserviceaccount.com\" \\\n  --role=\"roles\u002Fartifactregistry.reader\"\n",[242,643,644,659,673],{"__ignoreMap":240},[245,645,646,648,651,654,657],{"class":247,"line":248},[245,647,259],{"class":258},[245,649,650],{"class":262}," projects",[245,652,653],{"class":262}," add-iam-policy-binding",[245,655,656],{"class":262}," gshop-497319",[245,658,273],{"class":272},[245,660,661,664,666,669,671],{"class":247,"line":255},[245,662,663],{"class":262},"  --member=",[245,665,393],{"class":392},[245,667,668],{"class":262},"serviceAccount:620172615694-compute@developer.gserviceaccount.com",[245,670,393],{"class":392},[245,672,273],{"class":272},[245,674,675,678,680,683],{"class":247,"line":276},[245,676,677],{"class":262},"  --role=",[245,679,393],{"class":392},[245,681,682],{"class":262},"roles\u002Fartifactregistry.reader",[245,684,438],{"class":392},[230,686,688],{"id":687},"_3-ingress-遲遲拿不到-ipneg-not-ready","3. Ingress 遲遲拿不到 IP（NEG not ready）",[177,690,691,701,740,750],{},[180,692,693,696,697,700],{},[183,694,695],{},"問題 1","：用了 ",[242,698,699],{},"spec.ingressClassName: gce","，GKE 的 controller 不認這個，要用 annotation",[180,702,703,705,706],{},[183,704,617],{},"：改成：\n",[235,707,711],{"className":708,"code":709,"language":710,"meta":240,"style":240},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","metadata:\n  annotations:\n    kubernetes.io\u002Fingress.class: gce\n","yaml",[242,712,713,722,729],{"__ignoreMap":240},[245,714,715,719],{"class":247,"line":248},[245,716,718],{"class":717},"swJcz","metadata",[245,720,721],{"class":392},":\n",[245,723,724,727],{"class":247,"line":255},[245,725,726],{"class":717},"  annotations",[245,728,721],{"class":392},[245,730,731,734,737],{"class":247,"line":276},[245,732,733],{"class":717},"    kubernetes.io\u002Fingress.class",[245,735,736],{"class":392},":",[245,738,739],{"class":262}," gce\n",[180,741,742,745,746,749],{},[183,743,744],{},"問題 2","：Service 上有舊的 ",[242,747,748],{},"networking.gke.io\u002Ftarget-pool"," annotation 造成衝突",[180,751,752,638,754,801],{},[183,753,617],{},[235,755,757],{"className":237,"code":756,"language":239,"meta":240,"style":240},"kubectl annotate svc gshop-api networking.gke.io\u002Ftarget-pool-\nkubectl annotate svc gshop-dashboard networking.gke.io\u002Ftarget-pool-\nkubectl annotate svc gshop-web networking.gke.io\u002Ftarget-pool-\n",[242,758,759,775,788],{"__ignoreMap":240},[245,760,761,763,766,769,772],{"class":247,"line":248},[245,762,370],{"class":258},[245,764,765],{"class":262}," annotate",[245,767,768],{"class":262}," svc",[245,770,771],{"class":262}," gshop-api",[245,773,774],{"class":262}," networking.gke.io\u002Ftarget-pool-\n",[245,776,777,779,781,783,786],{"class":247,"line":255},[245,778,370],{"class":258},[245,780,765],{"class":262},[245,782,768],{"class":262},[245,784,785],{"class":262}," gshop-dashboard",[245,787,774],{"class":262},[245,789,790,792,794,796,799],{"class":247,"line":276},[245,791,370],{"class":258},[245,793,765],{"class":262},[245,795,768],{"class":262},[245,797,798],{"class":262}," gshop-web",[245,800,774],{"class":262},"\n再刪掉重建 Ingress",[230,803,805],{"id":804},"_4-502-bad-gateway健康檢查失敗","4. 502 Bad Gateway（健康檢查失敗）",[177,807,808,840],{},[180,809,810,812,813,816,817],{},[183,811,611],{},"：GCP Load Balancer 預設用 ",[242,814,815],{},"GET \u002F"," 做健康檢查，但各服務行為不同：\n",[177,818,819,825,834],{},[180,820,821,822,824],{},"gshop-api：",[242,823,131],{}," 沒有 route，回非 200",[180,826,827,828,830,831],{},"gshop-dashboard：",[242,829,131],{}," 302 redirect 到 ",[242,832,833],{},"\u002Flogin",[180,835,836,837,839],{},"gshop-web：",[242,838,131],{}," 正常回 200（不需要處理）",[180,841,842,844,845,848,849,864,865],{},[183,843,617],{},"：建 ",[242,846,847],{},"BackendConfig"," 指定正確的健康檢查路徑：\n",[235,850,852],{"className":708,"code":851,"language":710,"meta":240,"style":240},"# gshop-api → \u002Fhealth\n# gshop-dashboard → \u002Flogin\n",[242,853,854,859],{"__ignoreMap":240},[245,855,856],{"class":247,"line":248},[245,857,858],{"class":251},"# gshop-api → \u002Fhealth\n",[245,860,861],{"class":247,"line":255},[245,862,863],{"class":251},"# gshop-dashboard → \u002Flogin\n","\n並在 Service 加 annotation：\n",[235,866,868],{"className":708,"code":867,"language":710,"meta":240,"style":240},"cloud.google.com\u002Fbackend-config: '{\"default\": \"gshop-api-backend-config\"}'\n",[242,869,870],{"__ignoreMap":240},[245,871,872,875,877,880,883],{"class":247,"line":248},[245,873,874],{"class":717},"cloud.google.com\u002Fbackend-config",[245,876,736],{"class":392},[245,878,879],{"class":392}," '",[245,881,882],{"class":262},"{\"default\": \"gshop-api-backend-config\"}",[245,884,885],{"class":392},"'\n",[230,887,889],{"id":888},"_5-supabase-連線失敗enotfound","5. Supabase 連線失敗（ENOTFOUND）",[177,891,892,901],{},[180,893,894,896,897,900],{},[183,895,611],{},"：Supabase 直連（",[242,898,899],{},"db.xxx.supabase.co:5432","）是 IPv6 only，GKE 是 IPv4 only",[180,902,903,905,906,909,910,918,919,1021],{},[183,904,617],{},"：改用 Supabase ",[183,907,908],{},"Session Pooler"," connection string：\n",[235,911,916],{"className":912,"code":914,"language":915},[913],"language-text","postgresql:\u002F\u002Fpostgres.vqjzzlutlovqoqshwbza:PASSWORD@aws-1-ap-southeast-1.pooler.supabase.com:5432\u002Fpostgres\n","text",[242,917,914],{"__ignoreMap":240},"\n更新 K8s secret：\n",[235,920,922],{"className":237,"code":921,"language":239,"meta":240,"style":240},"NEW_URL=\"postgresql:\u002F\u002F...\"\nkubectl patch secret gshop-api-secret -p \"{\\\"data\\\":{\\\"DATABASE_URL\\\":\\\"$(echo -n $NEW_URL | base64)\\\"}}\"\nkubectl rollout restart deployment\u002Fgshop-api\n",[242,923,924,939,1010],{"__ignoreMap":240},[245,925,926,929,932,934,937],{"class":247,"line":248},[245,927,928],{"class":272},"NEW_URL",[245,930,931],{"class":392},"=",[245,933,393],{"class":392},[245,935,936],{"class":262},"postgresql:\u002F\u002F...",[245,938,438],{"class":392},[245,940,941,943,946,948,950,953,956,959,962,965,967,970,972,975,977,979,981,984,988,991,994,997,1000,1003,1005,1008],{"class":247,"line":255},[245,942,370],{"class":258},[245,944,945],{"class":262}," patch",[245,947,376],{"class":262},[245,949,382],{"class":262},[245,951,952],{"class":262}," -p",[245,954,955],{"class":392}," \"",[245,957,958],{"class":262},"{",[245,960,961],{"class":272},"\\\"",[245,963,964],{"class":262},"data",[245,966,961],{"class":272},[245,968,969],{"class":262},":{",[245,971,961],{"class":272},[245,973,974],{"class":262},"DATABASE_URL",[245,976,961],{"class":272},[245,978,736],{"class":262},[245,980,961],{"class":272},[245,982,983],{"class":392},"$(",[245,985,987],{"class":986},"s2Zo4","echo",[245,989,990],{"class":262}," -n ",[245,992,993],{"class":272},"$NEW_URL",[245,995,996],{"class":392}," |",[245,998,999],{"class":258}," base64",[245,1001,1002],{"class":392},")",[245,1004,961],{"class":272},[245,1006,1007],{"class":262},"}}",[245,1009,438],{"class":392},[245,1011,1012,1014,1016,1018],{"class":247,"line":276},[245,1013,370],{"class":258},[245,1015,589],{"class":262},[245,1017,592],{"class":262},[245,1019,1020],{"class":262}," deployment\u002Fgshop-api\n",[351,1022,1023],{},[354,1024,1025],{},"本地開發不需要改，Mac 支援 IPv6 直連沒問題",[224,1027],{},[173,1029,1031],{"id":1030},"dns-https-設定","DNS & HTTPS 設定",[230,1033,1035],{"id":1034},"cloudflare-a-records","Cloudflare A Records",[1037,1038,1039,1052],"table",{},[1040,1041,1042],"thead",{},[1043,1044,1045,1049],"tr",{},[1046,1047,1048],"th",{},"子網域",[1046,1050,1051],{},"IP",[1053,1054,1055,1064,1071],"tbody",{},[1043,1056,1057,1061],{},[1058,1059,1060],"td",{},"api.garydemo.com",[1058,1062,1063],{},"34.160.168.110",[1043,1065,1066,1069],{},[1058,1067,1068],{},"dashboard.garydemo.com",[1058,1070,1063],{},[1043,1072,1073,1076],{},[1058,1074,1075],{},"web.garydemo.com",[1058,1077,1063],{},[351,1079,1080],{},[354,1081,1082],{},"三個都指向同一個 Ingress IP，由 Ingress 依 Host header 分流",[230,1084,1086],{"id":1085},"cloudflare-ssltls-模式","Cloudflare SSL\u002FTLS 模式",[177,1088,1089,1095],{},[180,1090,1091,1092],{},"設為 ",[183,1093,1094],{},"Full (Strict)",[180,1096,1097],{},"使用 Cloudflare Origin Certificate 存為 K8s TLS Secret",[230,1099,1100],{"id":1100},"流量路徑",[235,1102,1105],{"className":1103,"code":1104,"language":915},[913],"瀏覽器 → Cloudflare（Proxy + TLS）→ GCP Load Balancer → Ingress → Pod\n",[242,1106,1104],{"__ignoreMap":240},[224,1108],{},[173,1110,1112],{"id":1111},"cicdgithub-actions","CI\u002FCD（GitHub Actions）",[354,1114,1115],{},"每個 service 各自有獨立 repo，push 到 main 自動 build + deploy。",[235,1117,1120],{"className":1118,"code":1119,"language":915},[913],"push main → GitHub Actions → build image → push Artifact Registry → kubectl rollout restart\n",[242,1121,1119],{"__ignoreMap":240},[230,1123,1125],{"id":1124},"前置設定一次性","前置設定（一次性）",[235,1127,1129],{"className":237,"code":1128,"language":239,"meta":240,"style":240},"gcloud iam service-accounts create github-actions \\\n  --display-name=\"GitHub Actions\"\n\ngcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:github-actions@gshop-497319.iam.gserviceaccount.com\" \\\n  --role=\"roles\u002Fartifactregistry.writer\"\n\ngcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:github-actions@gshop-497319.iam.gserviceaccount.com\" \\\n  --role=\"roles\u002Fcontainer.developer\"\n\ngcloud iam service-accounts keys create sa-key.json \\\n  --iam-account=github-actions@gshop-497319.iam.gserviceaccount.com\n",[242,1130,1131,1148,1160,1164,1176,1189,1200,1204,1216,1228,1239,1243,1262],{"__ignoreMap":240},[245,1132,1133,1135,1138,1141,1143,1146],{"class":247,"line":248},[245,1134,259],{"class":258},[245,1136,1137],{"class":262}," iam",[245,1139,1140],{"class":262}," service-accounts",[245,1142,373],{"class":262},[245,1144,1145],{"class":262}," github-actions",[245,1147,273],{"class":272},[245,1149,1150,1153,1155,1158],{"class":247,"line":255},[245,1151,1152],{"class":262},"  --display-name=",[245,1154,393],{"class":392},[245,1156,1157],{"class":262},"GitHub Actions",[245,1159,438],{"class":392},[245,1161,1162],{"class":247,"line":276},[245,1163,288],{"emptyLinePlaceholder":130},[245,1165,1166,1168,1170,1172,1174],{"class":247,"line":285},[245,1167,259],{"class":258},[245,1169,650],{"class":262},[245,1171,653],{"class":262},[245,1173,656],{"class":262},[245,1175,273],{"class":272},[245,1177,1178,1180,1182,1185,1187],{"class":247,"line":291},[245,1179,663],{"class":262},[245,1181,393],{"class":392},[245,1183,1184],{"class":262},"serviceAccount:github-actions@gshop-497319.iam.gserviceaccount.com",[245,1186,393],{"class":392},[245,1188,273],{"class":272},[245,1190,1191,1193,1195,1198],{"class":247,"line":297},[245,1192,677],{"class":262},[245,1194,393],{"class":392},[245,1196,1197],{"class":262},"roles\u002Fartifactregistry.writer",[245,1199,438],{"class":392},[245,1201,1202],{"class":247,"line":311},[245,1203,288],{"emptyLinePlaceholder":130},[245,1205,1206,1208,1210,1212,1214],{"class":247,"line":319},[245,1207,259],{"class":258},[245,1209,650],{"class":262},[245,1211,653],{"class":262},[245,1213,656],{"class":262},[245,1215,273],{"class":272},[245,1217,1218,1220,1222,1224,1226],{"class":247,"line":324},[245,1219,663],{"class":262},[245,1221,393],{"class":392},[245,1223,1184],{"class":262},[245,1225,393],{"class":392},[245,1227,273],{"class":272},[245,1229,1230,1232,1234,1237],{"class":247,"line":330},[245,1231,677],{"class":262},[245,1233,393],{"class":392},[245,1235,1236],{"class":262},"roles\u002Fcontainer.developer",[245,1238,438],{"class":392},[245,1240,1241],{"class":247,"line":344},[245,1242,288],{"emptyLinePlaceholder":130},[245,1244,1246,1248,1250,1252,1255,1257,1260],{"class":247,"line":1245},12,[245,1247,259],{"class":258},[245,1249,1137],{"class":262},[245,1251,1140],{"class":262},[245,1253,1254],{"class":262}," keys",[245,1256,373],{"class":262},[245,1258,1259],{"class":262}," sa-key.json",[245,1261,273],{"class":272},[245,1263,1265],{"class":247,"line":1264},13,[245,1266,1267],{"class":262},"  --iam-account=github-actions@gshop-497319.iam.gserviceaccount.com\n",[354,1269,1270,1271,1274,1275,1278],{},"GitHub org → Settings → Secrets → New organization secret，名稱 ",[242,1272,1273],{},"GCP_SA_KEY","，貼入 ",[242,1276,1277],{},"sa-key.json"," 內容。",[230,1280,1282],{"id":1281},"workflow-範例","Workflow 範例",[235,1284,1286],{"className":708,"code":1285,"language":710,"meta":240,"style":240},"name: Deploy gshop-api\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: google-github-actions\u002Fauth@v2\n        with:\n          credentials_json: ${{ secrets.GCP_SA_KEY }}\n      - uses: google-github-actions\u002Fsetup-gcloud@v2\n      - run: gcloud auth configure-docker asia-east1-docker.pkg.dev\n      - name: Build and push image\n        run: |\n          docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest .\n          docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n      - uses: google-github-actions\u002Fget-gke-credentials@v2\n        with:\n          cluster_name: gshop-cluster\n          location: asia-east1\n      - run: kubectl rollout restart deployment\u002Fgshop-api\n",[242,1287,1288,1298,1302,1310,1317,1324,1332,1336,1343,1350,1360,1367,1379,1390,1398,1409,1421,1434,1447,1459,1465,1471,1483,1490,1501,1512],{"__ignoreMap":240},[245,1289,1290,1293,1295],{"class":247,"line":248},[245,1291,1292],{"class":717},"name",[245,1294,736],{"class":392},[245,1296,1297],{"class":262}," Deploy gshop-api\n",[245,1299,1300],{"class":247,"line":255},[245,1301,288],{"emptyLinePlaceholder":130},[245,1303,1304,1308],{"class":247,"line":276},[245,1305,1307],{"class":1306},"sfNiH","on",[245,1309,721],{"class":392},[245,1311,1312,1315],{"class":247,"line":285},[245,1313,1314],{"class":717},"  push",[245,1316,721],{"class":392},[245,1318,1319,1322],{"class":247,"line":291},[245,1320,1321],{"class":717},"    branches",[245,1323,721],{"class":392},[245,1325,1326,1329],{"class":247,"line":297},[245,1327,1328],{"class":392},"      -",[245,1330,1331],{"class":262}," main\n",[245,1333,1334],{"class":247,"line":311},[245,1335,288],{"emptyLinePlaceholder":130},[245,1337,1338,1341],{"class":247,"line":319},[245,1339,1340],{"class":717},"jobs",[245,1342,721],{"class":392},[245,1344,1345,1348],{"class":247,"line":324},[245,1346,1347],{"class":717},"  deploy",[245,1349,721],{"class":392},[245,1351,1352,1355,1357],{"class":247,"line":330},[245,1353,1354],{"class":717},"    runs-on",[245,1356,736],{"class":392},[245,1358,1359],{"class":262}," ubuntu-latest\n",[245,1361,1362,1365],{"class":247,"line":344},[245,1363,1364],{"class":717},"    steps",[245,1366,721],{"class":392},[245,1368,1369,1371,1374,1376],{"class":247,"line":1245},[245,1370,1328],{"class":392},[245,1372,1373],{"class":717}," uses",[245,1375,736],{"class":392},[245,1377,1378],{"class":262}," actions\u002Fcheckout@v4\n",[245,1380,1381,1383,1385,1387],{"class":247,"line":1264},[245,1382,1328],{"class":392},[245,1384,1373],{"class":717},[245,1386,736],{"class":392},[245,1388,1389],{"class":262}," google-github-actions\u002Fauth@v2\n",[245,1391,1393,1396],{"class":247,"line":1392},14,[245,1394,1395],{"class":717},"        with",[245,1397,721],{"class":392},[245,1399,1401,1404,1406],{"class":247,"line":1400},15,[245,1402,1403],{"class":717},"          credentials_json",[245,1405,736],{"class":392},[245,1407,1408],{"class":262}," ${{ secrets.GCP_SA_KEY }}\n",[245,1410,1412,1414,1416,1418],{"class":247,"line":1411},16,[245,1413,1328],{"class":392},[245,1415,1373],{"class":717},[245,1417,736],{"class":392},[245,1419,1420],{"class":262}," google-github-actions\u002Fsetup-gcloud@v2\n",[245,1422,1424,1426,1429,1431],{"class":247,"line":1423},17,[245,1425,1328],{"class":392},[245,1427,1428],{"class":717}," run",[245,1430,736],{"class":392},[245,1432,1433],{"class":262}," gcloud auth configure-docker asia-east1-docker.pkg.dev\n",[245,1435,1437,1439,1442,1444],{"class":247,"line":1436},18,[245,1438,1328],{"class":392},[245,1440,1441],{"class":717}," name",[245,1443,736],{"class":392},[245,1445,1446],{"class":262}," Build and push image\n",[245,1448,1450,1453,1455],{"class":247,"line":1449},19,[245,1451,1452],{"class":717},"        run",[245,1454,736],{"class":392},[245,1456,1458],{"class":1457},"s7zQu"," |\n",[245,1460,1462],{"class":247,"line":1461},20,[245,1463,1464],{"class":262},"          docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest .\n",[245,1466,1468],{"class":247,"line":1467},21,[245,1469,1470],{"class":262},"          docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n",[245,1472,1474,1476,1478,1480],{"class":247,"line":1473},22,[245,1475,1328],{"class":392},[245,1477,1373],{"class":717},[245,1479,736],{"class":392},[245,1481,1482],{"class":262}," google-github-actions\u002Fget-gke-credentials@v2\n",[245,1484,1486,1488],{"class":247,"line":1485},23,[245,1487,1395],{"class":717},[245,1489,721],{"class":392},[245,1491,1493,1496,1498],{"class":247,"line":1492},24,[245,1494,1495],{"class":717},"          cluster_name",[245,1497,736],{"class":392},[245,1499,1500],{"class":262}," gshop-cluster\n",[245,1502,1504,1507,1509],{"class":247,"line":1503},25,[245,1505,1506],{"class":717},"          location",[245,1508,736],{"class":392},[245,1510,1511],{"class":262}," asia-east1\n",[245,1513,1515,1517,1519,1521],{"class":247,"line":1514},26,[245,1516,1328],{"class":392},[245,1518,1428],{"class":717},[245,1520,736],{"class":392},[245,1522,1523],{"class":262}," kubectl rollout restart deployment\u002Fgshop-api\n",[351,1525,1526],{},[354,1527,1528],{},"GitHub Actions runner 是 ubuntu amd64，build 出的 image 天生就是 amd64，不需要 Cloud Build",[224,1530],{},[173,1532,1533],{"id":1533},"常用指令",[235,1535,1537],{"className":237,"code":1536,"language":239,"meta":240,"style":240},"# 查 pod 狀態\nkubectl get pods\n\n# 查 ingress IP\nkubectl get ingress gshop-ingress\n\n# 查某服務 log\nkubectl logs -l app=gshop-api --tail=50\n\n# 查 backend 健康狀態\ngcloud compute backend-services get-health \u003Cbackend-name> --global\n\n# 列出所有 backend\ngcloud compute backend-services list --global\n\n# 查 NEG\ngcloud compute network-endpoint-groups list\n",[242,1538,1539,1544,1554,1558,1563,1575,1579,1584,1600,1604,1609,1637,1641,1646,1659,1663,1668],{"__ignoreMap":240},[245,1540,1541],{"class":247,"line":248},[245,1542,1543],{"class":251},"# 查 pod 狀態\n",[245,1545,1546,1548,1551],{"class":247,"line":255},[245,1547,370],{"class":258},[245,1549,1550],{"class":262}," get",[245,1552,1553],{"class":262}," pods\n",[245,1555,1556],{"class":247,"line":276},[245,1557,288],{"emptyLinePlaceholder":130},[245,1559,1560],{"class":247,"line":285},[245,1561,1562],{"class":251},"# 查 ingress IP\n",[245,1564,1565,1567,1569,1572],{"class":247,"line":291},[245,1566,370],{"class":258},[245,1568,1550],{"class":262},[245,1570,1571],{"class":262}," ingress",[245,1573,1574],{"class":262}," gshop-ingress\n",[245,1576,1577],{"class":247,"line":297},[245,1578,288],{"emptyLinePlaceholder":130},[245,1580,1581],{"class":247,"line":311},[245,1582,1583],{"class":251},"# 查某服務 log\n",[245,1585,1586,1588,1591,1594,1597],{"class":247,"line":319},[245,1587,370],{"class":258},[245,1589,1590],{"class":262}," logs",[245,1592,1593],{"class":262}," -l",[245,1595,1596],{"class":262}," app=gshop-api",[245,1598,1599],{"class":262}," --tail=50\n",[245,1601,1602],{"class":247,"line":324},[245,1603,288],{"emptyLinePlaceholder":130},[245,1605,1606],{"class":247,"line":330},[245,1607,1608],{"class":251},"# 查 backend 健康狀態\n",[245,1610,1611,1613,1616,1619,1622,1625,1628,1631,1634],{"class":247,"line":344},[245,1612,259],{"class":258},[245,1614,1615],{"class":262}," compute",[245,1617,1618],{"class":262}," backend-services",[245,1620,1621],{"class":262}," get-health",[245,1623,1624],{"class":392}," \u003C",[245,1626,1627],{"class":262},"backend-nam",[245,1629,1630],{"class":272},"e",[245,1632,1633],{"class":392},">",[245,1635,1636],{"class":262}," --global\n",[245,1638,1639],{"class":247,"line":1245},[245,1640,288],{"emptyLinePlaceholder":130},[245,1642,1643],{"class":247,"line":1264},[245,1644,1645],{"class":251},"# 列出所有 backend\n",[245,1647,1648,1650,1652,1654,1657],{"class":247,"line":1392},[245,1649,259],{"class":258},[245,1651,1615],{"class":262},[245,1653,1618],{"class":262},[245,1655,1656],{"class":262}," list",[245,1658,1636],{"class":262},[245,1660,1661],{"class":247,"line":1400},[245,1662,288],{"emptyLinePlaceholder":130},[245,1664,1665],{"class":247,"line":1411},[245,1666,1667],{"class":251},"# 查 NEG\n",[245,1669,1670,1672,1674,1677],{"class":247,"line":1423},[245,1671,259],{"class":258},[245,1673,1615],{"class":262},[245,1675,1676],{"class":262}," network-endpoint-groups",[245,1678,1679],{"class":262}," list\n",[1681,1682,1683],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":240,"searchDepth":255,"depth":255,"links":1685},[1686,1687,1694,1701,1706,1710],{"id":175,"depth":255,"text":175},{"id":228,"depth":255,"text":228,"children":1688},[1689,1690,1691,1692,1693],{"id":232,"depth":276,"text":233},{"id":359,"depth":276,"text":360},{"id":441,"depth":276,"text":442},{"id":478,"depth":276,"text":479},{"id":544,"depth":276,"text":545},{"id":600,"depth":255,"text":600,"children":1695},[1696,1697,1698,1699,1700],{"id":603,"depth":276,"text":604},{"id":625,"depth":276,"text":626},{"id":687,"depth":276,"text":688},{"id":804,"depth":276,"text":805},{"id":888,"depth":276,"text":889},{"id":1030,"depth":255,"text":1031,"children":1702},[1703,1704,1705],{"id":1034,"depth":276,"text":1035},{"id":1085,"depth":276,"text":1086},{"id":1100,"depth":276,"text":1100},{"id":1111,"depth":255,"text":1112,"children":1707},[1708,1709],{"id":1124,"depth":276,"text":1125},{"id":1281,"depth":276,"text":1282},{"id":1533,"depth":255,"text":1533},"2026-06-16","記錄將個人電商（GShop）部署到 GKE Autopilot 的完整流程。","md","\u002Fimages\u002Fgke.jpg",{},{"title":14,"description":1712},"nwSAGWy6EGbowdd_9ERLH4cormr59zF4uBKDtY7Wdco",{"id":1719,"title":10,"author":1720,"body":1722,"date":4010,"description":4011,"extension":1713,"externalUrl":43,"image":4012,"meta":4013,"minRead":291,"navigation":130,"path":11,"seo":4014,"stem":12,"__hash__":4015},"blog\u002Farticles\u002Fdaily-snapshot.md",{"name":166,"avatar":1721},{"src":168,"alt":166},{"type":170,"value":1723,"toc":3995},[1724,1727,1730,1750,1764,1767,1769,1773,1779,1785,1787,1791,1794,1970,1972,1976,1979,3442,3445,3482,3484,3488,3613,3615,3618,3621,3692,3694,3698,3701,3706,3712,3715,3719,3722,3868,3874,3878,3895,3898,3902,3905,3907,3914,3916,3919,3969,3971,3974,3981,3992],[173,1725,1726],{"id":1726},"問題背景",[354,1728,1729],{},"訂單管理後台有一個總覽頁面，需要顯示以下統計數字：",[177,1731,1732,1735,1738,1741,1744,1747],{},[180,1733,1734],{},"當日營業額與訂單數",[180,1736,1737],{},"新增用戶數",[180,1739,1740],{},"平均客單價",[180,1742,1743],{},"熱銷商品 Top 10",[180,1745,1746],{},"各類別銷售佔比",[180,1748,1749],{},"各付款方式分佈",[354,1751,1752,1753,1756,1757,1756,1760,1763],{},"起初資料量小，直接對 ",[242,1754,1755],{},"orders","、",[242,1758,1759],{},"order_items",[242,1761,1762],{},"payments"," 做聚合查詢沒有問題。",[354,1765,1766],{},"隨著訂單累積，這些查詢開始拖慢整個頁面，每次載入需要數秒，而且每個進入後台的管理員都會觸發一次跨表掃描。",[224,1768],{},[173,1770,1772],{"id":1771},"解法daily-snapshot","解法：Daily Snapshot",[354,1774,1775,1776],{},"核心思路：",[183,1777,1778],{},"不在用戶請求時計算，改成每天固定時間預先算好，存進一張 Snapshot Table，查詢時直接讀一筆記錄。",[235,1780,1783],{"className":1781,"code":1782,"language":915},[913],"每天 00:00 Cron Job 執行\n      ↓\n讀取昨日訂單、商品、付款資料，執行聚合計算\n      ↓\n將結果寫入 daily_snapshots 表（一天一筆）\n      ↓\n前端請求總覽時，直接 SELECT 最新一筆 snapshot\n",[242,1784,1782],{"__ignoreMap":240},[224,1786],{},[173,1788,1790],{"id":1789},"snapshot-table-設計","Snapshot Table 設計",[354,1792,1793],{},"Snapshot 除了基本的金額與訂單數，還用 JSON 欄位儲存熱銷商品、類別、付款方式等排行資料：",[235,1795,1799],{"className":1796,"code":1797,"language":1798,"meta":240,"style":240},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F Sequelize Model\nDailySnapshot = {\n  date: DataTypes.DATEONLY, \u002F\u002F 唯一鍵，一天一筆\n  revenue: DataTypes.DECIMAL, \u002F\u002F 當日營業額（paid\u002Fshipped\u002Fdelivered）\n  orderCount: DataTypes.INTEGER, \u002F\u002F 當日總訂單數\n  newUserCount: DataTypes.INTEGER, \u002F\u002F 當日新增用戶數\n  avgOrderValue: DataTypes.DECIMAL,\n  topProducts: DataTypes.JSON, \u002F\u002F [{ productId, productName, totalRevenue, totalQuantity }]\n  topCategories: DataTypes.JSON, \u002F\u002F [{ categoryId, categoryName, totalRevenue, totalQuantity }]\n  paymentMethods: DataTypes.JSON, \u002F\u002F [{ method, count, amount }]\n};\n","js",[242,1800,1801,1806,1816,1838,1857,1876,1894,1910,1929,1947,1965],{"__ignoreMap":240},[245,1802,1803],{"class":247,"line":248},[245,1804,1805],{"class":251},"\u002F\u002F Sequelize Model\n",[245,1807,1808,1811,1813],{"class":247,"line":255},[245,1809,1810],{"class":272},"DailySnapshot ",[245,1812,931],{"class":392},[245,1814,1815],{"class":392}," {\n",[245,1817,1818,1821,1823,1826,1829,1832,1835],{"class":247,"line":276},[245,1819,1820],{"class":717},"  date",[245,1822,736],{"class":392},[245,1824,1825],{"class":272}," DataTypes",[245,1827,1828],{"class":392},".",[245,1830,1831],{"class":272},"DATEONLY",[245,1833,1834],{"class":392},",",[245,1836,1837],{"class":251}," \u002F\u002F 唯一鍵，一天一筆\n",[245,1839,1840,1843,1845,1847,1849,1852,1854],{"class":247,"line":285},[245,1841,1842],{"class":717},"  revenue",[245,1844,736],{"class":392},[245,1846,1825],{"class":272},[245,1848,1828],{"class":392},[245,1850,1851],{"class":272},"DECIMAL",[245,1853,1834],{"class":392},[245,1855,1856],{"class":251}," \u002F\u002F 當日營業額（paid\u002Fshipped\u002Fdelivered）\n",[245,1858,1859,1862,1864,1866,1868,1871,1873],{"class":247,"line":291},[245,1860,1861],{"class":717},"  orderCount",[245,1863,736],{"class":392},[245,1865,1825],{"class":272},[245,1867,1828],{"class":392},[245,1869,1870],{"class":272},"INTEGER",[245,1872,1834],{"class":392},[245,1874,1875],{"class":251}," \u002F\u002F 當日總訂單數\n",[245,1877,1878,1881,1883,1885,1887,1889,1891],{"class":247,"line":297},[245,1879,1880],{"class":717},"  newUserCount",[245,1882,736],{"class":392},[245,1884,1825],{"class":272},[245,1886,1828],{"class":392},[245,1888,1870],{"class":272},[245,1890,1834],{"class":392},[245,1892,1893],{"class":251}," \u002F\u002F 當日新增用戶數\n",[245,1895,1896,1899,1901,1903,1905,1907],{"class":247,"line":311},[245,1897,1898],{"class":717},"  avgOrderValue",[245,1900,736],{"class":392},[245,1902,1825],{"class":272},[245,1904,1828],{"class":392},[245,1906,1851],{"class":272},[245,1908,1909],{"class":392},",\n",[245,1911,1912,1915,1917,1919,1921,1924,1926],{"class":247,"line":319},[245,1913,1914],{"class":717},"  topProducts",[245,1916,736],{"class":392},[245,1918,1825],{"class":272},[245,1920,1828],{"class":392},[245,1922,1923],{"class":272},"JSON",[245,1925,1834],{"class":392},[245,1927,1928],{"class":251}," \u002F\u002F [{ productId, productName, totalRevenue, totalQuantity }]\n",[245,1930,1931,1934,1936,1938,1940,1942,1944],{"class":247,"line":324},[245,1932,1933],{"class":717},"  topCategories",[245,1935,736],{"class":392},[245,1937,1825],{"class":272},[245,1939,1828],{"class":392},[245,1941,1923],{"class":272},[245,1943,1834],{"class":392},[245,1945,1946],{"class":251}," \u002F\u002F [{ categoryId, categoryName, totalRevenue, totalQuantity }]\n",[245,1948,1949,1952,1954,1956,1958,1960,1962],{"class":247,"line":330},[245,1950,1951],{"class":717},"  paymentMethods",[245,1953,736],{"class":392},[245,1955,1825],{"class":272},[245,1957,1828],{"class":392},[245,1959,1923],{"class":272},[245,1961,1834],{"class":392},[245,1963,1964],{"class":251}," \u002F\u002F [{ method, count, amount }]\n",[245,1966,1967],{"class":247,"line":344},[245,1968,1969],{"class":392},"};\n",[224,1971],{},[173,1973,1975],{"id":1974},"builddailysnapshot-實作","buildDailySnapshot 實作",[354,1977,1978],{},"以下是實際的計算函式，對四張表同時發出查詢後一次 upsert 進 snapshot table：",[235,1980,1982],{"className":1796,"code":1981,"language":1798,"meta":240,"style":240},"import sequelize from \"..\u002Fconfig\u002Fdb.js\";\nimport { DailySnapshot } from \"..\u002Fmodels\u002Findex.js\";\n\nexport async function buildDailySnapshot(targetDate) {\n  \u002F\u002F 預設計算昨天\n  const date =\n    targetDate ||\n    (() => {\n      const d = new Date();\n      d.setDate(d.getDate() - 1);\n      return d;\n    })();\n\n  const dateStr = toLocalDateStr(date);\n  const start = new Date(date);\n  start.setHours(0, 0, 0, 0);\n  const end = new Date(start);\n  end.setDate(end.getDate() + 1);\n\n  \u002F\u002F 四支查詢並行執行，減少等待時間\n  const [[revenue], [userRow], topProducts, topCategories, paymentMethods] =\n    await Promise.all([\n      \u002F\u002F 營業額、訂單數\n      sequelize.query(\n        `\n        SELECT\n          COALESCE(SUM(total_amount) FILTER (WHERE status IN ('paid','shipped','delivered')), 0) AS revenue,\n          COUNT(*) FILTER (WHERE status IN ('paid','shipped','delivered')) AS paid_count,\n          COUNT(*) AS order_count\n        FROM orders WHERE created_at >= :start AND created_at \u003C :end\n      `,\n        { replacements: { start, end }, type: sequelize.QueryTypes.SELECT },\n      ),\n\n      \u002F\u002F 新增用戶數\n      sequelize.query(\n        `\n        SELECT COUNT(*) AS count FROM users\n        WHERE created_at >= :start AND created_at \u003C :end\n      `,\n        { replacements: { start, end }, type: sequelize.QueryTypes.SELECT },\n      ),\n\n      \u002F\u002F 熱銷商品 Top 10\n      sequelize.query(\n        `\n        SELECT oi.product_id AS \"productId\", oi.product_name AS \"productName\",\n               SUM(oi.subtotal) AS \"totalRevenue\", SUM(oi.quantity) AS \"totalQuantity\"\n        FROM order_items oi JOIN orders o ON o.id = oi.order_id\n        WHERE o.created_at >= :start AND o.created_at \u003C :end AND o.status != 'cancelled'\n        GROUP BY oi.product_id, oi.product_name\n        ORDER BY \"totalRevenue\" DESC LIMIT 10\n      `,\n        { replacements: { start, end }, type: sequelize.QueryTypes.SELECT },\n      ),\n\n      \u002F\u002F 各類別銷售 Top 10\n      sequelize.query(\n        `\n        SELECT p.category_id AS \"categoryId\", c.name AS \"categoryName\",\n               SUM(oi.subtotal) AS \"totalRevenue\", SUM(oi.quantity) AS \"totalQuantity\"\n        FROM order_items oi\n        JOIN orders o ON o.id = oi.order_id\n        JOIN products p ON p.id = oi.product_id\n        JOIN categories c ON c.id = p.category_id\n        WHERE o.created_at >= :start AND o.created_at \u003C :end AND o.status != 'cancelled'\n        GROUP BY p.category_id, c.name\n        ORDER BY \"totalRevenue\" DESC LIMIT 10\n      `,\n        { replacements: { start, end }, type: sequelize.QueryTypes.SELECT },\n      ),\n\n      \u002F\u002F 付款方式分佈\n      sequelize.query(\n        `\n        SELECT pay.method, COUNT(*) AS count, SUM(pay.amount) AS amount\n        FROM payments pay JOIN orders o ON o.id = pay.order_id\n        WHERE o.created_at >= :start AND o.created_at \u003C :end\n          AND o.status IN ('paid','shipped','delivered')\n        GROUP BY pay.method\n      `,\n        { replacements: { start, end }, type: sequelize.QueryTypes.SELECT },\n      ),\n    ]);\n\n  const rev = parseFloat(revenue.revenue) || 0;\n  const paidCount = parseInt(revenue.paid_count) || 0;\n\n  \u002F\u002F upsert：重跑不會產生重複資料\n  await DailySnapshot.upsert({\n    date: dateStr,\n    revenue: rev,\n    orderCount: parseInt(revenue.order_count) || 0,\n    newUserCount: parseInt(userRow.count) || 0,\n    avgOrderValue: paidCount > 0 ? parseFloat((rev \u002F paidCount).toFixed(2)) : 0,\n    topProducts: topProducts.map((r) => ({\n      ...r,\n      totalRevenue: parseFloat(r.totalRevenue),\n      totalQuantity: parseInt(r.totalQuantity),\n    })),\n    topCategories: topCategories.map((r) => ({\n      ...r,\n      totalRevenue: parseFloat(r.totalRevenue),\n      totalQuantity: parseInt(r.totalQuantity),\n    })),\n    paymentMethods: paymentMethods.map((r) => ({\n      method: r.method,\n      count: parseInt(r.count),\n      amount: parseFloat(r.amount),\n    })),\n  });\n}\n",[242,1983,1984,2005,2030,2034,2060,2065,2076,2084,2097,2118,2152,2161,2171,2175,2196,2217,2249,2271,2300,2304,2309,2348,2364,2369,2382,2387,2392,2398,2404,2410,2416,2424,2467,2475,2480,2486,2497,2502,2508,2514,2521,2556,2563,2568,2574,2585,2590,2596,2602,2608,2614,2620,2626,2633,2668,2675,2680,2686,2697,2702,2708,2713,2719,2725,2731,2737,2742,2748,2753,2760,2795,2802,2807,2813,2824,2829,2835,2841,2847,2853,2859,2866,2901,2908,2916,2921,2952,2982,2987,2993,3011,3023,3035,3062,3089,3141,3172,3182,3205,3228,3238,3266,3275,3296,3317,3326,3354,3372,3394,3417,3426,3436],{"__ignoreMap":240},[245,1985,1986,1989,1992,1995,1997,2000,2002],{"class":247,"line":248},[245,1987,1988],{"class":1457},"import",[245,1990,1991],{"class":272}," sequelize ",[245,1993,1994],{"class":1457},"from",[245,1996,955],{"class":392},[245,1998,1999],{"class":262},"..\u002Fconfig\u002Fdb.js",[245,2001,393],{"class":392},[245,2003,2004],{"class":392},";\n",[245,2006,2007,2009,2012,2015,2018,2021,2023,2026,2028],{"class":247,"line":255},[245,2008,1988],{"class":1457},[245,2010,2011],{"class":392}," {",[245,2013,2014],{"class":272}," DailySnapshot",[245,2016,2017],{"class":392}," }",[245,2019,2020],{"class":1457}," from",[245,2022,955],{"class":392},[245,2024,2025],{"class":262},"..\u002Fmodels\u002Findex.js",[245,2027,393],{"class":392},[245,2029,2004],{"class":392},[245,2031,2032],{"class":247,"line":276},[245,2033,288],{"emptyLinePlaceholder":130},[245,2035,2036,2039,2043,2046,2049,2052,2056,2058],{"class":247,"line":285},[245,2037,2038],{"class":1457},"export",[245,2040,2042],{"class":2041},"spNyl"," async",[245,2044,2045],{"class":2041}," function",[245,2047,2048],{"class":986}," buildDailySnapshot",[245,2050,2051],{"class":392},"(",[245,2053,2055],{"class":2054},"sHdIc","targetDate",[245,2057,1002],{"class":392},[245,2059,1815],{"class":392},[245,2061,2062],{"class":247,"line":291},[245,2063,2064],{"class":251},"  \u002F\u002F 預設計算昨天\n",[245,2066,2067,2070,2073],{"class":247,"line":297},[245,2068,2069],{"class":2041},"  const",[245,2071,2072],{"class":272}," date",[245,2074,2075],{"class":392}," =\n",[245,2077,2078,2081],{"class":247,"line":311},[245,2079,2080],{"class":272},"    targetDate",[245,2082,2083],{"class":392}," ||\n",[245,2085,2086,2089,2092,2095],{"class":247,"line":319},[245,2087,2088],{"class":717},"    (",[245,2090,2091],{"class":392},"()",[245,2093,2094],{"class":2041}," =>",[245,2096,1815],{"class":392},[245,2098,2099,2102,2105,2108,2111,2114,2116],{"class":247,"line":324},[245,2100,2101],{"class":2041},"      const",[245,2103,2104],{"class":272}," d",[245,2106,2107],{"class":392}," =",[245,2109,2110],{"class":392}," new",[245,2112,2113],{"class":986}," Date",[245,2115,2091],{"class":717},[245,2117,2004],{"class":392},[245,2119,2120,2123,2125,2128,2130,2133,2135,2138,2141,2144,2148,2150],{"class":247,"line":330},[245,2121,2122],{"class":272},"      d",[245,2124,1828],{"class":392},[245,2126,2127],{"class":986},"setDate",[245,2129,2051],{"class":717},[245,2131,2132],{"class":272},"d",[245,2134,1828],{"class":392},[245,2136,2137],{"class":986},"getDate",[245,2139,2140],{"class":717},"() ",[245,2142,2143],{"class":392},"-",[245,2145,2147],{"class":2146},"sbssI"," 1",[245,2149,1002],{"class":717},[245,2151,2004],{"class":392},[245,2153,2154,2157,2159],{"class":247,"line":344},[245,2155,2156],{"class":1457},"      return",[245,2158,2104],{"class":272},[245,2160,2004],{"class":392},[245,2162,2163,2166,2169],{"class":247,"line":1245},[245,2164,2165],{"class":392},"    }",[245,2167,2168],{"class":717},")()",[245,2170,2004],{"class":392},[245,2172,2173],{"class":247,"line":1264},[245,2174,288],{"emptyLinePlaceholder":130},[245,2176,2177,2179,2182,2184,2187,2189,2192,2194],{"class":247,"line":1392},[245,2178,2069],{"class":2041},[245,2180,2181],{"class":272}," dateStr",[245,2183,2107],{"class":392},[245,2185,2186],{"class":986}," toLocalDateStr",[245,2188,2051],{"class":717},[245,2190,2191],{"class":272},"date",[245,2193,1002],{"class":717},[245,2195,2004],{"class":392},[245,2197,2198,2200,2203,2205,2207,2209,2211,2213,2215],{"class":247,"line":1400},[245,2199,2069],{"class":2041},[245,2201,2202],{"class":272}," start",[245,2204,2107],{"class":392},[245,2206,2110],{"class":392},[245,2208,2113],{"class":986},[245,2210,2051],{"class":717},[245,2212,2191],{"class":272},[245,2214,1002],{"class":717},[245,2216,2004],{"class":392},[245,2218,2219,2222,2224,2227,2229,2232,2234,2237,2239,2241,2243,2245,2247],{"class":247,"line":1411},[245,2220,2221],{"class":272},"  start",[245,2223,1828],{"class":392},[245,2225,2226],{"class":986},"setHours",[245,2228,2051],{"class":717},[245,2230,2231],{"class":2146},"0",[245,2233,1834],{"class":392},[245,2235,2236],{"class":2146}," 0",[245,2238,1834],{"class":392},[245,2240,2236],{"class":2146},[245,2242,1834],{"class":392},[245,2244,2236],{"class":2146},[245,2246,1002],{"class":717},[245,2248,2004],{"class":392},[245,2250,2251,2253,2256,2258,2260,2262,2264,2267,2269],{"class":247,"line":1423},[245,2252,2069],{"class":2041},[245,2254,2255],{"class":272}," end",[245,2257,2107],{"class":392},[245,2259,2110],{"class":392},[245,2261,2113],{"class":986},[245,2263,2051],{"class":717},[245,2265,2266],{"class":272},"start",[245,2268,1002],{"class":717},[245,2270,2004],{"class":392},[245,2272,2273,2276,2278,2280,2282,2285,2287,2289,2291,2294,2296,2298],{"class":247,"line":1436},[245,2274,2275],{"class":272},"  end",[245,2277,1828],{"class":392},[245,2279,2127],{"class":986},[245,2281,2051],{"class":717},[245,2283,2284],{"class":272},"end",[245,2286,1828],{"class":392},[245,2288,2137],{"class":986},[245,2290,2140],{"class":717},[245,2292,2293],{"class":392},"+",[245,2295,2147],{"class":2146},[245,2297,1002],{"class":717},[245,2299,2004],{"class":392},[245,2301,2302],{"class":247,"line":1449},[245,2303,288],{"emptyLinePlaceholder":130},[245,2305,2306],{"class":247,"line":1461},[245,2307,2308],{"class":251},"  \u002F\u002F 四支查詢並行執行，減少等待時間\n",[245,2310,2311,2313,2316,2319,2322,2325,2328,2330,2333,2335,2338,2340,2343,2346],{"class":247,"line":1467},[245,2312,2069],{"class":2041},[245,2314,2315],{"class":392}," [[",[245,2317,2318],{"class":272},"revenue",[245,2320,2321],{"class":392},"],",[245,2323,2324],{"class":392}," [",[245,2326,2327],{"class":272},"userRow",[245,2329,2321],{"class":392},[245,2331,2332],{"class":272}," topProducts",[245,2334,1834],{"class":392},[245,2336,2337],{"class":272}," topCategories",[245,2339,1834],{"class":392},[245,2341,2342],{"class":272}," paymentMethods",[245,2344,2345],{"class":392},"]",[245,2347,2075],{"class":392},[245,2349,2350,2353,2356,2358,2361],{"class":247,"line":1473},[245,2351,2352],{"class":1457},"    await",[245,2354,2355],{"class":258}," Promise",[245,2357,1828],{"class":392},[245,2359,2360],{"class":986},"all",[245,2362,2363],{"class":717},"([\n",[245,2365,2366],{"class":247,"line":1485},[245,2367,2368],{"class":251},"      \u002F\u002F 營業額、訂單數\n",[245,2370,2371,2374,2376,2379],{"class":247,"line":1492},[245,2372,2373],{"class":272},"      sequelize",[245,2375,1828],{"class":392},[245,2377,2378],{"class":986},"query",[245,2380,2381],{"class":717},"(\n",[245,2383,2384],{"class":247,"line":1503},[245,2385,2386],{"class":392},"        `\n",[245,2388,2389],{"class":247,"line":1514},[245,2390,2391],{"class":262},"        SELECT\n",[245,2393,2395],{"class":247,"line":2394},27,[245,2396,2397],{"class":262},"          COALESCE(SUM(total_amount) FILTER (WHERE status IN ('paid','shipped','delivered')), 0) AS revenue,\n",[245,2399,2401],{"class":247,"line":2400},28,[245,2402,2403],{"class":262},"          COUNT(*) FILTER (WHERE status IN ('paid','shipped','delivered')) AS paid_count,\n",[245,2405,2407],{"class":247,"line":2406},29,[245,2408,2409],{"class":262},"          COUNT(*) AS order_count\n",[245,2411,2413],{"class":247,"line":2412},30,[245,2414,2415],{"class":262},"        FROM orders WHERE created_at >= :start AND created_at \u003C :end\n",[245,2417,2419,2422],{"class":247,"line":2418},31,[245,2420,2421],{"class":392},"      `",[245,2423,1909],{"class":392},[245,2425,2427,2430,2433,2435,2437,2439,2441,2443,2446,2449,2451,2454,2456,2459,2461,2464],{"class":247,"line":2426},32,[245,2428,2429],{"class":392},"        {",[245,2431,2432],{"class":717}," replacements",[245,2434,736],{"class":392},[245,2436,2011],{"class":392},[245,2438,2202],{"class":272},[245,2440,1834],{"class":392},[245,2442,2255],{"class":272},[245,2444,2445],{"class":392}," },",[245,2447,2448],{"class":717}," type",[245,2450,736],{"class":392},[245,2452,2453],{"class":272}," sequelize",[245,2455,1828],{"class":392},[245,2457,2458],{"class":272},"QueryTypes",[245,2460,1828],{"class":392},[245,2462,2463],{"class":272},"SELECT",[245,2465,2466],{"class":392}," },\n",[245,2468,2470,2473],{"class":247,"line":2469},33,[245,2471,2472],{"class":717},"      )",[245,2474,1909],{"class":392},[245,2476,2478],{"class":247,"line":2477},34,[245,2479,288],{"emptyLinePlaceholder":130},[245,2481,2483],{"class":247,"line":2482},35,[245,2484,2485],{"class":251},"      \u002F\u002F 新增用戶數\n",[245,2487,2489,2491,2493,2495],{"class":247,"line":2488},36,[245,2490,2373],{"class":272},[245,2492,1828],{"class":392},[245,2494,2378],{"class":986},[245,2496,2381],{"class":717},[245,2498,2500],{"class":247,"line":2499},37,[245,2501,2386],{"class":392},[245,2503,2505],{"class":247,"line":2504},38,[245,2506,2507],{"class":262},"        SELECT COUNT(*) AS count FROM users\n",[245,2509,2511],{"class":247,"line":2510},39,[245,2512,2513],{"class":262},"        WHERE created_at >= :start AND created_at \u003C :end\n",[245,2515,2517,2519],{"class":247,"line":2516},40,[245,2518,2421],{"class":392},[245,2520,1909],{"class":392},[245,2522,2524,2526,2528,2530,2532,2534,2536,2538,2540,2542,2544,2546,2548,2550,2552,2554],{"class":247,"line":2523},41,[245,2525,2429],{"class":392},[245,2527,2432],{"class":717},[245,2529,736],{"class":392},[245,2531,2011],{"class":392},[245,2533,2202],{"class":272},[245,2535,1834],{"class":392},[245,2537,2255],{"class":272},[245,2539,2445],{"class":392},[245,2541,2448],{"class":717},[245,2543,736],{"class":392},[245,2545,2453],{"class":272},[245,2547,1828],{"class":392},[245,2549,2458],{"class":272},[245,2551,1828],{"class":392},[245,2553,2463],{"class":272},[245,2555,2466],{"class":392},[245,2557,2559,2561],{"class":247,"line":2558},42,[245,2560,2472],{"class":717},[245,2562,1909],{"class":392},[245,2564,2566],{"class":247,"line":2565},43,[245,2567,288],{"emptyLinePlaceholder":130},[245,2569,2571],{"class":247,"line":2570},44,[245,2572,2573],{"class":251},"      \u002F\u002F 熱銷商品 Top 10\n",[245,2575,2577,2579,2581,2583],{"class":247,"line":2576},45,[245,2578,2373],{"class":272},[245,2580,1828],{"class":392},[245,2582,2378],{"class":986},[245,2584,2381],{"class":717},[245,2586,2588],{"class":247,"line":2587},46,[245,2589,2386],{"class":392},[245,2591,2593],{"class":247,"line":2592},47,[245,2594,2595],{"class":262},"        SELECT oi.product_id AS \"productId\", oi.product_name AS \"productName\",\n",[245,2597,2599],{"class":247,"line":2598},48,[245,2600,2601],{"class":262},"               SUM(oi.subtotal) AS \"totalRevenue\", SUM(oi.quantity) AS \"totalQuantity\"\n",[245,2603,2605],{"class":247,"line":2604},49,[245,2606,2607],{"class":262},"        FROM order_items oi JOIN orders o ON o.id = oi.order_id\n",[245,2609,2611],{"class":247,"line":2610},50,[245,2612,2613],{"class":262},"        WHERE o.created_at >= :start AND o.created_at \u003C :end AND o.status != 'cancelled'\n",[245,2615,2617],{"class":247,"line":2616},51,[245,2618,2619],{"class":262},"        GROUP BY oi.product_id, oi.product_name\n",[245,2621,2623],{"class":247,"line":2622},52,[245,2624,2625],{"class":262},"        ORDER BY \"totalRevenue\" DESC LIMIT 10\n",[245,2627,2629,2631],{"class":247,"line":2628},53,[245,2630,2421],{"class":392},[245,2632,1909],{"class":392},[245,2634,2636,2638,2640,2642,2644,2646,2648,2650,2652,2654,2656,2658,2660,2662,2664,2666],{"class":247,"line":2635},54,[245,2637,2429],{"class":392},[245,2639,2432],{"class":717},[245,2641,736],{"class":392},[245,2643,2011],{"class":392},[245,2645,2202],{"class":272},[245,2647,1834],{"class":392},[245,2649,2255],{"class":272},[245,2651,2445],{"class":392},[245,2653,2448],{"class":717},[245,2655,736],{"class":392},[245,2657,2453],{"class":272},[245,2659,1828],{"class":392},[245,2661,2458],{"class":272},[245,2663,1828],{"class":392},[245,2665,2463],{"class":272},[245,2667,2466],{"class":392},[245,2669,2671,2673],{"class":247,"line":2670},55,[245,2672,2472],{"class":717},[245,2674,1909],{"class":392},[245,2676,2678],{"class":247,"line":2677},56,[245,2679,288],{"emptyLinePlaceholder":130},[245,2681,2683],{"class":247,"line":2682},57,[245,2684,2685],{"class":251},"      \u002F\u002F 各類別銷售 Top 10\n",[245,2687,2689,2691,2693,2695],{"class":247,"line":2688},58,[245,2690,2373],{"class":272},[245,2692,1828],{"class":392},[245,2694,2378],{"class":986},[245,2696,2381],{"class":717},[245,2698,2700],{"class":247,"line":2699},59,[245,2701,2386],{"class":392},[245,2703,2705],{"class":247,"line":2704},60,[245,2706,2707],{"class":262},"        SELECT p.category_id AS \"categoryId\", c.name AS \"categoryName\",\n",[245,2709,2711],{"class":247,"line":2710},61,[245,2712,2601],{"class":262},[245,2714,2716],{"class":247,"line":2715},62,[245,2717,2718],{"class":262},"        FROM order_items oi\n",[245,2720,2722],{"class":247,"line":2721},63,[245,2723,2724],{"class":262},"        JOIN orders o ON o.id = oi.order_id\n",[245,2726,2728],{"class":247,"line":2727},64,[245,2729,2730],{"class":262},"        JOIN products p ON p.id = oi.product_id\n",[245,2732,2734],{"class":247,"line":2733},65,[245,2735,2736],{"class":262},"        JOIN categories c ON c.id = p.category_id\n",[245,2738,2740],{"class":247,"line":2739},66,[245,2741,2613],{"class":262},[245,2743,2745],{"class":247,"line":2744},67,[245,2746,2747],{"class":262},"        GROUP BY p.category_id, c.name\n",[245,2749,2751],{"class":247,"line":2750},68,[245,2752,2625],{"class":262},[245,2754,2756,2758],{"class":247,"line":2755},69,[245,2757,2421],{"class":392},[245,2759,1909],{"class":392},[245,2761,2763,2765,2767,2769,2771,2773,2775,2777,2779,2781,2783,2785,2787,2789,2791,2793],{"class":247,"line":2762},70,[245,2764,2429],{"class":392},[245,2766,2432],{"class":717},[245,2768,736],{"class":392},[245,2770,2011],{"class":392},[245,2772,2202],{"class":272},[245,2774,1834],{"class":392},[245,2776,2255],{"class":272},[245,2778,2445],{"class":392},[245,2780,2448],{"class":717},[245,2782,736],{"class":392},[245,2784,2453],{"class":272},[245,2786,1828],{"class":392},[245,2788,2458],{"class":272},[245,2790,1828],{"class":392},[245,2792,2463],{"class":272},[245,2794,2466],{"class":392},[245,2796,2798,2800],{"class":247,"line":2797},71,[245,2799,2472],{"class":717},[245,2801,1909],{"class":392},[245,2803,2805],{"class":247,"line":2804},72,[245,2806,288],{"emptyLinePlaceholder":130},[245,2808,2810],{"class":247,"line":2809},73,[245,2811,2812],{"class":251},"      \u002F\u002F 付款方式分佈\n",[245,2814,2816,2818,2820,2822],{"class":247,"line":2815},74,[245,2817,2373],{"class":272},[245,2819,1828],{"class":392},[245,2821,2378],{"class":986},[245,2823,2381],{"class":717},[245,2825,2827],{"class":247,"line":2826},75,[245,2828,2386],{"class":392},[245,2830,2832],{"class":247,"line":2831},76,[245,2833,2834],{"class":262},"        SELECT pay.method, COUNT(*) AS count, SUM(pay.amount) AS amount\n",[245,2836,2838],{"class":247,"line":2837},77,[245,2839,2840],{"class":262},"        FROM payments pay JOIN orders o ON o.id = pay.order_id\n",[245,2842,2844],{"class":247,"line":2843},78,[245,2845,2846],{"class":262},"        WHERE o.created_at >= :start AND o.created_at \u003C :end\n",[245,2848,2850],{"class":247,"line":2849},79,[245,2851,2852],{"class":262},"          AND o.status IN ('paid','shipped','delivered')\n",[245,2854,2856],{"class":247,"line":2855},80,[245,2857,2858],{"class":262},"        GROUP BY pay.method\n",[245,2860,2862,2864],{"class":247,"line":2861},81,[245,2863,2421],{"class":392},[245,2865,1909],{"class":392},[245,2867,2869,2871,2873,2875,2877,2879,2881,2883,2885,2887,2889,2891,2893,2895,2897,2899],{"class":247,"line":2868},82,[245,2870,2429],{"class":392},[245,2872,2432],{"class":717},[245,2874,736],{"class":392},[245,2876,2011],{"class":392},[245,2878,2202],{"class":272},[245,2880,1834],{"class":392},[245,2882,2255],{"class":272},[245,2884,2445],{"class":392},[245,2886,2448],{"class":717},[245,2888,736],{"class":392},[245,2890,2453],{"class":272},[245,2892,1828],{"class":392},[245,2894,2458],{"class":272},[245,2896,1828],{"class":392},[245,2898,2463],{"class":272},[245,2900,2466],{"class":392},[245,2902,2904,2906],{"class":247,"line":2903},83,[245,2905,2472],{"class":717},[245,2907,1909],{"class":392},[245,2909,2911,2914],{"class":247,"line":2910},84,[245,2912,2913],{"class":717},"    ])",[245,2915,2004],{"class":392},[245,2917,2919],{"class":247,"line":2918},85,[245,2920,288],{"emptyLinePlaceholder":130},[245,2922,2924,2926,2929,2931,2934,2936,2938,2940,2942,2945,2948,2950],{"class":247,"line":2923},86,[245,2925,2069],{"class":2041},[245,2927,2928],{"class":272}," rev",[245,2930,2107],{"class":392},[245,2932,2933],{"class":986}," parseFloat",[245,2935,2051],{"class":717},[245,2937,2318],{"class":272},[245,2939,1828],{"class":392},[245,2941,2318],{"class":272},[245,2943,2944],{"class":717},") ",[245,2946,2947],{"class":392},"||",[245,2949,2236],{"class":2146},[245,2951,2004],{"class":392},[245,2953,2955,2957,2960,2962,2965,2967,2969,2971,2974,2976,2978,2980],{"class":247,"line":2954},87,[245,2956,2069],{"class":2041},[245,2958,2959],{"class":272}," paidCount",[245,2961,2107],{"class":392},[245,2963,2964],{"class":986}," parseInt",[245,2966,2051],{"class":717},[245,2968,2318],{"class":272},[245,2970,1828],{"class":392},[245,2972,2973],{"class":272},"paid_count",[245,2975,2944],{"class":717},[245,2977,2947],{"class":392},[245,2979,2236],{"class":2146},[245,2981,2004],{"class":392},[245,2983,2985],{"class":247,"line":2984},88,[245,2986,288],{"emptyLinePlaceholder":130},[245,2988,2990],{"class":247,"line":2989},89,[245,2991,2992],{"class":251},"  \u002F\u002F upsert：重跑不會產生重複資料\n",[245,2994,2996,2999,3001,3003,3006,3008],{"class":247,"line":2995},90,[245,2997,2998],{"class":1457},"  await",[245,3000,2014],{"class":272},[245,3002,1828],{"class":392},[245,3004,3005],{"class":986},"upsert",[245,3007,2051],{"class":717},[245,3009,3010],{"class":392},"{\n",[245,3012,3014,3017,3019,3021],{"class":247,"line":3013},91,[245,3015,3016],{"class":717},"    date",[245,3018,736],{"class":392},[245,3020,2181],{"class":272},[245,3022,1909],{"class":392},[245,3024,3026,3029,3031,3033],{"class":247,"line":3025},92,[245,3027,3028],{"class":717},"    revenue",[245,3030,736],{"class":392},[245,3032,2928],{"class":272},[245,3034,1909],{"class":392},[245,3036,3038,3041,3043,3045,3047,3049,3051,3054,3056,3058,3060],{"class":247,"line":3037},93,[245,3039,3040],{"class":717},"    orderCount",[245,3042,736],{"class":392},[245,3044,2964],{"class":986},[245,3046,2051],{"class":717},[245,3048,2318],{"class":272},[245,3050,1828],{"class":392},[245,3052,3053],{"class":272},"order_count",[245,3055,2944],{"class":717},[245,3057,2947],{"class":392},[245,3059,2236],{"class":2146},[245,3061,1909],{"class":392},[245,3063,3065,3068,3070,3072,3074,3076,3078,3081,3083,3085,3087],{"class":247,"line":3064},94,[245,3066,3067],{"class":717},"    newUserCount",[245,3069,736],{"class":392},[245,3071,2964],{"class":986},[245,3073,2051],{"class":717},[245,3075,2327],{"class":272},[245,3077,1828],{"class":392},[245,3079,3080],{"class":272},"count",[245,3082,2944],{"class":717},[245,3084,2947],{"class":392},[245,3086,2236],{"class":2146},[245,3088,1909],{"class":392},[245,3090,3092,3095,3097,3099,3102,3104,3107,3109,3112,3115,3118,3120,3122,3124,3127,3129,3132,3135,3137,3139],{"class":247,"line":3091},95,[245,3093,3094],{"class":717},"    avgOrderValue",[245,3096,736],{"class":392},[245,3098,2959],{"class":272},[245,3100,3101],{"class":392}," >",[245,3103,2236],{"class":2146},[245,3105,3106],{"class":392}," ?",[245,3108,2933],{"class":986},[245,3110,3111],{"class":717},"((",[245,3113,3114],{"class":272},"rev",[245,3116,3117],{"class":392}," \u002F",[245,3119,2959],{"class":272},[245,3121,1002],{"class":717},[245,3123,1828],{"class":392},[245,3125,3126],{"class":986},"toFixed",[245,3128,2051],{"class":717},[245,3130,3131],{"class":2146},"2",[245,3133,3134],{"class":717},")) ",[245,3136,736],{"class":392},[245,3138,2236],{"class":2146},[245,3140,1909],{"class":392},[245,3142,3144,3147,3149,3151,3153,3156,3158,3160,3163,3165,3167,3170],{"class":247,"line":3143},96,[245,3145,3146],{"class":717},"    topProducts",[245,3148,736],{"class":392},[245,3150,2332],{"class":272},[245,3152,1828],{"class":392},[245,3154,3155],{"class":986},"map",[245,3157,2051],{"class":717},[245,3159,2051],{"class":392},[245,3161,3162],{"class":2054},"r",[245,3164,1002],{"class":392},[245,3166,2094],{"class":2041},[245,3168,3169],{"class":717}," (",[245,3171,3010],{"class":392},[245,3173,3175,3178,3180],{"class":247,"line":3174},97,[245,3176,3177],{"class":392},"      ...",[245,3179,3162],{"class":272},[245,3181,1909],{"class":392},[245,3183,3185,3188,3190,3192,3194,3196,3198,3201,3203],{"class":247,"line":3184},98,[245,3186,3187],{"class":717},"      totalRevenue",[245,3189,736],{"class":392},[245,3191,2933],{"class":986},[245,3193,2051],{"class":717},[245,3195,3162],{"class":272},[245,3197,1828],{"class":392},[245,3199,3200],{"class":272},"totalRevenue",[245,3202,1002],{"class":717},[245,3204,1909],{"class":392},[245,3206,3208,3211,3213,3215,3217,3219,3221,3224,3226],{"class":247,"line":3207},99,[245,3209,3210],{"class":717},"      totalQuantity",[245,3212,736],{"class":392},[245,3214,2964],{"class":986},[245,3216,2051],{"class":717},[245,3218,3162],{"class":272},[245,3220,1828],{"class":392},[245,3222,3223],{"class":272},"totalQuantity",[245,3225,1002],{"class":717},[245,3227,1909],{"class":392},[245,3229,3231,3233,3236],{"class":247,"line":3230},100,[245,3232,2165],{"class":392},[245,3234,3235],{"class":717},"))",[245,3237,1909],{"class":392},[245,3239,3241,3244,3246,3248,3250,3252,3254,3256,3258,3260,3262,3264],{"class":247,"line":3240},101,[245,3242,3243],{"class":717},"    topCategories",[245,3245,736],{"class":392},[245,3247,2337],{"class":272},[245,3249,1828],{"class":392},[245,3251,3155],{"class":986},[245,3253,2051],{"class":717},[245,3255,2051],{"class":392},[245,3257,3162],{"class":2054},[245,3259,1002],{"class":392},[245,3261,2094],{"class":2041},[245,3263,3169],{"class":717},[245,3265,3010],{"class":392},[245,3267,3269,3271,3273],{"class":247,"line":3268},102,[245,3270,3177],{"class":392},[245,3272,3162],{"class":272},[245,3274,1909],{"class":392},[245,3276,3278,3280,3282,3284,3286,3288,3290,3292,3294],{"class":247,"line":3277},103,[245,3279,3187],{"class":717},[245,3281,736],{"class":392},[245,3283,2933],{"class":986},[245,3285,2051],{"class":717},[245,3287,3162],{"class":272},[245,3289,1828],{"class":392},[245,3291,3200],{"class":272},[245,3293,1002],{"class":717},[245,3295,1909],{"class":392},[245,3297,3299,3301,3303,3305,3307,3309,3311,3313,3315],{"class":247,"line":3298},104,[245,3300,3210],{"class":717},[245,3302,736],{"class":392},[245,3304,2964],{"class":986},[245,3306,2051],{"class":717},[245,3308,3162],{"class":272},[245,3310,1828],{"class":392},[245,3312,3223],{"class":272},[245,3314,1002],{"class":717},[245,3316,1909],{"class":392},[245,3318,3320,3322,3324],{"class":247,"line":3319},105,[245,3321,2165],{"class":392},[245,3323,3235],{"class":717},[245,3325,1909],{"class":392},[245,3327,3329,3332,3334,3336,3338,3340,3342,3344,3346,3348,3350,3352],{"class":247,"line":3328},106,[245,3330,3331],{"class":717},"    paymentMethods",[245,3333,736],{"class":392},[245,3335,2342],{"class":272},[245,3337,1828],{"class":392},[245,3339,3155],{"class":986},[245,3341,2051],{"class":717},[245,3343,2051],{"class":392},[245,3345,3162],{"class":2054},[245,3347,1002],{"class":392},[245,3349,2094],{"class":2041},[245,3351,3169],{"class":717},[245,3353,3010],{"class":392},[245,3355,3357,3360,3362,3365,3367,3370],{"class":247,"line":3356},107,[245,3358,3359],{"class":717},"      method",[245,3361,736],{"class":392},[245,3363,3364],{"class":272}," r",[245,3366,1828],{"class":392},[245,3368,3369],{"class":272},"method",[245,3371,1909],{"class":392},[245,3373,3375,3378,3380,3382,3384,3386,3388,3390,3392],{"class":247,"line":3374},108,[245,3376,3377],{"class":717},"      count",[245,3379,736],{"class":392},[245,3381,2964],{"class":986},[245,3383,2051],{"class":717},[245,3385,3162],{"class":272},[245,3387,1828],{"class":392},[245,3389,3080],{"class":272},[245,3391,1002],{"class":717},[245,3393,1909],{"class":392},[245,3395,3397,3400,3402,3404,3406,3408,3410,3413,3415],{"class":247,"line":3396},109,[245,3398,3399],{"class":717},"      amount",[245,3401,736],{"class":392},[245,3403,2933],{"class":986},[245,3405,2051],{"class":717},[245,3407,3162],{"class":272},[245,3409,1828],{"class":392},[245,3411,3412],{"class":272},"amount",[245,3414,1002],{"class":717},[245,3416,1909],{"class":392},[245,3418,3420,3422,3424],{"class":247,"line":3419},110,[245,3421,2165],{"class":392},[245,3423,3235],{"class":717},[245,3425,1909],{"class":392},[245,3427,3429,3432,3434],{"class":247,"line":3428},111,[245,3430,3431],{"class":392},"  }",[245,3433,1002],{"class":717},[245,3435,2004],{"class":392},[245,3437,3439],{"class":247,"line":3438},112,[245,3440,3441],{"class":392},"}\n",[354,3443,3444],{},"幾個值得注意的設計細節：",[177,3446,3447,3455,3463,3470],{},[180,3448,3449,3454],{},[183,3450,3451],{},[242,3452,3453],{},"Promise.all","：四支查詢並行發出，不等前一支結束才跑下一支",[180,3456,3457,3462],{},[183,3458,3459],{},[242,3460,3461],{},"FILTER (WHERE status IN (...))","：只統計有效訂單的營業額，排除取消訂單",[180,3464,3465,3469],{},[183,3466,3467],{},[242,3468,3005],{},"：Cron Job 重跑（例如補跑失敗的日期）時不會產生重複記錄",[180,3471,3472,3477,3478,3481],{},[183,3473,3474],{},[242,3475,3476],{},"toLocalDateStr","：手動格式化本地日期，避免 ",[242,3479,3480],{},"toISOString()"," 因時區偏移導致日期錯誤",[224,3483],{},[173,3485,3487],{"id":3486},"cron-job-排程","Cron Job 排程",[235,3489,3491],{"className":1796,"code":3490,"language":1798,"meta":240,"style":240},"import cron from \"node-cron\";\nimport { buildDailySnapshot } from \".\u002Fjobs\u002FdailySnapshot.js\";\n\n\u002F\u002F 每天凌晨 00:05 執行（留 5 分鐘緩衝確保跨日資料落庫）\ncron.schedule(\"5 0 * * *\", async () => {\n  await buildDailySnapshot();\n  console.log(\"[Snapshot] 昨日 snapshot 建立完成\");\n});\n",[242,3492,3493,3511,3532,3536,3541,3571,3581,3604],{"__ignoreMap":240},[245,3494,3495,3497,3500,3502,3504,3507,3509],{"class":247,"line":248},[245,3496,1988],{"class":1457},[245,3498,3499],{"class":272}," cron ",[245,3501,1994],{"class":1457},[245,3503,955],{"class":392},[245,3505,3506],{"class":262},"node-cron",[245,3508,393],{"class":392},[245,3510,2004],{"class":392},[245,3512,3513,3515,3517,3519,3521,3523,3525,3528,3530],{"class":247,"line":255},[245,3514,1988],{"class":1457},[245,3516,2011],{"class":392},[245,3518,2048],{"class":272},[245,3520,2017],{"class":392},[245,3522,2020],{"class":1457},[245,3524,955],{"class":392},[245,3526,3527],{"class":262},".\u002Fjobs\u002FdailySnapshot.js",[245,3529,393],{"class":392},[245,3531,2004],{"class":392},[245,3533,3534],{"class":247,"line":276},[245,3535,288],{"emptyLinePlaceholder":130},[245,3537,3538],{"class":247,"line":285},[245,3539,3540],{"class":251},"\u002F\u002F 每天凌晨 00:05 執行（留 5 分鐘緩衝確保跨日資料落庫）\n",[245,3542,3543,3546,3548,3551,3553,3555,3558,3560,3562,3564,3567,3569],{"class":247,"line":291},[245,3544,3545],{"class":272},"cron",[245,3547,1828],{"class":392},[245,3549,3550],{"class":986},"schedule",[245,3552,2051],{"class":272},[245,3554,393],{"class":392},[245,3556,3557],{"class":262},"5 0 * * *",[245,3559,393],{"class":392},[245,3561,1834],{"class":392},[245,3563,2042],{"class":2041},[245,3565,3566],{"class":392}," ()",[245,3568,2094],{"class":2041},[245,3570,1815],{"class":392},[245,3572,3573,3575,3577,3579],{"class":247,"line":297},[245,3574,2998],{"class":1457},[245,3576,2048],{"class":986},[245,3578,2091],{"class":717},[245,3580,2004],{"class":392},[245,3582,3583,3586,3588,3591,3593,3595,3598,3600,3602],{"class":247,"line":311},[245,3584,3585],{"class":272},"  console",[245,3587,1828],{"class":392},[245,3589,3590],{"class":986},"log",[245,3592,2051],{"class":717},[245,3594,393],{"class":392},[245,3596,3597],{"class":262},"[Snapshot] 昨日 snapshot 建立完成",[245,3599,393],{"class":392},[245,3601,1002],{"class":717},[245,3603,2004],{"class":392},[245,3605,3606,3609,3611],{"class":247,"line":319},[245,3607,3608],{"class":392},"}",[245,3610,1002],{"class":272},[245,3612,2004],{"class":392},[224,3614],{},[173,3616,3617],{"id":3617},"查詢方式",[354,3619,3620],{},"總覽 API 改成直接讀 snapshot，不再碰原始資料表：",[235,3622,3624],{"className":1796,"code":3623,"language":1798,"meta":240,"style":240},"\u002F\u002F ✅ 讀最新一筆 snapshot，毫秒級回應\nconst snapshot = await DailySnapshot.findOne({\n  order: [[\"date\", \"DESC\"]],\n});\n",[242,3625,3626,3631,3655,3684],{"__ignoreMap":240},[245,3627,3628],{"class":247,"line":248},[245,3629,3630],{"class":251},"\u002F\u002F ✅ 讀最新一筆 snapshot，毫秒級回應\n",[245,3632,3633,3636,3639,3641,3644,3646,3648,3651,3653],{"class":247,"line":255},[245,3634,3635],{"class":2041},"const",[245,3637,3638],{"class":272}," snapshot ",[245,3640,931],{"class":392},[245,3642,3643],{"class":1457}," await",[245,3645,2014],{"class":272},[245,3647,1828],{"class":392},[245,3649,3650],{"class":986},"findOne",[245,3652,2051],{"class":272},[245,3654,3010],{"class":392},[245,3656,3657,3660,3662,3664,3666,3668,3670,3672,3674,3677,3679,3682],{"class":247,"line":276},[245,3658,3659],{"class":717},"  order",[245,3661,736],{"class":392},[245,3663,2315],{"class":272},[245,3665,393],{"class":392},[245,3667,2191],{"class":262},[245,3669,393],{"class":392},[245,3671,1834],{"class":392},[245,3673,955],{"class":392},[245,3675,3676],{"class":262},"DESC",[245,3678,393],{"class":392},[245,3680,3681],{"class":272},"]]",[245,3683,1909],{"class":392},[245,3685,3686,3688,3690],{"class":247,"line":285},[245,3687,3608],{"class":392},[245,3689,1002],{"class":272},[245,3691,2004],{"class":392},[224,3693],{},[173,3695,3697],{"id":3696},"訂單狀態會變動怎麼辦","訂單狀態會變動怎麼辦？",[354,3699,3700],{},"Snapshot 是某個時間點的快照，但訂單狀態會在那之後繼續變動，這是使用這個模式必須正視的問題。",[354,3702,3703],{},[183,3704,3705],{},"舉個例子：",[235,3707,3710],{"className":3708,"code":3709,"language":915},[913],"23:50  訂單建立，狀態 pending\n00:05  Cron Job 跑完 snapshot，這筆訂單未被計入營業額\n09:00  用戶付款，狀態變 paid\n",[242,3711,3709],{"__ignoreMap":240},[354,3713,3714],{},"昨天的 snapshot 永遠不會包含這筆訂單，數字就是錯的。",[230,3716,3718],{"id":3717},"解法一補跑近幾天的-snapshot","解法一：補跑近幾天的 Snapshot",[354,3720,3721],{},"每天除了算昨天，也重算過去 N 天，讓狀態更新能被追上：",[235,3723,3725],{"className":1796,"code":3724,"language":1798,"meta":240,"style":240},"\u002F\u002F 每天重算最近 7 天\ncron.schedule(\"5 0 * * *\", async () => {\n  for (let i = 1; i \u003C= 7; i++) {\n    const d = new Date();\n    d.setDate(d.getDate() - i);\n    await buildDailySnapshot(d);\n  }\n});\n",[242,3726,3727,3732,3758,3797,3814,3841,3855,3860],{"__ignoreMap":240},[245,3728,3729],{"class":247,"line":248},[245,3730,3731],{"class":251},"\u002F\u002F 每天重算最近 7 天\n",[245,3733,3734,3736,3738,3740,3742,3744,3746,3748,3750,3752,3754,3756],{"class":247,"line":255},[245,3735,3545],{"class":272},[245,3737,1828],{"class":392},[245,3739,3550],{"class":986},[245,3741,2051],{"class":272},[245,3743,393],{"class":392},[245,3745,3557],{"class":262},[245,3747,393],{"class":392},[245,3749,1834],{"class":392},[245,3751,2042],{"class":2041},[245,3753,3566],{"class":392},[245,3755,2094],{"class":2041},[245,3757,1815],{"class":392},[245,3759,3760,3763,3765,3768,3771,3773,3775,3778,3780,3783,3786,3788,3790,3793,3795],{"class":247,"line":276},[245,3761,3762],{"class":1457},"  for",[245,3764,3169],{"class":717},[245,3766,3767],{"class":2041},"let",[245,3769,3770],{"class":272}," i",[245,3772,2107],{"class":392},[245,3774,2147],{"class":2146},[245,3776,3777],{"class":392},";",[245,3779,3770],{"class":272},[245,3781,3782],{"class":392}," \u003C=",[245,3784,3785],{"class":2146}," 7",[245,3787,3777],{"class":392},[245,3789,3770],{"class":272},[245,3791,3792],{"class":392},"++",[245,3794,2944],{"class":717},[245,3796,3010],{"class":392},[245,3798,3799,3802,3804,3806,3808,3810,3812],{"class":247,"line":285},[245,3800,3801],{"class":2041},"    const",[245,3803,2104],{"class":272},[245,3805,2107],{"class":392},[245,3807,2110],{"class":392},[245,3809,2113],{"class":986},[245,3811,2091],{"class":717},[245,3813,2004],{"class":392},[245,3815,3816,3819,3821,3823,3825,3827,3829,3831,3833,3835,3837,3839],{"class":247,"line":291},[245,3817,3818],{"class":272},"    d",[245,3820,1828],{"class":392},[245,3822,2127],{"class":986},[245,3824,2051],{"class":717},[245,3826,2132],{"class":272},[245,3828,1828],{"class":392},[245,3830,2137],{"class":986},[245,3832,2140],{"class":717},[245,3834,2143],{"class":392},[245,3836,3770],{"class":272},[245,3838,1002],{"class":717},[245,3840,2004],{"class":392},[245,3842,3843,3845,3847,3849,3851,3853],{"class":247,"line":297},[245,3844,2352],{"class":1457},[245,3846,2048],{"class":986},[245,3848,2051],{"class":717},[245,3850,2132],{"class":272},[245,3852,1002],{"class":717},[245,3854,2004],{"class":392},[245,3856,3857],{"class":247,"line":311},[245,3858,3859],{"class":392},"  }\n",[245,3861,3862,3864,3866],{"class":247,"line":319},[245,3863,3608],{"class":392},[245,3865,1002],{"class":272},[245,3867,2004],{"class":392},[354,3869,3870,3871,3873],{},"適合狀態變動集中在近期（例如大多數訂單在 3 天內完成付款）的情境。",[242,3872,3005],{}," 的設計讓補跑變得安全，不會產生重複資料。",[230,3875,3877],{"id":3876},"解法二只快照終態訂單","解法二：只快照終態訂單",[354,3879,3880,3881,1756,3884,3887,3888,1756,3891,3894],{},"只把 ",[242,3882,3883],{},"delivered",[242,3885,3886],{},"cancelled"," 這類不會再變動的訂單算進 snapshot，",[242,3889,3890],{},"paid",[242,3892,3893],{},"shipped"," 等還在流動中的訂單留給即時查詢。",[354,3896,3897],{},"代價是 snapshot 數字會比實際交易日期滯後幾天，但每一筆都是確定的終態數字。",[230,3899,3901],{"id":3900},"解法三接受誤差","解法三：接受誤差",[354,3903,3904],{},"如果這個總覽是給內部看的管理報表，T+1 有些許誤差通常可以接受。業務決策不需要精確到每一筆，數字的趨勢比精確值更重要。",[224,3906],{},[354,3908,3909,3910,3913],{},"目前採用的是",[183,3911,3912],{},"解法一","，每天重算最近 7 天，在資料準確性與實作複雜度之間取得平衡。",[224,3915],{},[173,3917,3918],{"id":3918},"效果對比",[1037,3920,3921,3934],{},[1040,3922,3923],{},[1043,3924,3925,3928,3931],{},[1046,3926,3927],{},"指標",[1046,3929,3930],{},"改善前",[1046,3932,3933],{},"改善後",[1053,3935,3936,3947,3958],{},[1043,3937,3938,3941,3944],{},[1058,3939,3940],{},"查詢時間",[1058,3942,3943],{},"數秒",[1058,3945,3946],{},"\u003C 10ms",[1043,3948,3949,3952,3955],{},[1058,3950,3951],{},"資料庫負載",[1058,3953,3954],{},"每次請求跨表掃描",[1058,3956,3957],{},"每日一次聚合",[1043,3959,3960,3963,3966],{},[1058,3961,3962],{},"資料即時性",[1058,3964,3965],{},"即時",[1058,3967,3968],{},"前一天結算準確",[224,3970],{},[173,3972,3973],{"id":3973},"適用場景",[354,3975,3976,3977,3980],{},"這個模式適合",[183,3978,3979],{},"讀多寫少、資料量大、對即時性要求不高","的統計需求，例如：",[177,3982,3983,3986,3989],{},[180,3984,3985],{},"管理後台的訂單、用戶、收入總覽",[180,3987,3988],{},"報表系統的歷史趨勢圖",[180,3990,3991],{},"定期推播給管理員的每日摘要",[1681,3993,3994],{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":240,"searchDepth":255,"depth":255,"links":3996},[3997,3998,3999,4000,4001,4002,4003,4008,4009],{"id":1726,"depth":255,"text":1726},{"id":1771,"depth":255,"text":1772},{"id":1789,"depth":255,"text":1790},{"id":1974,"depth":255,"text":1975},{"id":3486,"depth":255,"text":3487},{"id":3617,"depth":255,"text":3617},{"id":3696,"depth":255,"text":3697,"children":4004},[4005,4006,4007],{"id":3717,"depth":276,"text":3718},{"id":3876,"depth":276,"text":3877},{"id":3900,"depth":276,"text":3901},{"id":3918,"depth":255,"text":3918},{"id":3973,"depth":255,"text":3973},"2026-03-10","資料量龐大導致查詢變慢，透過 Cron Job 將每日統計好的資料寫入 Snapshot Table，查詢不再需要跨表掃描改為單表讀取。","\u002Fimages\u002Fdaily-snapshot.jpg",{},{"title":10,"description":4011},"wWf7247tjW3NLW0rgckerIiTQ17DpIh6yqR3V0OJ3lE",{"id":4017,"title":22,"author":4018,"body":4020,"date":5183,"description":5184,"extension":1713,"externalUrl":43,"image":5185,"meta":5186,"minRead":319,"navigation":130,"path":23,"seo":5187,"stem":24,"__hash__":5188},"blog\u002Farticles\u002Fsecurity-best-practices.md",{"name":166,"avatar":4019},{"src":168,"alt":166},{"type":170,"value":4021,"toc":5160},[4022,4025,4029,4032,4134,4139,4158,4162,4165,4196,4198,4201,4205,4208,4290,4294,4297,4347,4352,4358,4361,4417,4419,4423,4426,4471,4473,4476,4479,4516,4521,4536,4538,4542,4546,4549,4684,4688,4695,4800,4802,4805,4809,4905,4909,4912,4951,4953,4956,4959,5079,5084,5095,5097,5101,5154,5157],[173,4023,4024],{"id":4024},"身份驗證與授權",[230,4026,4028],{"id":4027},"使用-jwt-的注意事項","使用 JWT 的注意事項",[354,4030,4031],{},"JWT（JSON Web Token）是常見的無狀態驗證方案，但實作細節很容易踩坑。",[235,4033,4037],{"className":4034,"code":4035,"language":4036,"meta":240,"style":240},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F ❌ 錯誤：演算法設為 none，任何人都能偽造 token\njwt.verify(token, secret, { algorithms: ['none'] })\n\n\u002F\u002F ✅ 正確：明確指定演算法\njwt.verify(token, secret, { algorithms: ['HS256'] })\n","ts",[242,4038,4039,4044,4088,4092,4097],{"__ignoreMap":240},[245,4040,4041],{"class":247,"line":248},[245,4042,4043],{"class":251},"\u002F\u002F ❌ 錯誤：演算法設為 none，任何人都能偽造 token\n",[245,4045,4046,4049,4051,4054,4057,4059,4061,4063,4065,4068,4070,4072,4075,4078,4080,4083,4085],{"class":247,"line":255},[245,4047,4048],{"class":272},"jwt",[245,4050,1828],{"class":392},[245,4052,4053],{"class":986},"verify",[245,4055,4056],{"class":272},"(token",[245,4058,1834],{"class":392},[245,4060,376],{"class":272},[245,4062,1834],{"class":392},[245,4064,2011],{"class":392},[245,4066,4067],{"class":717}," algorithms",[245,4069,736],{"class":392},[245,4071,2324],{"class":272},[245,4073,4074],{"class":392},"'",[245,4076,4077],{"class":262},"none",[245,4079,4074],{"class":392},[245,4081,4082],{"class":272},"] ",[245,4084,3608],{"class":392},[245,4086,4087],{"class":272},")\n",[245,4089,4090],{"class":247,"line":276},[245,4091,288],{"emptyLinePlaceholder":130},[245,4093,4094],{"class":247,"line":285},[245,4095,4096],{"class":251},"\u002F\u002F ✅ 正確：明確指定演算法\n",[245,4098,4099,4101,4103,4105,4107,4109,4111,4113,4115,4117,4119,4121,4123,4126,4128,4130,4132],{"class":247,"line":291},[245,4100,4048],{"class":272},[245,4102,1828],{"class":392},[245,4104,4053],{"class":986},[245,4106,4056],{"class":272},[245,4108,1834],{"class":392},[245,4110,376],{"class":272},[245,4112,1834],{"class":392},[245,4114,2011],{"class":392},[245,4116,4067],{"class":717},[245,4118,736],{"class":392},[245,4120,2324],{"class":272},[245,4122,4074],{"class":392},[245,4124,4125],{"class":262},"HS256",[245,4127,4074],{"class":392},[245,4129,4082],{"class":272},[245,4131,3608],{"class":392},[245,4133,4087],{"class":272},[354,4135,4136],{},[183,4137,4138],{},"常見錯誤：",[177,4140,4141,4152,4155],{},[180,4142,4143,4144,4147,4148,4151],{},"將 JWT 存在 ",[242,4145,4146],{},"localStorage","，容易被 XSS 竊取，應改用 ",[242,4149,4150],{},"httpOnly"," Cookie",[180,4153,4154],{},"Token 有效期設太長，應配合 Refresh Token 機制縮短 Access Token 壽命",[180,4156,4157],{},"沒有實作 Token 撤銷機制，登出後 token 仍然有效",[230,4159,4161],{"id":4160},"最小權限原則principle-of-least-privilege","最小權限原則（Principle of Least Privilege）",[354,4163,4164],{},"每個用戶、服務、資料庫帳號只給完成任務所需的最小權限。",[235,4166,4170],{"className":4167,"code":4168,"language":4169,"meta":240,"style":240},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","-- ❌ 給 API 帳號完整資料庫權限\nGRANT ALL PRIVILEGES ON *.* TO 'api_user'@'%';\n\n-- ✅ 只給必要的讀寫權限\nGRANT SELECT, INSERT, UPDATE ON app_db.orders TO 'api_user'@'localhost';\n","sql",[242,4171,4172,4177,4182,4186,4191],{"__ignoreMap":240},[245,4173,4174],{"class":247,"line":248},[245,4175,4176],{},"-- ❌ 給 API 帳號完整資料庫權限\n",[245,4178,4179],{"class":247,"line":255},[245,4180,4181],{},"GRANT ALL PRIVILEGES ON *.* TO 'api_user'@'%';\n",[245,4183,4184],{"class":247,"line":276},[245,4185,288],{"emptyLinePlaceholder":130},[245,4187,4188],{"class":247,"line":285},[245,4189,4190],{},"-- ✅ 只給必要的讀寫權限\n",[245,4192,4193],{"class":247,"line":291},[245,4194,4195],{},"GRANT SELECT, INSERT, UPDATE ON app_db.orders TO 'api_user'@'localhost';\n",[224,4197],{},[173,4199,4200],{"id":4200},"輸入驗證與防注入",[230,4202,4204],{"id":4203},"sql-injection","SQL Injection",[354,4206,4207],{},"永遠不要用字串拼接組 SQL 查詢。",[235,4209,4211],{"className":4034,"code":4210,"language":4036,"meta":240,"style":240},"\u002F\u002F ❌ 危險：攻擊者輸入 ' OR '1'='1 即可繞過驗證\nconst query = `SELECT * FROM users WHERE email = '${email}'`\n\n\u002F\u002F ✅ 使用參數化查詢\nconst query = 'SELECT * FROM users WHERE email = $1'\nawait db.query(query, [email])\n",[242,4212,4213,4218,4246,4250,4255,4270],{"__ignoreMap":240},[245,4214,4215],{"class":247,"line":248},[245,4216,4217],{"class":251},"\u002F\u002F ❌ 危險：攻擊者輸入 ' OR '1'='1 即可繞過驗證\n",[245,4219,4220,4222,4225,4227,4230,4233,4236,4239,4241,4243],{"class":247,"line":255},[245,4221,3635],{"class":2041},[245,4223,4224],{"class":272}," query ",[245,4226,931],{"class":392},[245,4228,4229],{"class":392}," `",[245,4231,4232],{"class":262},"SELECT * FROM users WHERE email = '",[245,4234,4235],{"class":392},"${",[245,4237,4238],{"class":272},"email",[245,4240,3608],{"class":392},[245,4242,4074],{"class":262},[245,4244,4245],{"class":392},"`\n",[245,4247,4248],{"class":247,"line":276},[245,4249,288],{"emptyLinePlaceholder":130},[245,4251,4252],{"class":247,"line":285},[245,4253,4254],{"class":251},"\u002F\u002F ✅ 使用參數化查詢\n",[245,4256,4257,4259,4261,4263,4265,4268],{"class":247,"line":291},[245,4258,3635],{"class":2041},[245,4260,4224],{"class":272},[245,4262,931],{"class":392},[245,4264,879],{"class":392},[245,4266,4267],{"class":262},"SELECT * FROM users WHERE email = $1",[245,4269,885],{"class":392},[245,4271,4272,4275,4278,4280,4282,4285,4287],{"class":247,"line":297},[245,4273,4274],{"class":1457},"await",[245,4276,4277],{"class":272}," db",[245,4279,1828],{"class":392},[245,4281,2378],{"class":986},[245,4283,4284],{"class":272},"(query",[245,4286,1834],{"class":392},[245,4288,4289],{"class":272}," [email])\n",[230,4291,4293],{"id":4292},"xsscross-site-scripting","XSS（Cross-Site Scripting）",[354,4295,4296],{},"對所有用戶輸入進行跳脫處理，避免注入惡意腳本。",[235,4298,4300],{"className":4034,"code":4299,"language":4036,"meta":240,"style":240},"import DOMPurify from 'dompurify'\n\n\u002F\u002F ✅ 渲染用戶輸入的 HTML 前先清理\nconst clean = DOMPurify.sanitize(userInput)\n",[242,4301,4302,4318,4322,4327],{"__ignoreMap":240},[245,4303,4304,4306,4309,4311,4313,4316],{"class":247,"line":248},[245,4305,1988],{"class":1457},[245,4307,4308],{"class":272}," DOMPurify ",[245,4310,1994],{"class":1457},[245,4312,879],{"class":392},[245,4314,4315],{"class":262},"dompurify",[245,4317,885],{"class":392},[245,4319,4320],{"class":247,"line":255},[245,4321,288],{"emptyLinePlaceholder":130},[245,4323,4324],{"class":247,"line":276},[245,4325,4326],{"class":251},"\u002F\u002F ✅ 渲染用戶輸入的 HTML 前先清理\n",[245,4328,4329,4331,4334,4336,4339,4341,4344],{"class":247,"line":285},[245,4330,3635],{"class":2041},[245,4332,4333],{"class":272}," clean ",[245,4335,931],{"class":392},[245,4337,4338],{"class":272}," DOMPurify",[245,4340,1828],{"class":392},[245,4342,4343],{"class":986},"sanitize",[245,4345,4346],{"class":272},"(userInput)\n",[354,4348,4349],{},[183,4350,4351],{},"HTTP Header 防護：",[235,4353,4356],{"className":4354,"code":4355,"language":915},[913],"Content-Security-Policy: default-src 'self'\nX-Content-Type-Options: nosniff\nX-Frame-Options: DENY\n",[242,4357,4355],{"__ignoreMap":240},[230,4359,4360],{"id":4360},"環境變數管理",[235,4362,4364],{"className":237,"code":4363,"language":239,"meta":240,"style":240},"# ❌ 絕對不能 commit 進 git\nDB_PASSWORD=mysecretpassword\nJWT_SECRET=abc123\n\n# ✅ 用 .env 並加入 .gitignore\necho \".env\" >> .gitignore\n",[242,4365,4366,4371,4381,4391,4395,4400],{"__ignoreMap":240},[245,4367,4368],{"class":247,"line":248},[245,4369,4370],{"class":251},"# ❌ 絕對不能 commit 進 git\n",[245,4372,4373,4376,4378],{"class":247,"line":255},[245,4374,4375],{"class":272},"DB_PASSWORD",[245,4377,931],{"class":392},[245,4379,4380],{"class":262},"mysecretpassword\n",[245,4382,4383,4386,4388],{"class":247,"line":276},[245,4384,4385],{"class":272},"JWT_SECRET",[245,4387,931],{"class":392},[245,4389,4390],{"class":262},"abc123\n",[245,4392,4393],{"class":247,"line":285},[245,4394,288],{"emptyLinePlaceholder":130},[245,4396,4397],{"class":247,"line":291},[245,4398,4399],{"class":251},"# ✅ 用 .env 並加入 .gitignore\n",[245,4401,4402,4404,4406,4409,4411,4414],{"class":247,"line":297},[245,4403,987],{"class":986},[245,4405,955],{"class":392},[245,4407,4408],{"class":262},".env",[245,4410,393],{"class":392},[245,4412,4413],{"class":392}," >>",[245,4415,4416],{"class":262}," .gitignore\n",[224,4418],{},[173,4420,4422],{"id":4421},"https-與資料傳輸","HTTPS 與資料傳輸",[354,4424,4425],{},"所有生產環境流量必須走 HTTPS，並確保 TLS 設定正確。",[235,4427,4431],{"className":4428,"code":4429,"language":4430,"meta":240,"style":240},"language-nginx shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","server {\n    listen 443 ssl;\n    ssl_protocols TLSv1.2 TLSv1.3;   # 停用舊版 TLS\n    ssl_ciphers HIGH:!aNULL:!MD5;\n\n    # HSTS：強制瀏覽器只走 HTTPS\n    add_header Strict-Transport-Security \"max-age=31536000\" always;\n}\n","nginx",[242,4432,4433,4438,4443,4448,4453,4457,4462,4467],{"__ignoreMap":240},[245,4434,4435],{"class":247,"line":248},[245,4436,4437],{},"server {\n",[245,4439,4440],{"class":247,"line":255},[245,4441,4442],{},"    listen 443 ssl;\n",[245,4444,4445],{"class":247,"line":276},[245,4446,4447],{},"    ssl_protocols TLSv1.2 TLSv1.3;   # 停用舊版 TLS\n",[245,4449,4450],{"class":247,"line":285},[245,4451,4452],{},"    ssl_ciphers HIGH:!aNULL:!MD5;\n",[245,4454,4455],{"class":247,"line":291},[245,4456,288],{"emptyLinePlaceholder":130},[245,4458,4459],{"class":247,"line":297},[245,4460,4461],{},"    # HSTS：強制瀏覽器只走 HTTPS\n",[245,4463,4464],{"class":247,"line":311},[245,4465,4466],{},"    add_header Strict-Transport-Security \"max-age=31536000\" always;\n",[245,4468,4469],{"class":247,"line":319},[245,4470,3441],{},[224,4472],{},[173,4474,4475],{"id":4475},"依賴管理",[354,4477,4478],{},"第三方套件是常見的攻擊入口，需要定期審查。",[235,4480,4482],{"className":237,"code":4481,"language":239,"meta":240,"style":240},"# 掃描已知漏洞\nnpm audit\n\n# 自動修復低風險漏洞\nnpm audit fix\n",[242,4483,4484,4489,4497,4501,4506],{"__ignoreMap":240},[245,4485,4486],{"class":247,"line":248},[245,4487,4488],{"class":251},"# 掃描已知漏洞\n",[245,4490,4491,4494],{"class":247,"line":255},[245,4492,4493],{"class":258},"npm",[245,4495,4496],{"class":262}," audit\n",[245,4498,4499],{"class":247,"line":276},[245,4500,288],{"emptyLinePlaceholder":130},[245,4502,4503],{"class":247,"line":285},[245,4504,4505],{"class":251},"# 自動修復低風險漏洞\n",[245,4507,4508,4510,4513],{"class":247,"line":291},[245,4509,4493],{"class":258},[245,4511,4512],{"class":262}," audit",[245,4514,4515],{"class":262}," fix\n",[354,4517,4518],{},[183,4519,4520],{},"最佳實踐：",[177,4522,4523,4530,4533],{},[180,4524,4525,4526,4529],{},"鎖定套件版本（",[242,4527,4528],{},"package-lock.json"," 要 commit）",[180,4531,4532],{},"定期更新依賴，訂閱安全通報（如 GitHub Dependabot）",[180,4534,4535],{},"不引入不必要的套件，減少攻擊面",[224,4537],{},[173,4539,4541],{"id":4540},"api-安全","API 安全",[230,4543,4545],{"id":4544},"rate-limiting","Rate Limiting",[354,4547,4548],{},"防止暴力破解與 DDoS。",[235,4550,4552],{"className":4034,"code":4551,"language":4036,"meta":240,"style":240},"import rateLimit from 'express-rate-limit'\n\nconst loginLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, \u002F\u002F 15 分鐘\n  max: 10,                   \u002F\u002F 最多 10 次嘗試\n  message: '嘗試次數過多，請稍後再試'\n})\n\napp.post('\u002Fauth\u002Flogin', loginLimiter, loginHandler)\n",[242,4553,4554,4570,4574,4590,4616,4631,4645,4651,4655],{"__ignoreMap":240},[245,4555,4556,4558,4561,4563,4565,4568],{"class":247,"line":248},[245,4557,1988],{"class":1457},[245,4559,4560],{"class":272}," rateLimit ",[245,4562,1994],{"class":1457},[245,4564,879],{"class":392},[245,4566,4567],{"class":262},"express-rate-limit",[245,4569,885],{"class":392},[245,4571,4572],{"class":247,"line":255},[245,4573,288],{"emptyLinePlaceholder":130},[245,4575,4576,4578,4581,4583,4586,4588],{"class":247,"line":276},[245,4577,3635],{"class":2041},[245,4579,4580],{"class":272}," loginLimiter ",[245,4582,931],{"class":392},[245,4584,4585],{"class":986}," rateLimit",[245,4587,2051],{"class":272},[245,4589,3010],{"class":392},[245,4591,4592,4595,4597,4600,4603,4606,4608,4611,4613],{"class":247,"line":285},[245,4593,4594],{"class":717},"  windowMs",[245,4596,736],{"class":392},[245,4598,4599],{"class":2146}," 15",[245,4601,4602],{"class":392}," *",[245,4604,4605],{"class":2146}," 60",[245,4607,4602],{"class":392},[245,4609,4610],{"class":2146}," 1000",[245,4612,1834],{"class":392},[245,4614,4615],{"class":251}," \u002F\u002F 15 分鐘\n",[245,4617,4618,4621,4623,4626,4628],{"class":247,"line":291},[245,4619,4620],{"class":717},"  max",[245,4622,736],{"class":392},[245,4624,4625],{"class":2146}," 10",[245,4627,1834],{"class":392},[245,4629,4630],{"class":251},"                   \u002F\u002F 最多 10 次嘗試\n",[245,4632,4633,4636,4638,4640,4643],{"class":247,"line":297},[245,4634,4635],{"class":717},"  message",[245,4637,736],{"class":392},[245,4639,879],{"class":392},[245,4641,4642],{"class":262},"嘗試次數過多，請稍後再試",[245,4644,885],{"class":392},[245,4646,4647,4649],{"class":247,"line":311},[245,4648,3608],{"class":392},[245,4650,4087],{"class":272},[245,4652,4653],{"class":247,"line":319},[245,4654,288],{"emptyLinePlaceholder":130},[245,4656,4657,4660,4662,4665,4667,4669,4672,4674,4676,4679,4681],{"class":247,"line":324},[245,4658,4659],{"class":272},"app",[245,4661,1828],{"class":392},[245,4663,4664],{"class":986},"post",[245,4666,2051],{"class":272},[245,4668,4074],{"class":392},[245,4670,4671],{"class":262},"\u002Fauth\u002Flogin",[245,4673,4074],{"class":392},[245,4675,1834],{"class":392},[245,4677,4678],{"class":272}," loginLimiter",[245,4680,1834],{"class":392},[245,4682,4683],{"class":272}," loginHandler)\n",[230,4685,4687],{"id":4686},"cors-設定","CORS 設定",[354,4689,4690,4691,4694],{},"不要用 ",[242,4692,4693],{},"*"," 允許所有來源。",[235,4696,4698],{"className":4034,"code":4697,"language":4036,"meta":240,"style":240},"\u002F\u002F ❌ 允許所有來源\napp.use(cors({ origin: '*' }))\n\n\u002F\u002F ✅ 明確指定允許的來源\napp.use(cors({\n  origin: ['https:\u002F\u002Fyourdomain.com'],\n  credentials: true\n}))\n",[242,4699,4700,4705,4739,4743,4748,4764,4784,4794],{"__ignoreMap":240},[245,4701,4702],{"class":247,"line":248},[245,4703,4704],{"class":251},"\u002F\u002F ❌ 允許所有來源\n",[245,4706,4707,4709,4711,4714,4716,4719,4721,4723,4726,4728,4730,4732,4734,4736],{"class":247,"line":255},[245,4708,4659],{"class":272},[245,4710,1828],{"class":392},[245,4712,4713],{"class":986},"use",[245,4715,2051],{"class":272},[245,4717,4718],{"class":986},"cors",[245,4720,2051],{"class":272},[245,4722,958],{"class":392},[245,4724,4725],{"class":717}," origin",[245,4727,736],{"class":392},[245,4729,879],{"class":392},[245,4731,4693],{"class":262},[245,4733,4074],{"class":392},[245,4735,2017],{"class":392},[245,4737,4738],{"class":272},"))\n",[245,4740,4741],{"class":247,"line":276},[245,4742,288],{"emptyLinePlaceholder":130},[245,4744,4745],{"class":247,"line":285},[245,4746,4747],{"class":251},"\u002F\u002F ✅ 明確指定允許的來源\n",[245,4749,4750,4752,4754,4756,4758,4760,4762],{"class":247,"line":291},[245,4751,4659],{"class":272},[245,4753,1828],{"class":392},[245,4755,4713],{"class":986},[245,4757,2051],{"class":272},[245,4759,4718],{"class":986},[245,4761,2051],{"class":272},[245,4763,3010],{"class":392},[245,4765,4766,4769,4771,4773,4775,4778,4780,4782],{"class":247,"line":297},[245,4767,4768],{"class":717},"  origin",[245,4770,736],{"class":392},[245,4772,2324],{"class":272},[245,4774,4074],{"class":392},[245,4776,4777],{"class":262},"https:\u002F\u002Fyourdomain.com",[245,4779,4074],{"class":392},[245,4781,2345],{"class":272},[245,4783,1909],{"class":392},[245,4785,4786,4789,4791],{"class":247,"line":311},[245,4787,4788],{"class":717},"  credentials",[245,4790,736],{"class":392},[245,4792,4793],{"class":1306}," true\n",[245,4795,4796,4798],{"class":247,"line":319},[245,4797,3608],{"class":392},[245,4799,4738],{"class":272},[224,4801],{},[173,4803,4804],{"id":4804},"基礎設施安全",[230,4806,4808],{"id":4807},"kubernetes-gke","Kubernetes \u002F GKE",[235,4810,4812],{"className":708,"code":4811,"language":710,"meta":240,"style":240},"# ✅ 不以 root 身份執行容器\nsecurityContext:\n  runAsNonRoot: true\n  runAsUser: 1000\n  readOnlyRootFilesystem: true\n\n# ✅ 限制資源，防止單一 Pod 吃光資源\nresources:\n  limits:\n    cpu: \"500m\"\n    memory: \"256Mi\"\n",[242,4813,4814,4819,4826,4835,4845,4854,4858,4863,4870,4877,4891],{"__ignoreMap":240},[245,4815,4816],{"class":247,"line":248},[245,4817,4818],{"class":251},"# ✅ 不以 root 身份執行容器\n",[245,4820,4821,4824],{"class":247,"line":255},[245,4822,4823],{"class":717},"securityContext",[245,4825,721],{"class":392},[245,4827,4828,4831,4833],{"class":247,"line":276},[245,4829,4830],{"class":717},"  runAsNonRoot",[245,4832,736],{"class":392},[245,4834,4793],{"class":1306},[245,4836,4837,4840,4842],{"class":247,"line":285},[245,4838,4839],{"class":717},"  runAsUser",[245,4841,736],{"class":392},[245,4843,4844],{"class":2146}," 1000\n",[245,4846,4847,4850,4852],{"class":247,"line":291},[245,4848,4849],{"class":717},"  readOnlyRootFilesystem",[245,4851,736],{"class":392},[245,4853,4793],{"class":1306},[245,4855,4856],{"class":247,"line":297},[245,4857,288],{"emptyLinePlaceholder":130},[245,4859,4860],{"class":247,"line":311},[245,4861,4862],{"class":251},"# ✅ 限制資源，防止單一 Pod 吃光資源\n",[245,4864,4865,4868],{"class":247,"line":319},[245,4866,4867],{"class":717},"resources",[245,4869,721],{"class":392},[245,4871,4872,4875],{"class":247,"line":324},[245,4873,4874],{"class":717},"  limits",[245,4876,721],{"class":392},[245,4878,4879,4882,4884,4886,4889],{"class":247,"line":330},[245,4880,4881],{"class":717},"    cpu",[245,4883,736],{"class":392},[245,4885,955],{"class":392},[245,4887,4888],{"class":262},"500m",[245,4890,438],{"class":392},[245,4892,4893,4896,4898,4900,4903],{"class":247,"line":344},[245,4894,4895],{"class":717},"    memory",[245,4897,736],{"class":392},[245,4899,955],{"class":392},[245,4901,4902],{"class":262},"256Mi",[245,4904,438],{"class":392},[230,4906,4908],{"id":4907},"secret-管理","Secret 管理",[354,4910,4911],{},"不要把 secret 直接寫在 YAML 裡。",[235,4913,4915],{"className":237,"code":4914,"language":239,"meta":240,"style":240},"# ✅ 使用 Kubernetes Secret\nkubectl create secret generic db-secret \\\n  --from-literal=password=mysecretpassword\n\n# 或使用 Google Secret Manager \u002F AWS Secrets Manager\n",[242,4916,4917,4922,4937,4942,4946],{"__ignoreMap":240},[245,4918,4919],{"class":247,"line":248},[245,4920,4921],{"class":251},"# ✅ 使用 Kubernetes Secret\n",[245,4923,4924,4926,4928,4930,4932,4935],{"class":247,"line":255},[245,4925,370],{"class":258},[245,4927,373],{"class":262},[245,4929,376],{"class":262},[245,4931,379],{"class":262},[245,4933,4934],{"class":262}," db-secret",[245,4936,273],{"class":272},[245,4938,4939],{"class":247,"line":276},[245,4940,4941],{"class":262},"  --from-literal=password=mysecretpassword\n",[245,4943,4944],{"class":247,"line":285},[245,4945,288],{"emptyLinePlaceholder":130},[245,4947,4948],{"class":247,"line":291},[245,4949,4950],{"class":251},"# 或使用 Google Secret Manager \u002F AWS Secrets Manager\n",[224,4952],{},[173,4954,4955],{"id":4955},"日誌與監控",[354,4957,4958],{},"記錄足夠的資訊幫助事後調查，但避免記錄敏感資料。",[235,4960,4962],{"className":4034,"code":4961,"language":4036,"meta":240,"style":240},"\u002F\u002F ❌ 日誌包含密碼\nlogger.info(`Login attempt: ${email} \u002F ${password}`)\n\n\u002F\u002F ✅ 只記錄必要資訊\nlogger.info(`Login attempt: ${email}`, { ip: req.ip, userAgent: req.headers['user-agent'] })\n",[242,4963,4964,4969,5006,5010,5015],{"__ignoreMap":240},[245,4965,4966],{"class":247,"line":248},[245,4967,4968],{"class":251},"\u002F\u002F ❌ 日誌包含密碼\n",[245,4970,4971,4974,4976,4979,4981,4984,4987,4989,4991,4993,4996,4998,5001,5004],{"class":247,"line":255},[245,4972,4973],{"class":272},"logger",[245,4975,1828],{"class":392},[245,4977,4978],{"class":986},"info",[245,4980,2051],{"class":272},[245,4982,4983],{"class":392},"`",[245,4985,4986],{"class":262},"Login attempt: ",[245,4988,4235],{"class":392},[245,4990,4238],{"class":272},[245,4992,3608],{"class":392},[245,4994,4995],{"class":262}," \u002F ",[245,4997,4235],{"class":392},[245,4999,5000],{"class":272},"password",[245,5002,5003],{"class":392},"}`",[245,5005,4087],{"class":272},[245,5007,5008],{"class":247,"line":276},[245,5009,288],{"emptyLinePlaceholder":130},[245,5011,5012],{"class":247,"line":285},[245,5013,5014],{"class":251},"\u002F\u002F ✅ 只記錄必要資訊\n",[245,5016,5017,5019,5021,5023,5025,5027,5029,5031,5033,5035,5037,5039,5042,5044,5047,5049,5052,5054,5057,5059,5061,5063,5066,5068,5071,5073,5075,5077],{"class":247,"line":291},[245,5018,4973],{"class":272},[245,5020,1828],{"class":392},[245,5022,4978],{"class":986},[245,5024,2051],{"class":272},[245,5026,4983],{"class":392},[245,5028,4986],{"class":262},[245,5030,4235],{"class":392},[245,5032,4238],{"class":272},[245,5034,5003],{"class":392},[245,5036,1834],{"class":392},[245,5038,2011],{"class":392},[245,5040,5041],{"class":717}," ip",[245,5043,736],{"class":392},[245,5045,5046],{"class":272}," req",[245,5048,1828],{"class":392},[245,5050,5051],{"class":272},"ip",[245,5053,1834],{"class":392},[245,5055,5056],{"class":717}," userAgent",[245,5058,736],{"class":392},[245,5060,5046],{"class":272},[245,5062,1828],{"class":392},[245,5064,5065],{"class":272},"headers[",[245,5067,4074],{"class":392},[245,5069,5070],{"class":262},"user-agent",[245,5072,4074],{"class":392},[245,5074,4082],{"class":272},[245,5076,3608],{"class":392},[245,5078,4087],{"class":272},[354,5080,5081],{},[183,5082,5083],{},"應該監控的指標：",[177,5085,5086,5089,5092],{},[180,5087,5088],{},"異常登入失敗次數",[180,5090,5091],{},"非預期的 API 錯誤率上升",[180,5093,5094],{},"非工作時間的大量資料存取",[224,5096],{},[173,5098,5100],{"id":5099},"安全開發流程devsecops","安全開發流程（DevSecOps）",[1037,5102,5103,5113],{},[1040,5104,5105],{},[1043,5106,5107,5110],{},[1046,5108,5109],{},"階段",[1046,5111,5112],{},"實踐",[1053,5114,5115,5123,5138,5146],{},[1043,5116,5117,5120],{},[1058,5118,5119],{},"開發",[1058,5121,5122],{},"Code Review、靜態分析（ESLint security rules）",[1043,5124,5125,5128],{},[1058,5126,5127],{},"CI\u002FCD",[1058,5129,5130,5131,1756,5134,5137],{},"自動化掃描（",[242,5132,5133],{},"npm audit",[242,5135,5136],{},"trivy"," 掃 Docker image）",[1043,5139,5140,5143],{},[1058,5141,5142],{},"部署",[1058,5144,5145],{},"最小權限、Secret Manager、網路隔離",[1043,5147,5148,5151],{},[1058,5149,5150],{},"運營",[1058,5152,5153],{},"日誌監控、定期滲透測試、漏洞回報機制",[354,5155,5156],{},"資安不是一次性的工作，而是需要在整個開發流程中持續落實的文化。",[1681,5158,5159],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":240,"searchDepth":255,"depth":255,"links":5161},[5162,5166,5171,5172,5173,5177,5181,5182],{"id":4024,"depth":255,"text":4024,"children":5163},[5164,5165],{"id":4027,"depth":276,"text":4028},{"id":4160,"depth":276,"text":4161},{"id":4200,"depth":255,"text":4200,"children":5167},[5168,5169,5170],{"id":4203,"depth":276,"text":4204},{"id":4292,"depth":276,"text":4293},{"id":4360,"depth":276,"text":4360},{"id":4421,"depth":255,"text":4422},{"id":4475,"depth":255,"text":4475},{"id":4540,"depth":255,"text":4541,"children":5174},[5175,5176],{"id":4544,"depth":276,"text":4545},{"id":4686,"depth":276,"text":4687},{"id":4804,"depth":255,"text":4804,"children":5178},[5179,5180],{"id":4807,"depth":276,"text":4808},{"id":4907,"depth":276,"text":4908},{"id":4955,"depth":255,"text":4955},{"id":5099,"depth":255,"text":5100},"2025-12-05","整理日常開發中常用的資安觀念與實踐方式。","\u002Fimages\u002Fsecurity.jpg",{},{"title":22,"description":5184},"zWX3ru6e2VKC3sHo1ft7LY1UaT_FOrgnGi5WvJmxyJU",{"data":5190,"body":5191},{},{"type":5192,"children":5193},"root",[5194],{"type":5195,"tag":354,"props":5196,"children":5197},"element",{},[5198],{"type":915,"value":5199},"我專精於後端工程與雲端基礎設施。包含設計與建置 REST\u002FgRPC API、分散式系統架構、資料庫 schema 設計與優化、Kubernetes 叢集建置與維運、CI\u002FCD 流程自動化，以及為擴展後端的團隊提供技術顧問服務。",{"data":5201,"body":5202},{},{"type":5192,"children":5203},[5204],{"type":5195,"tag":354,"props":5205,"children":5206},{},[5207],{"type":915,"value":5208},"我從需求收集與系統設計開始，在寫下第一行程式碼之前先產出架構文件。接著以小而經過充分測試的增量進行迭代——整合測試、壓力測試，以及從第一天起就內建的可觀測性。我與產品和前端團隊密切合作，確保介面乾淨且有版本管理。",{"data":5210,"body":5211},{},{"type":5192,"children":5212},[5213],{"type":5195,"tag":354,"props":5214,"children":5215},{},[5216],{"type":915,"value":5217},"當然。早期新創公司能從務實的架構選擇中受益，這些選擇可以擴展而不需要完全重寫。我幫助團隊快速推進，同時不累積沉重的技術債。",1781661889871]