[{"data":1,"prerenderedAt":7145},["ShallowReactive",2],{"navigation":3,"blog-page":34,"blogs":44},[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,"body":37,"description":38,"extension":39,"links":37,"meta":40,"navigation":41,"path":6,"seo":42,"stem":7,"__hash__":43},"pages\u002Farticles.yml","最新文章",null,"關於分散式系統、後端工程與生產環境維運的一些心得 。","yml",{},true,{"title":36,"description":38},"LB9mnfasuCtN2WjoHpapxmnuCzUwjQwLSzJ0UtUm4Mo",[45,1601,3899,5072,5789,6419],{"id":46,"title":14,"author":47,"body":51,"date":1594,"description":1595,"extension":1596,"externalUrl":37,"image":1597,"meta":1598,"minRead":173,"navigation":41,"path":15,"seo":1599,"stem":16,"__hash__":1600},"blog\u002Farticles\u002Fgke-deployment.md",{"name":48,"avatar":49},"Gary",{"src":50,"alt":48},"\u002Fimages\u002Fselfie.webp",{"type":52,"value":53,"toc":1567},"minimark",[54,58,105,108,111,116,232,239,243,321,325,358,362,424,428,478,480,483,487,505,509,567,571,684,688,769,773,909,911,915,919,961,966,970,981,984,990,992,996,999,1005,1009,1151,1162,1166,1407,1412,1414,1417,1563],[55,56,57],"h2",{"id":57},"架構",[59,60,61,69,75,81,87,93,99],"ul",{},[62,63,64,68],"li",{},[65,66,67],"strong",{},"gshop-api"," — Node.js\u002FExpress，Port 3001",[62,70,71,74],{},[65,72,73],{},"gshop-dashboard"," — Nuxt.js SSR，Port 3002",[62,76,77,80],{},[65,78,79],{},"gshop-web"," — Nuxt.js SSR，Port 3003",[62,82,83,86],{},[65,84,85],{},"GKE Autopilot Cluster"," — gshop-cluster，asia-east1",[62,88,89,92],{},[65,90,91],{},"Artifact Registry"," — asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop",[62,94,95,98],{},[65,96,97],{},"Database"," — Supabase（Session Pooler）",[62,100,101,104],{},[65,102,103],{},"Domain"," — garydemo.com（Cloudflare）",[106,107],"hr",{},[55,109,110],{"id":110},"部署流程",[112,113,115],"h3",{"id":114},"_1-build-push-image每次更新都要做","1. Build & Push Image（每次更新都要做）",[117,118,123],"pre",{"className":119,"code":120,"language":121,"meta":122,"style":122},"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","",[124,125,126,135,156,165,171,177,191,199,204,210,224],"code",{"__ignoreMap":122},[127,128,131],"span",{"class":129,"line":130},"line",1,[127,132,134],{"class":133},"sHwdD","# API\n",[127,136,138,142,146,149,152],{"class":129,"line":137},2,[127,139,141],{"class":140},"sBMFI","gcloud",[127,143,145],{"class":144},"sfazB"," builds",[127,147,148],{"class":144}," submit",[127,150,151],{"class":144}," .\u002Fgshop-api",[127,153,155],{"class":154},"sTEyZ"," \\\n",[127,157,159,162],{"class":129,"line":158},3,[127,160,161],{"class":144},"  --tag",[127,163,164],{"class":144}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n",[127,166,168],{"class":129,"line":167},4,[127,169,170],{"emptyLinePlaceholder":41},"\n",[127,172,174],{"class":129,"line":173},5,[127,175,176],{"class":133},"# Dashboard\n",[127,178,180,182,184,186,189],{"class":129,"line":179},6,[127,181,141],{"class":140},[127,183,145],{"class":144},[127,185,148],{"class":144},[127,187,188],{"class":144}," .\u002Fgshop-dashboard",[127,190,155],{"class":154},[127,192,194,196],{"class":129,"line":193},7,[127,195,161],{"class":144},[127,197,198],{"class":144}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fdashboard:latest\n",[127,200,202],{"class":129,"line":201},8,[127,203,170],{"emptyLinePlaceholder":41},[127,205,207],{"class":129,"line":206},9,[127,208,209],{"class":133},"# Web\n",[127,211,213,215,217,219,222],{"class":129,"line":212},10,[127,214,141],{"class":140},[127,216,145],{"class":144},[127,218,148],{"class":144},[127,220,221],{"class":144}," .\u002Fgshop-web",[127,223,155],{"class":154},[127,225,227,229],{"class":129,"line":226},11,[127,228,161],{"class":144},[127,230,231],{"class":144}," asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fweb:latest\n",[233,234,235],"blockquote",{},[236,237,238],"p",{},"Cloud Build 在雲端 build（解決本地 Apple Silicon arm64\u002Famd64 問題）",[112,240,242],{"id":241},"_2-建立-kubernetes-secret","2. 建立 Kubernetes Secret",[117,244,246],{"className":119,"code":245,"language":121,"meta":122,"style":122},"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",[124,247,248,267,283,296,309],{"__ignoreMap":122},[127,249,250,253,256,259,262,265],{"class":129,"line":130},[127,251,252],{"class":140},"kubectl",[127,254,255],{"class":144}," create",[127,257,258],{"class":144}," secret",[127,260,261],{"class":144}," generic",[127,263,264],{"class":144}," gshop-api-secret",[127,266,155],{"class":154},[127,268,269,272,276,279,281],{"class":129,"line":137},[127,270,271],{"class":144},"  --from-literal=DATABASE_URL=",[127,273,275],{"class":274},"sMK4o","\"",[127,277,278],{"class":144},"...",[127,280,275],{"class":274},[127,282,155],{"class":154},[127,284,285,288,290,292,294],{"class":129,"line":158},[127,286,287],{"class":144},"  --from-literal=JWT_SECRET=",[127,289,275],{"class":274},[127,291,278],{"class":144},[127,293,275],{"class":274},[127,295,155],{"class":154},[127,297,298,301,303,305,307],{"class":129,"line":167},[127,299,300],{"class":144},"  --from-literal=GCS_BUCKET_NAME=",[127,302,275],{"class":274},[127,304,278],{"class":144},[127,306,275],{"class":274},[127,308,155],{"class":154},[127,310,311,314,316,318],{"class":129,"line":173},[127,312,313],{"class":144},"  --from-literal=ANTHROPIC_API_KEY=",[127,315,275],{"class":274},[127,317,278],{"class":144},[127,319,320],{"class":274},"\"\n",[112,322,324],{"id":323},"_3-建立-cloudflare-origin-certificate-tls-secret","3. 建立 Cloudflare Origin Certificate TLS Secret",[117,326,328],{"className":119,"code":327,"language":121,"meta":122,"style":122},"kubectl create secret tls cloudflare-origin-cert \\\n  --cert=certificate.pem \\\n  --key=private.key\n",[124,329,330,346,353],{"__ignoreMap":122},[127,331,332,334,336,338,341,344],{"class":129,"line":130},[127,333,252],{"class":140},[127,335,255],{"class":144},[127,337,258],{"class":144},[127,339,340],{"class":144}," tls",[127,342,343],{"class":144}," cloudflare-origin-cert",[127,345,155],{"class":154},[127,347,348,351],{"class":129,"line":137},[127,349,350],{"class":144},"  --cert=certificate.pem",[127,352,155],{"class":154},[127,354,355],{"class":129,"line":158},[127,356,357],{"class":144},"  --key=private.key\n",[112,359,361],{"id":360},"_4-套用-k8s-設定","4. 套用 K8s 設定",[117,363,365],{"className":119,"code":364,"language":121,"meta":122,"style":122},"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",[124,366,367,380,391,402,413],{"__ignoreMap":122},[127,368,369,371,374,377],{"class":129,"line":130},[127,370,252],{"class":140},[127,372,373],{"class":144}," apply",[127,375,376],{"class":144}," -f",[127,378,379],{"class":144}," k8s\u002Fbackend-config.yaml\n",[127,381,382,384,386,388],{"class":129,"line":137},[127,383,252],{"class":140},[127,385,373],{"class":144},[127,387,376],{"class":144},[127,389,390],{"class":144}," k8s\u002Fapi-deployment.yaml\n",[127,392,393,395,397,399],{"class":129,"line":158},[127,394,252],{"class":140},[127,396,373],{"class":144},[127,398,376],{"class":144},[127,400,401],{"class":144}," k8s\u002Fdashboard-deployment.yaml\n",[127,403,404,406,408,410],{"class":129,"line":167},[127,405,252],{"class":140},[127,407,373],{"class":144},[127,409,376],{"class":144},[127,411,412],{"class":144}," k8s\u002Fweb-deployment.yaml\n",[127,414,415,417,419,421],{"class":129,"line":173},[127,416,252],{"class":140},[127,418,373],{"class":144},[127,420,376],{"class":144},[127,422,423],{"class":144}," k8s\u002Fingress.yaml\n",[112,425,427],{"id":426},"_5-更新部署有新-commit-時","5. 更新部署（有新 commit 時）",[117,429,431],{"className":119,"code":430,"language":121,"meta":122,"style":122},"# 重新 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",[124,432,433,438,450,456,460,465],{"__ignoreMap":122},[127,434,435],{"class":129,"line":130},[127,436,437],{"class":133},"# 重新 build image\n",[127,439,440,442,444,446,448],{"class":129,"line":137},[127,441,141],{"class":140},[127,443,145],{"class":144},[127,445,148],{"class":144},[127,447,188],{"class":144},[127,449,155],{"class":154},[127,451,452,454],{"class":129,"line":158},[127,453,161],{"class":144},[127,455,198],{"class":144},[127,457,458],{"class":129,"line":167},[127,459,170],{"emptyLinePlaceholder":41},[127,461,462],{"class":129,"line":173},[127,463,464],{"class":133},"# 重啟 pod（Rolling Update，不會 downtime）\n",[127,466,467,469,472,475],{"class":129,"line":179},[127,468,252],{"class":140},[127,470,471],{"class":144}," rollout",[127,473,474],{"class":144}," restart",[127,476,477],{"class":144}," deployment\u002Fgshop-dashboard\n",[106,479],{},[55,481,482],{"id":482},"遇到的狀況與解決",[112,484,486],{"id":485},"_1-arm64amd64-平台不符","1. arm64\u002Famd64 平台不符",[59,488,489,495],{},[62,490,491,494],{},[65,492,493],{},"問題","：本地 Apple Silicon Mac build 出 arm64 image，GKE 需要 amd64",[62,496,497,500,501,504],{},[65,498,499],{},"解法","：改用 ",[124,502,503],{},"gcloud builds submit","，Cloud Build 在 amd64 機器上 build",[112,506,508],{"id":507},"_2-imagepullbackoff沒權限拉-image","2. ImagePullBackOff（沒權限拉 image）",[59,510,511,516],{},[62,512,513,515],{},[65,514,493],{},"：GKE Service Account 沒有 Artifact Registry 讀取權限",[62,517,518,520,521],{},[65,519,499],{},"：\n",[117,522,524],{"className":119,"code":523,"language":121,"meta":122,"style":122},"gcloud projects add-iam-policy-binding gshop-497319 \\\n  --member=\"serviceAccount:620172615694-compute@developer.gserviceaccount.com\" \\\n  --role=\"roles\u002Fartifactregistry.reader\"\n",[124,525,526,541,555],{"__ignoreMap":122},[127,527,528,530,533,536,539],{"class":129,"line":130},[127,529,141],{"class":140},[127,531,532],{"class":144}," projects",[127,534,535],{"class":144}," add-iam-policy-binding",[127,537,538],{"class":144}," gshop-497319",[127,540,155],{"class":154},[127,542,543,546,548,551,553],{"class":129,"line":137},[127,544,545],{"class":144},"  --member=",[127,547,275],{"class":274},[127,549,550],{"class":144},"serviceAccount:620172615694-compute@developer.gserviceaccount.com",[127,552,275],{"class":274},[127,554,155],{"class":154},[127,556,557,560,562,565],{"class":129,"line":158},[127,558,559],{"class":144},"  --role=",[127,561,275],{"class":274},[127,563,564],{"class":144},"roles\u002Fartifactregistry.reader",[127,566,320],{"class":274},[112,568,570],{"id":569},"_3-ingress-遲遲拿不到-ipneg-not-ready","3. Ingress 遲遲拿不到 IP（NEG not ready）",[59,572,573,583,622,632],{},[62,574,575,578,579,582],{},[65,576,577],{},"問題 1","：用了 ",[124,580,581],{},"spec.ingressClassName: gce","，GKE 的 controller 不認這個，要用 annotation",[62,584,585,587,588],{},[65,586,499],{},"：改成：\n",[117,589,593],{"className":590,"code":591,"language":592,"meta":122,"style":122},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","metadata:\n  annotations:\n    kubernetes.io\u002Fingress.class: gce\n","yaml",[124,594,595,604,611],{"__ignoreMap":122},[127,596,597,601],{"class":129,"line":130},[127,598,600],{"class":599},"swJcz","metadata",[127,602,603],{"class":274},":\n",[127,605,606,609],{"class":129,"line":137},[127,607,608],{"class":599},"  annotations",[127,610,603],{"class":274},[127,612,613,616,619],{"class":129,"line":158},[127,614,615],{"class":599},"    kubernetes.io\u002Fingress.class",[127,617,618],{"class":274},":",[127,620,621],{"class":144}," gce\n",[62,623,624,627,628,631],{},[65,625,626],{},"問題 2","：Service 上有舊的 ",[124,629,630],{},"networking.gke.io\u002Ftarget-pool"," annotation 造成衝突",[62,633,634,520,636,683],{},[65,635,499],{},[117,637,639],{"className":119,"code":638,"language":121,"meta":122,"style":122},"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",[124,640,641,657,670],{"__ignoreMap":122},[127,642,643,645,648,651,654],{"class":129,"line":130},[127,644,252],{"class":140},[127,646,647],{"class":144}," annotate",[127,649,650],{"class":144}," svc",[127,652,653],{"class":144}," gshop-api",[127,655,656],{"class":144}," networking.gke.io\u002Ftarget-pool-\n",[127,658,659,661,663,665,668],{"class":129,"line":137},[127,660,252],{"class":140},[127,662,647],{"class":144},[127,664,650],{"class":144},[127,666,667],{"class":144}," gshop-dashboard",[127,669,656],{"class":144},[127,671,672,674,676,678,681],{"class":129,"line":158},[127,673,252],{"class":140},[127,675,647],{"class":144},[127,677,650],{"class":144},[127,679,680],{"class":144}," gshop-web",[127,682,656],{"class":144},"\n再刪掉重建 Ingress",[112,685,687],{"id":686},"_4-502-bad-gateway健康檢查失敗","4. 502 Bad Gateway（健康檢查失敗）",[59,689,690,723],{},[62,691,692,694,695,698,699],{},[65,693,493],{},"：GCP Load Balancer 預設用 ",[124,696,697],{},"GET \u002F"," 做健康檢查，但各服務行為不同：\n",[59,700,701,708,717],{},[62,702,703,704,707],{},"gshop-api：",[124,705,706],{},"\u002F"," 沒有 route，回非 200",[62,709,710,711,713,714],{},"gshop-dashboard：",[124,712,706],{}," 302 redirect 到 ",[124,715,716],{},"\u002Flogin",[62,718,719,720,722],{},"gshop-web：",[124,721,706],{}," 正常回 200（不需要處理）",[62,724,725,727,728,731,732,747,748],{},[65,726,499],{},"：建 ",[124,729,730],{},"BackendConfig"," 指定正確的健康檢查路徑：\n",[117,733,735],{"className":590,"code":734,"language":592,"meta":122,"style":122},"# gshop-api → \u002Fhealth\n# gshop-dashboard → \u002Flogin\n",[124,736,737,742],{"__ignoreMap":122},[127,738,739],{"class":129,"line":130},[127,740,741],{"class":133},"# gshop-api → \u002Fhealth\n",[127,743,744],{"class":129,"line":137},[127,745,746],{"class":133},"# gshop-dashboard → \u002Flogin\n","\n並在 Service 加 annotation：\n",[117,749,751],{"className":590,"code":750,"language":592,"meta":122,"style":122},"cloud.google.com\u002Fbackend-config: '{\"default\": \"gshop-api-backend-config\"}'\n",[124,752,753],{"__ignoreMap":122},[127,754,755,758,760,763,766],{"class":129,"line":130},[127,756,757],{"class":599},"cloud.google.com\u002Fbackend-config",[127,759,618],{"class":274},[127,761,762],{"class":274}," '",[127,764,765],{"class":144},"{\"default\": \"gshop-api-backend-config\"}",[127,767,768],{"class":274},"'\n",[112,770,772],{"id":771},"_5-supabase-連線失敗enotfound","5. Supabase 連線失敗（ENOTFOUND）",[59,774,775,784],{},[62,776,777,779,780,783],{},[65,778,493],{},"：Supabase 直連（",[124,781,782],{},"db.xxx.supabase.co:5432","）是 IPv6 only，GKE 是 IPv4 only",[62,785,786,788,789,792,793,801,802,904],{},[65,787,499],{},"：改用 Supabase ",[65,790,791],{},"Session Pooler"," connection string：\n",[117,794,799],{"className":795,"code":797,"language":798},[796],"language-text","postgresql:\u002F\u002Fpostgres.vqjzzlutlovqoqshwbza:PASSWORD@aws-1-ap-southeast-1.pooler.supabase.com:5432\u002Fpostgres\n","text",[124,800,797],{"__ignoreMap":122},"\n更新 K8s secret：\n",[117,803,805],{"className":119,"code":804,"language":121,"meta":122,"style":122},"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",[124,806,807,822,893],{"__ignoreMap":122},[127,808,809,812,815,817,820],{"class":129,"line":130},[127,810,811],{"class":154},"NEW_URL",[127,813,814],{"class":274},"=",[127,816,275],{"class":274},[127,818,819],{"class":144},"postgresql:\u002F\u002F...",[127,821,320],{"class":274},[127,823,824,826,829,831,833,836,839,842,845,848,850,853,855,858,860,862,864,867,871,874,877,880,883,886,888,891],{"class":129,"line":137},[127,825,252],{"class":140},[127,827,828],{"class":144}," patch",[127,830,258],{"class":144},[127,832,264],{"class":144},[127,834,835],{"class":144}," -p",[127,837,838],{"class":274}," \"",[127,840,841],{"class":144},"{",[127,843,844],{"class":154},"\\\"",[127,846,847],{"class":144},"data",[127,849,844],{"class":154},[127,851,852],{"class":144},":{",[127,854,844],{"class":154},[127,856,857],{"class":144},"DATABASE_URL",[127,859,844],{"class":154},[127,861,618],{"class":144},[127,863,844],{"class":154},[127,865,866],{"class":274},"$(",[127,868,870],{"class":869},"s2Zo4","echo",[127,872,873],{"class":144}," -n ",[127,875,876],{"class":154},"$NEW_URL",[127,878,879],{"class":274}," |",[127,881,882],{"class":140}," base64",[127,884,885],{"class":274},")",[127,887,844],{"class":154},[127,889,890],{"class":144},"}}",[127,892,320],{"class":274},[127,894,895,897,899,901],{"class":129,"line":158},[127,896,252],{"class":140},[127,898,471],{"class":144},[127,900,474],{"class":144},[127,902,903],{"class":144}," deployment\u002Fgshop-api\n",[233,905,906],{},[236,907,908],{},"本地開發不需要改，Mac 支援 IPv6 直連沒問題",[106,910],{},[55,912,914],{"id":913},"dns-https-設定","DNS & HTTPS 設定",[112,916,918],{"id":917},"cloudflare-a-records","Cloudflare A Records",[920,921,922,935],"table",{},[923,924,925],"thead",{},[926,927,928,932],"tr",{},[929,930,931],"th",{},"子網域",[929,933,934],{},"IP",[936,937,938,947,954],"tbody",{},[926,939,940,944],{},[941,942,943],"td",{},"api.garydemo.com",[941,945,946],{},"34.160.168.110",[926,948,949,952],{},[941,950,951],{},"dashboard.garydemo.com",[941,953,946],{},[926,955,956,959],{},[941,957,958],{},"web.garydemo.com",[941,960,946],{},[233,962,963],{},[236,964,965],{},"三個都指向同一個 Ingress IP，由 Ingress 依 Host header 分流",[112,967,969],{"id":968},"cloudflare-ssltls-模式","Cloudflare SSL\u002FTLS 模式",[59,971,972,978],{},[62,973,974,975],{},"設為 ",[65,976,977],{},"Full (Strict)",[62,979,980],{},"使用 Cloudflare Origin Certificate 存為 K8s TLS Secret",[112,982,983],{"id":983},"流量路徑",[117,985,988],{"className":986,"code":987,"language":798},[796],"瀏覽器 → Cloudflare（Proxy + TLS）→ GCP Load Balancer → Ingress → Pod\n",[124,989,987],{"__ignoreMap":122},[106,991],{},[55,993,995],{"id":994},"cicdgithub-actions","CI\u002FCD（GitHub Actions）",[236,997,998],{},"每個 service 各自有獨立 repo，push 到 main 自動 build + deploy。",[117,1000,1003],{"className":1001,"code":1002,"language":798},[796],"push main → GitHub Actions → build image → push Artifact Registry → kubectl rollout restart\n",[124,1004,1002],{"__ignoreMap":122},[112,1006,1008],{"id":1007},"前置設定一次性","前置設定（一次性）",[117,1010,1012],{"className":119,"code":1011,"language":121,"meta":122,"style":122},"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",[124,1013,1014,1031,1043,1047,1059,1072,1083,1087,1099,1111,1122,1126,1145],{"__ignoreMap":122},[127,1015,1016,1018,1021,1024,1026,1029],{"class":129,"line":130},[127,1017,141],{"class":140},[127,1019,1020],{"class":144}," iam",[127,1022,1023],{"class":144}," service-accounts",[127,1025,255],{"class":144},[127,1027,1028],{"class":144}," github-actions",[127,1030,155],{"class":154},[127,1032,1033,1036,1038,1041],{"class":129,"line":137},[127,1034,1035],{"class":144},"  --display-name=",[127,1037,275],{"class":274},[127,1039,1040],{"class":144},"GitHub Actions",[127,1042,320],{"class":274},[127,1044,1045],{"class":129,"line":158},[127,1046,170],{"emptyLinePlaceholder":41},[127,1048,1049,1051,1053,1055,1057],{"class":129,"line":167},[127,1050,141],{"class":140},[127,1052,532],{"class":144},[127,1054,535],{"class":144},[127,1056,538],{"class":144},[127,1058,155],{"class":154},[127,1060,1061,1063,1065,1068,1070],{"class":129,"line":173},[127,1062,545],{"class":144},[127,1064,275],{"class":274},[127,1066,1067],{"class":144},"serviceAccount:github-actions@gshop-497319.iam.gserviceaccount.com",[127,1069,275],{"class":274},[127,1071,155],{"class":154},[127,1073,1074,1076,1078,1081],{"class":129,"line":179},[127,1075,559],{"class":144},[127,1077,275],{"class":274},[127,1079,1080],{"class":144},"roles\u002Fartifactregistry.writer",[127,1082,320],{"class":274},[127,1084,1085],{"class":129,"line":193},[127,1086,170],{"emptyLinePlaceholder":41},[127,1088,1089,1091,1093,1095,1097],{"class":129,"line":201},[127,1090,141],{"class":140},[127,1092,532],{"class":144},[127,1094,535],{"class":144},[127,1096,538],{"class":144},[127,1098,155],{"class":154},[127,1100,1101,1103,1105,1107,1109],{"class":129,"line":206},[127,1102,545],{"class":144},[127,1104,275],{"class":274},[127,1106,1067],{"class":144},[127,1108,275],{"class":274},[127,1110,155],{"class":154},[127,1112,1113,1115,1117,1120],{"class":129,"line":212},[127,1114,559],{"class":144},[127,1116,275],{"class":274},[127,1118,1119],{"class":144},"roles\u002Fcontainer.developer",[127,1121,320],{"class":274},[127,1123,1124],{"class":129,"line":226},[127,1125,170],{"emptyLinePlaceholder":41},[127,1127,1129,1131,1133,1135,1138,1140,1143],{"class":129,"line":1128},12,[127,1130,141],{"class":140},[127,1132,1020],{"class":144},[127,1134,1023],{"class":144},[127,1136,1137],{"class":144}," keys",[127,1139,255],{"class":144},[127,1141,1142],{"class":144}," sa-key.json",[127,1144,155],{"class":154},[127,1146,1148],{"class":129,"line":1147},13,[127,1149,1150],{"class":144},"  --iam-account=github-actions@gshop-497319.iam.gserviceaccount.com\n",[236,1152,1153,1154,1157,1158,1161],{},"GitHub org → Settings → Secrets → New organization secret，名稱 ",[124,1155,1156],{},"GCP_SA_KEY","，貼入 ",[124,1159,1160],{},"sa-key.json"," 內容。",[112,1163,1165],{"id":1164},"workflow-範例","Workflow 範例",[117,1167,1169],{"className":590,"code":1168,"language":592,"meta":122,"style":122},"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",[124,1170,1171,1181,1185,1193,1200,1207,1215,1219,1226,1233,1243,1250,1262,1273,1281,1292,1304,1317,1330,1342,1348,1354,1366,1373,1384,1395],{"__ignoreMap":122},[127,1172,1173,1176,1178],{"class":129,"line":130},[127,1174,1175],{"class":599},"name",[127,1177,618],{"class":274},[127,1179,1180],{"class":144}," Deploy gshop-api\n",[127,1182,1183],{"class":129,"line":137},[127,1184,170],{"emptyLinePlaceholder":41},[127,1186,1187,1191],{"class":129,"line":158},[127,1188,1190],{"class":1189},"sfNiH","on",[127,1192,603],{"class":274},[127,1194,1195,1198],{"class":129,"line":167},[127,1196,1197],{"class":599},"  push",[127,1199,603],{"class":274},[127,1201,1202,1205],{"class":129,"line":173},[127,1203,1204],{"class":599},"    branches",[127,1206,603],{"class":274},[127,1208,1209,1212],{"class":129,"line":179},[127,1210,1211],{"class":274},"      -",[127,1213,1214],{"class":144}," main\n",[127,1216,1217],{"class":129,"line":193},[127,1218,170],{"emptyLinePlaceholder":41},[127,1220,1221,1224],{"class":129,"line":201},[127,1222,1223],{"class":599},"jobs",[127,1225,603],{"class":274},[127,1227,1228,1231],{"class":129,"line":206},[127,1229,1230],{"class":599},"  deploy",[127,1232,603],{"class":274},[127,1234,1235,1238,1240],{"class":129,"line":212},[127,1236,1237],{"class":599},"    runs-on",[127,1239,618],{"class":274},[127,1241,1242],{"class":144}," ubuntu-latest\n",[127,1244,1245,1248],{"class":129,"line":226},[127,1246,1247],{"class":599},"    steps",[127,1249,603],{"class":274},[127,1251,1252,1254,1257,1259],{"class":129,"line":1128},[127,1253,1211],{"class":274},[127,1255,1256],{"class":599}," uses",[127,1258,618],{"class":274},[127,1260,1261],{"class":144}," actions\u002Fcheckout@v4\n",[127,1263,1264,1266,1268,1270],{"class":129,"line":1147},[127,1265,1211],{"class":274},[127,1267,1256],{"class":599},[127,1269,618],{"class":274},[127,1271,1272],{"class":144}," google-github-actions\u002Fauth@v2\n",[127,1274,1276,1279],{"class":129,"line":1275},14,[127,1277,1278],{"class":599},"        with",[127,1280,603],{"class":274},[127,1282,1284,1287,1289],{"class":129,"line":1283},15,[127,1285,1286],{"class":599},"          credentials_json",[127,1288,618],{"class":274},[127,1290,1291],{"class":144}," ${{ secrets.GCP_SA_KEY }}\n",[127,1293,1295,1297,1299,1301],{"class":129,"line":1294},16,[127,1296,1211],{"class":274},[127,1298,1256],{"class":599},[127,1300,618],{"class":274},[127,1302,1303],{"class":144}," google-github-actions\u002Fsetup-gcloud@v2\n",[127,1305,1307,1309,1312,1314],{"class":129,"line":1306},17,[127,1308,1211],{"class":274},[127,1310,1311],{"class":599}," run",[127,1313,618],{"class":274},[127,1315,1316],{"class":144}," gcloud auth configure-docker asia-east1-docker.pkg.dev\n",[127,1318,1320,1322,1325,1327],{"class":129,"line":1319},18,[127,1321,1211],{"class":274},[127,1323,1324],{"class":599}," name",[127,1326,618],{"class":274},[127,1328,1329],{"class":144}," Build and push image\n",[127,1331,1333,1336,1338],{"class":129,"line":1332},19,[127,1334,1335],{"class":599},"        run",[127,1337,618],{"class":274},[127,1339,1341],{"class":1340},"s7zQu"," |\n",[127,1343,1345],{"class":129,"line":1344},20,[127,1346,1347],{"class":144},"          docker build -t asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest .\n",[127,1349,1351],{"class":129,"line":1350},21,[127,1352,1353],{"class":144},"          docker push asia-east1-docker.pkg.dev\u002Fgshop-497319\u002Fgshop\u002Fapi:latest\n",[127,1355,1357,1359,1361,1363],{"class":129,"line":1356},22,[127,1358,1211],{"class":274},[127,1360,1256],{"class":599},[127,1362,618],{"class":274},[127,1364,1365],{"class":144}," google-github-actions\u002Fget-gke-credentials@v2\n",[127,1367,1369,1371],{"class":129,"line":1368},23,[127,1370,1278],{"class":599},[127,1372,603],{"class":274},[127,1374,1376,1379,1381],{"class":129,"line":1375},24,[127,1377,1378],{"class":599},"          cluster_name",[127,1380,618],{"class":274},[127,1382,1383],{"class":144}," gshop-cluster\n",[127,1385,1387,1390,1392],{"class":129,"line":1386},25,[127,1388,1389],{"class":599},"          location",[127,1391,618],{"class":274},[127,1393,1394],{"class":144}," asia-east1\n",[127,1396,1398,1400,1402,1404],{"class":129,"line":1397},26,[127,1399,1211],{"class":274},[127,1401,1311],{"class":599},[127,1403,618],{"class":274},[127,1405,1406],{"class":144}," kubectl rollout restart deployment\u002Fgshop-api\n",[233,1408,1409],{},[236,1410,1411],{},"GitHub Actions runner 是 ubuntu amd64，build 出的 image 天生就是 amd64，不需要 Cloud Build",[106,1413],{},[55,1415,1416],{"id":1416},"常用指令",[117,1418,1420],{"className":119,"code":1419,"language":121,"meta":122,"style":122},"# 查 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",[124,1421,1422,1427,1437,1441,1446,1458,1462,1467,1483,1487,1492,1520,1524,1529,1542,1546,1551],{"__ignoreMap":122},[127,1423,1424],{"class":129,"line":130},[127,1425,1426],{"class":133},"# 查 pod 狀態\n",[127,1428,1429,1431,1434],{"class":129,"line":137},[127,1430,252],{"class":140},[127,1432,1433],{"class":144}," get",[127,1435,1436],{"class":144}," pods\n",[127,1438,1439],{"class":129,"line":158},[127,1440,170],{"emptyLinePlaceholder":41},[127,1442,1443],{"class":129,"line":167},[127,1444,1445],{"class":133},"# 查 ingress IP\n",[127,1447,1448,1450,1452,1455],{"class":129,"line":173},[127,1449,252],{"class":140},[127,1451,1433],{"class":144},[127,1453,1454],{"class":144}," ingress",[127,1456,1457],{"class":144}," gshop-ingress\n",[127,1459,1460],{"class":129,"line":179},[127,1461,170],{"emptyLinePlaceholder":41},[127,1463,1464],{"class":129,"line":193},[127,1465,1466],{"class":133},"# 查某服務 log\n",[127,1468,1469,1471,1474,1477,1480],{"class":129,"line":201},[127,1470,252],{"class":140},[127,1472,1473],{"class":144}," logs",[127,1475,1476],{"class":144}," -l",[127,1478,1479],{"class":144}," app=gshop-api",[127,1481,1482],{"class":144}," --tail=50\n",[127,1484,1485],{"class":129,"line":206},[127,1486,170],{"emptyLinePlaceholder":41},[127,1488,1489],{"class":129,"line":212},[127,1490,1491],{"class":133},"# 查 backend 健康狀態\n",[127,1493,1494,1496,1499,1502,1505,1508,1511,1514,1517],{"class":129,"line":226},[127,1495,141],{"class":140},[127,1497,1498],{"class":144}," compute",[127,1500,1501],{"class":144}," backend-services",[127,1503,1504],{"class":144}," get-health",[127,1506,1507],{"class":274}," \u003C",[127,1509,1510],{"class":144},"backend-nam",[127,1512,1513],{"class":154},"e",[127,1515,1516],{"class":274},">",[127,1518,1519],{"class":144}," --global\n",[127,1521,1522],{"class":129,"line":1128},[127,1523,170],{"emptyLinePlaceholder":41},[127,1525,1526],{"class":129,"line":1147},[127,1527,1528],{"class":133},"# 列出所有 backend\n",[127,1530,1531,1533,1535,1537,1540],{"class":129,"line":1275},[127,1532,141],{"class":140},[127,1534,1498],{"class":144},[127,1536,1501],{"class":144},[127,1538,1539],{"class":144}," list",[127,1541,1519],{"class":144},[127,1543,1544],{"class":129,"line":1283},[127,1545,170],{"emptyLinePlaceholder":41},[127,1547,1548],{"class":129,"line":1294},[127,1549,1550],{"class":133},"# 查 NEG\n",[127,1552,1553,1555,1557,1560],{"class":129,"line":1306},[127,1554,141],{"class":140},[127,1556,1498],{"class":144},[127,1558,1559],{"class":144}," network-endpoint-groups",[127,1561,1562],{"class":144}," list\n",[1564,1565,1566],"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":122,"searchDepth":137,"depth":137,"links":1568},[1569,1570,1577,1584,1589,1593],{"id":57,"depth":137,"text":57},{"id":110,"depth":137,"text":110,"children":1571},[1572,1573,1574,1575,1576],{"id":114,"depth":158,"text":115},{"id":241,"depth":158,"text":242},{"id":323,"depth":158,"text":324},{"id":360,"depth":158,"text":361},{"id":426,"depth":158,"text":427},{"id":482,"depth":137,"text":482,"children":1578},[1579,1580,1581,1582,1583],{"id":485,"depth":158,"text":486},{"id":507,"depth":158,"text":508},{"id":569,"depth":158,"text":570},{"id":686,"depth":158,"text":687},{"id":771,"depth":158,"text":772},{"id":913,"depth":137,"text":914,"children":1585},[1586,1587,1588],{"id":917,"depth":158,"text":918},{"id":968,"depth":158,"text":969},{"id":983,"depth":158,"text":983},{"id":994,"depth":137,"text":995,"children":1590},[1591,1592],{"id":1007,"depth":158,"text":1008},{"id":1164,"depth":158,"text":1165},{"id":1416,"depth":137,"text":1416},"2026-06-16","記錄將個人電商（GShop）部署到 GKE Autopilot 的完整流程。","md","\u002Fimages\u002Fgke.jpg",{},{"title":14,"description":1595},"nwSAGWy6EGbowdd_9ERLH4cormr59zF4uBKDtY7Wdco",{"id":1602,"title":10,"author":1603,"body":1605,"date":3893,"description":3894,"extension":1596,"externalUrl":37,"image":3895,"meta":3896,"minRead":173,"navigation":41,"path":11,"seo":3897,"stem":12,"__hash__":3898},"blog\u002Farticles\u002Fdaily-snapshot.md",{"name":48,"avatar":1604},{"src":50,"alt":48},{"type":52,"value":1606,"toc":3878},[1607,1610,1613,1633,1647,1650,1652,1656,1662,1668,1670,1674,1677,1853,1855,1859,1862,3325,3328,3365,3367,3371,3496,3498,3501,3504,3575,3577,3581,3584,3589,3595,3598,3602,3605,3751,3757,3761,3778,3781,3785,3788,3790,3797,3799,3802,3852,3854,3857,3864,3875],[55,1608,1609],{"id":1609},"問題背景",[236,1611,1612],{},"訂單管理後台有一個總覽頁面，需要顯示以下統計數字：",[59,1614,1615,1618,1621,1624,1627,1630],{},[62,1616,1617],{},"當日營業額與訂單數",[62,1619,1620],{},"新增用戶數",[62,1622,1623],{},"平均客單價",[62,1625,1626],{},"熱銷商品 Top 10",[62,1628,1629],{},"各類別銷售佔比",[62,1631,1632],{},"各付款方式分佈",[236,1634,1635,1636,1639,1640,1639,1643,1646],{},"起初資料量小，直接對 ",[124,1637,1638],{},"orders","、",[124,1641,1642],{},"order_items",[124,1644,1645],{},"payments"," 做聚合查詢沒有問題。",[236,1648,1649],{},"隨著訂單累積，這些查詢開始拖慢整個頁面，每次載入需要數秒，而且每個進入後台的管理員都會觸發一次跨表掃描。",[106,1651],{},[55,1653,1655],{"id":1654},"解法daily-snapshot","解法：Daily Snapshot",[236,1657,1658,1659],{},"核心思路：",[65,1660,1661],{},"不在用戶請求時計算，改成每天固定時間預先算好，存進一張 Snapshot Table，查詢時直接讀一筆記錄。",[117,1663,1666],{"className":1664,"code":1665,"language":798},[796],"每天 00:00 Cron Job 執行\n      ↓\n讀取昨日訂單、商品、付款資料，執行聚合計算\n      ↓\n將結果寫入 daily_snapshots 表（一天一筆）\n      ↓\n前端請求總覽時，直接 SELECT 最新一筆 snapshot\n",[124,1667,1665],{"__ignoreMap":122},[106,1669],{},[55,1671,1673],{"id":1672},"snapshot-table-設計","Snapshot Table 設計",[236,1675,1676],{},"Snapshot 除了基本的金額與訂單數，還用 JSON 欄位儲存熱銷商品、類別、付款方式等排行資料：",[117,1678,1682],{"className":1679,"code":1680,"language":1681,"meta":122,"style":122},"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",[124,1683,1684,1689,1699,1721,1740,1759,1777,1793,1812,1830,1848],{"__ignoreMap":122},[127,1685,1686],{"class":129,"line":130},[127,1687,1688],{"class":133},"\u002F\u002F Sequelize Model\n",[127,1690,1691,1694,1696],{"class":129,"line":137},[127,1692,1693],{"class":154},"DailySnapshot ",[127,1695,814],{"class":274},[127,1697,1698],{"class":274}," {\n",[127,1700,1701,1704,1706,1709,1712,1715,1718],{"class":129,"line":158},[127,1702,1703],{"class":599},"  date",[127,1705,618],{"class":274},[127,1707,1708],{"class":154}," DataTypes",[127,1710,1711],{"class":274},".",[127,1713,1714],{"class":154},"DATEONLY",[127,1716,1717],{"class":274},",",[127,1719,1720],{"class":133}," \u002F\u002F 唯一鍵，一天一筆\n",[127,1722,1723,1726,1728,1730,1732,1735,1737],{"class":129,"line":167},[127,1724,1725],{"class":599},"  revenue",[127,1727,618],{"class":274},[127,1729,1708],{"class":154},[127,1731,1711],{"class":274},[127,1733,1734],{"class":154},"DECIMAL",[127,1736,1717],{"class":274},[127,1738,1739],{"class":133}," \u002F\u002F 當日營業額（paid\u002Fshipped\u002Fdelivered）\n",[127,1741,1742,1745,1747,1749,1751,1754,1756],{"class":129,"line":173},[127,1743,1744],{"class":599},"  orderCount",[127,1746,618],{"class":274},[127,1748,1708],{"class":154},[127,1750,1711],{"class":274},[127,1752,1753],{"class":154},"INTEGER",[127,1755,1717],{"class":274},[127,1757,1758],{"class":133}," \u002F\u002F 當日總訂單數\n",[127,1760,1761,1764,1766,1768,1770,1772,1774],{"class":129,"line":179},[127,1762,1763],{"class":599},"  newUserCount",[127,1765,618],{"class":274},[127,1767,1708],{"class":154},[127,1769,1711],{"class":274},[127,1771,1753],{"class":154},[127,1773,1717],{"class":274},[127,1775,1776],{"class":133}," \u002F\u002F 當日新增用戶數\n",[127,1778,1779,1782,1784,1786,1788,1790],{"class":129,"line":193},[127,1780,1781],{"class":599},"  avgOrderValue",[127,1783,618],{"class":274},[127,1785,1708],{"class":154},[127,1787,1711],{"class":274},[127,1789,1734],{"class":154},[127,1791,1792],{"class":274},",\n",[127,1794,1795,1798,1800,1802,1804,1807,1809],{"class":129,"line":201},[127,1796,1797],{"class":599},"  topProducts",[127,1799,618],{"class":274},[127,1801,1708],{"class":154},[127,1803,1711],{"class":274},[127,1805,1806],{"class":154},"JSON",[127,1808,1717],{"class":274},[127,1810,1811],{"class":133}," \u002F\u002F [{ productId, productName, totalRevenue, totalQuantity }]\n",[127,1813,1814,1817,1819,1821,1823,1825,1827],{"class":129,"line":206},[127,1815,1816],{"class":599},"  topCategories",[127,1818,618],{"class":274},[127,1820,1708],{"class":154},[127,1822,1711],{"class":274},[127,1824,1806],{"class":154},[127,1826,1717],{"class":274},[127,1828,1829],{"class":133}," \u002F\u002F [{ categoryId, categoryName, totalRevenue, totalQuantity }]\n",[127,1831,1832,1835,1837,1839,1841,1843,1845],{"class":129,"line":212},[127,1833,1834],{"class":599},"  paymentMethods",[127,1836,618],{"class":274},[127,1838,1708],{"class":154},[127,1840,1711],{"class":274},[127,1842,1806],{"class":154},[127,1844,1717],{"class":274},[127,1846,1847],{"class":133}," \u002F\u002F [{ method, count, amount }]\n",[127,1849,1850],{"class":129,"line":226},[127,1851,1852],{"class":274},"};\n",[106,1854],{},[55,1856,1858],{"id":1857},"builddailysnapshot-實作","buildDailySnapshot 實作",[236,1860,1861],{},"以下是實際的計算函式，對四張表同時發出查詢後一次 upsert 進 snapshot table：",[117,1863,1865],{"className":1679,"code":1864,"language":1681,"meta":122,"style":122},"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",[124,1866,1867,1888,1913,1917,1943,1948,1959,1967,1980,2001,2035,2044,2054,2058,2079,2100,2132,2154,2183,2187,2192,2231,2247,2252,2265,2270,2275,2281,2287,2293,2299,2307,2350,2358,2363,2369,2380,2385,2391,2397,2404,2439,2446,2451,2457,2468,2473,2479,2485,2491,2497,2503,2509,2516,2551,2558,2563,2569,2580,2585,2591,2596,2602,2608,2614,2620,2625,2631,2636,2643,2678,2685,2690,2696,2707,2712,2718,2724,2730,2736,2742,2749,2784,2791,2799,2804,2835,2865,2870,2876,2894,2906,2918,2945,2972,3024,3055,3065,3088,3111,3121,3149,3158,3179,3200,3209,3237,3255,3277,3300,3309,3319],{"__ignoreMap":122},[127,1868,1869,1872,1875,1878,1880,1883,1885],{"class":129,"line":130},[127,1870,1871],{"class":1340},"import",[127,1873,1874],{"class":154}," sequelize ",[127,1876,1877],{"class":1340},"from",[127,1879,838],{"class":274},[127,1881,1882],{"class":144},"..\u002Fconfig\u002Fdb.js",[127,1884,275],{"class":274},[127,1886,1887],{"class":274},";\n",[127,1889,1890,1892,1895,1898,1901,1904,1906,1909,1911],{"class":129,"line":137},[127,1891,1871],{"class":1340},[127,1893,1894],{"class":274}," {",[127,1896,1897],{"class":154}," DailySnapshot",[127,1899,1900],{"class":274}," }",[127,1902,1903],{"class":1340}," from",[127,1905,838],{"class":274},[127,1907,1908],{"class":144},"..\u002Fmodels\u002Findex.js",[127,1910,275],{"class":274},[127,1912,1887],{"class":274},[127,1914,1915],{"class":129,"line":158},[127,1916,170],{"emptyLinePlaceholder":41},[127,1918,1919,1922,1926,1929,1932,1935,1939,1941],{"class":129,"line":167},[127,1920,1921],{"class":1340},"export",[127,1923,1925],{"class":1924},"spNyl"," async",[127,1927,1928],{"class":1924}," function",[127,1930,1931],{"class":869}," buildDailySnapshot",[127,1933,1934],{"class":274},"(",[127,1936,1938],{"class":1937},"sHdIc","targetDate",[127,1940,885],{"class":274},[127,1942,1698],{"class":274},[127,1944,1945],{"class":129,"line":173},[127,1946,1947],{"class":133},"  \u002F\u002F 預設計算昨天\n",[127,1949,1950,1953,1956],{"class":129,"line":179},[127,1951,1952],{"class":1924},"  const",[127,1954,1955],{"class":154}," date",[127,1957,1958],{"class":274}," =\n",[127,1960,1961,1964],{"class":129,"line":193},[127,1962,1963],{"class":154},"    targetDate",[127,1965,1966],{"class":274}," ||\n",[127,1968,1969,1972,1975,1978],{"class":129,"line":201},[127,1970,1971],{"class":599},"    (",[127,1973,1974],{"class":274},"()",[127,1976,1977],{"class":1924}," =>",[127,1979,1698],{"class":274},[127,1981,1982,1985,1988,1991,1994,1997,1999],{"class":129,"line":206},[127,1983,1984],{"class":1924},"      const",[127,1986,1987],{"class":154}," d",[127,1989,1990],{"class":274}," =",[127,1992,1993],{"class":274}," new",[127,1995,1996],{"class":869}," Date",[127,1998,1974],{"class":599},[127,2000,1887],{"class":274},[127,2002,2003,2006,2008,2011,2013,2016,2018,2021,2024,2027,2031,2033],{"class":129,"line":212},[127,2004,2005],{"class":154},"      d",[127,2007,1711],{"class":274},[127,2009,2010],{"class":869},"setDate",[127,2012,1934],{"class":599},[127,2014,2015],{"class":154},"d",[127,2017,1711],{"class":274},[127,2019,2020],{"class":869},"getDate",[127,2022,2023],{"class":599},"() ",[127,2025,2026],{"class":274},"-",[127,2028,2030],{"class":2029},"sbssI"," 1",[127,2032,885],{"class":599},[127,2034,1887],{"class":274},[127,2036,2037,2040,2042],{"class":129,"line":226},[127,2038,2039],{"class":1340},"      return",[127,2041,1987],{"class":154},[127,2043,1887],{"class":274},[127,2045,2046,2049,2052],{"class":129,"line":1128},[127,2047,2048],{"class":274},"    }",[127,2050,2051],{"class":599},")()",[127,2053,1887],{"class":274},[127,2055,2056],{"class":129,"line":1147},[127,2057,170],{"emptyLinePlaceholder":41},[127,2059,2060,2062,2065,2067,2070,2072,2075,2077],{"class":129,"line":1275},[127,2061,1952],{"class":1924},[127,2063,2064],{"class":154}," dateStr",[127,2066,1990],{"class":274},[127,2068,2069],{"class":869}," toLocalDateStr",[127,2071,1934],{"class":599},[127,2073,2074],{"class":154},"date",[127,2076,885],{"class":599},[127,2078,1887],{"class":274},[127,2080,2081,2083,2086,2088,2090,2092,2094,2096,2098],{"class":129,"line":1283},[127,2082,1952],{"class":1924},[127,2084,2085],{"class":154}," start",[127,2087,1990],{"class":274},[127,2089,1993],{"class":274},[127,2091,1996],{"class":869},[127,2093,1934],{"class":599},[127,2095,2074],{"class":154},[127,2097,885],{"class":599},[127,2099,1887],{"class":274},[127,2101,2102,2105,2107,2110,2112,2115,2117,2120,2122,2124,2126,2128,2130],{"class":129,"line":1294},[127,2103,2104],{"class":154},"  start",[127,2106,1711],{"class":274},[127,2108,2109],{"class":869},"setHours",[127,2111,1934],{"class":599},[127,2113,2114],{"class":2029},"0",[127,2116,1717],{"class":274},[127,2118,2119],{"class":2029}," 0",[127,2121,1717],{"class":274},[127,2123,2119],{"class":2029},[127,2125,1717],{"class":274},[127,2127,2119],{"class":2029},[127,2129,885],{"class":599},[127,2131,1887],{"class":274},[127,2133,2134,2136,2139,2141,2143,2145,2147,2150,2152],{"class":129,"line":1306},[127,2135,1952],{"class":1924},[127,2137,2138],{"class":154}," end",[127,2140,1990],{"class":274},[127,2142,1993],{"class":274},[127,2144,1996],{"class":869},[127,2146,1934],{"class":599},[127,2148,2149],{"class":154},"start",[127,2151,885],{"class":599},[127,2153,1887],{"class":274},[127,2155,2156,2159,2161,2163,2165,2168,2170,2172,2174,2177,2179,2181],{"class":129,"line":1319},[127,2157,2158],{"class":154},"  end",[127,2160,1711],{"class":274},[127,2162,2010],{"class":869},[127,2164,1934],{"class":599},[127,2166,2167],{"class":154},"end",[127,2169,1711],{"class":274},[127,2171,2020],{"class":869},[127,2173,2023],{"class":599},[127,2175,2176],{"class":274},"+",[127,2178,2030],{"class":2029},[127,2180,885],{"class":599},[127,2182,1887],{"class":274},[127,2184,2185],{"class":129,"line":1332},[127,2186,170],{"emptyLinePlaceholder":41},[127,2188,2189],{"class":129,"line":1344},[127,2190,2191],{"class":133},"  \u002F\u002F 四支查詢並行執行，減少等待時間\n",[127,2193,2194,2196,2199,2202,2205,2208,2211,2213,2216,2218,2221,2223,2226,2229],{"class":129,"line":1350},[127,2195,1952],{"class":1924},[127,2197,2198],{"class":274}," [[",[127,2200,2201],{"class":154},"revenue",[127,2203,2204],{"class":274},"],",[127,2206,2207],{"class":274}," [",[127,2209,2210],{"class":154},"userRow",[127,2212,2204],{"class":274},[127,2214,2215],{"class":154}," topProducts",[127,2217,1717],{"class":274},[127,2219,2220],{"class":154}," topCategories",[127,2222,1717],{"class":274},[127,2224,2225],{"class":154}," paymentMethods",[127,2227,2228],{"class":274},"]",[127,2230,1958],{"class":274},[127,2232,2233,2236,2239,2241,2244],{"class":129,"line":1356},[127,2234,2235],{"class":1340},"    await",[127,2237,2238],{"class":140}," Promise",[127,2240,1711],{"class":274},[127,2242,2243],{"class":869},"all",[127,2245,2246],{"class":599},"([\n",[127,2248,2249],{"class":129,"line":1368},[127,2250,2251],{"class":133},"      \u002F\u002F 營業額、訂單數\n",[127,2253,2254,2257,2259,2262],{"class":129,"line":1375},[127,2255,2256],{"class":154},"      sequelize",[127,2258,1711],{"class":274},[127,2260,2261],{"class":869},"query",[127,2263,2264],{"class":599},"(\n",[127,2266,2267],{"class":129,"line":1386},[127,2268,2269],{"class":274},"        `\n",[127,2271,2272],{"class":129,"line":1397},[127,2273,2274],{"class":144},"        SELECT\n",[127,2276,2278],{"class":129,"line":2277},27,[127,2279,2280],{"class":144},"          COALESCE(SUM(total_amount) FILTER (WHERE status IN ('paid','shipped','delivered')), 0) AS revenue,\n",[127,2282,2284],{"class":129,"line":2283},28,[127,2285,2286],{"class":144},"          COUNT(*) FILTER (WHERE status IN ('paid','shipped','delivered')) AS paid_count,\n",[127,2288,2290],{"class":129,"line":2289},29,[127,2291,2292],{"class":144},"          COUNT(*) AS order_count\n",[127,2294,2296],{"class":129,"line":2295},30,[127,2297,2298],{"class":144},"        FROM orders WHERE created_at >= :start AND created_at \u003C :end\n",[127,2300,2302,2305],{"class":129,"line":2301},31,[127,2303,2304],{"class":274},"      `",[127,2306,1792],{"class":274},[127,2308,2310,2313,2316,2318,2320,2322,2324,2326,2329,2332,2334,2337,2339,2342,2344,2347],{"class":129,"line":2309},32,[127,2311,2312],{"class":274},"        {",[127,2314,2315],{"class":599}," replacements",[127,2317,618],{"class":274},[127,2319,1894],{"class":274},[127,2321,2085],{"class":154},[127,2323,1717],{"class":274},[127,2325,2138],{"class":154},[127,2327,2328],{"class":274}," },",[127,2330,2331],{"class":599}," type",[127,2333,618],{"class":274},[127,2335,2336],{"class":154}," sequelize",[127,2338,1711],{"class":274},[127,2340,2341],{"class":154},"QueryTypes",[127,2343,1711],{"class":274},[127,2345,2346],{"class":154},"SELECT",[127,2348,2349],{"class":274}," },\n",[127,2351,2353,2356],{"class":129,"line":2352},33,[127,2354,2355],{"class":599},"      )",[127,2357,1792],{"class":274},[127,2359,2361],{"class":129,"line":2360},34,[127,2362,170],{"emptyLinePlaceholder":41},[127,2364,2366],{"class":129,"line":2365},35,[127,2367,2368],{"class":133},"      \u002F\u002F 新增用戶數\n",[127,2370,2372,2374,2376,2378],{"class":129,"line":2371},36,[127,2373,2256],{"class":154},[127,2375,1711],{"class":274},[127,2377,2261],{"class":869},[127,2379,2264],{"class":599},[127,2381,2383],{"class":129,"line":2382},37,[127,2384,2269],{"class":274},[127,2386,2388],{"class":129,"line":2387},38,[127,2389,2390],{"class":144},"        SELECT COUNT(*) AS count FROM users\n",[127,2392,2394],{"class":129,"line":2393},39,[127,2395,2396],{"class":144},"        WHERE created_at >= :start AND created_at \u003C :end\n",[127,2398,2400,2402],{"class":129,"line":2399},40,[127,2401,2304],{"class":274},[127,2403,1792],{"class":274},[127,2405,2407,2409,2411,2413,2415,2417,2419,2421,2423,2425,2427,2429,2431,2433,2435,2437],{"class":129,"line":2406},41,[127,2408,2312],{"class":274},[127,2410,2315],{"class":599},[127,2412,618],{"class":274},[127,2414,1894],{"class":274},[127,2416,2085],{"class":154},[127,2418,1717],{"class":274},[127,2420,2138],{"class":154},[127,2422,2328],{"class":274},[127,2424,2331],{"class":599},[127,2426,618],{"class":274},[127,2428,2336],{"class":154},[127,2430,1711],{"class":274},[127,2432,2341],{"class":154},[127,2434,1711],{"class":274},[127,2436,2346],{"class":154},[127,2438,2349],{"class":274},[127,2440,2442,2444],{"class":129,"line":2441},42,[127,2443,2355],{"class":599},[127,2445,1792],{"class":274},[127,2447,2449],{"class":129,"line":2448},43,[127,2450,170],{"emptyLinePlaceholder":41},[127,2452,2454],{"class":129,"line":2453},44,[127,2455,2456],{"class":133},"      \u002F\u002F 熱銷商品 Top 10\n",[127,2458,2460,2462,2464,2466],{"class":129,"line":2459},45,[127,2461,2256],{"class":154},[127,2463,1711],{"class":274},[127,2465,2261],{"class":869},[127,2467,2264],{"class":599},[127,2469,2471],{"class":129,"line":2470},46,[127,2472,2269],{"class":274},[127,2474,2476],{"class":129,"line":2475},47,[127,2477,2478],{"class":144},"        SELECT oi.product_id AS \"productId\", oi.product_name AS \"productName\",\n",[127,2480,2482],{"class":129,"line":2481},48,[127,2483,2484],{"class":144},"               SUM(oi.subtotal) AS \"totalRevenue\", SUM(oi.quantity) AS \"totalQuantity\"\n",[127,2486,2488],{"class":129,"line":2487},49,[127,2489,2490],{"class":144},"        FROM order_items oi JOIN orders o ON o.id = oi.order_id\n",[127,2492,2494],{"class":129,"line":2493},50,[127,2495,2496],{"class":144},"        WHERE o.created_at >= :start AND o.created_at \u003C :end AND o.status != 'cancelled'\n",[127,2498,2500],{"class":129,"line":2499},51,[127,2501,2502],{"class":144},"        GROUP BY oi.product_id, oi.product_name\n",[127,2504,2506],{"class":129,"line":2505},52,[127,2507,2508],{"class":144},"        ORDER BY \"totalRevenue\" DESC LIMIT 10\n",[127,2510,2512,2514],{"class":129,"line":2511},53,[127,2513,2304],{"class":274},[127,2515,1792],{"class":274},[127,2517,2519,2521,2523,2525,2527,2529,2531,2533,2535,2537,2539,2541,2543,2545,2547,2549],{"class":129,"line":2518},54,[127,2520,2312],{"class":274},[127,2522,2315],{"class":599},[127,2524,618],{"class":274},[127,2526,1894],{"class":274},[127,2528,2085],{"class":154},[127,2530,1717],{"class":274},[127,2532,2138],{"class":154},[127,2534,2328],{"class":274},[127,2536,2331],{"class":599},[127,2538,618],{"class":274},[127,2540,2336],{"class":154},[127,2542,1711],{"class":274},[127,2544,2341],{"class":154},[127,2546,1711],{"class":274},[127,2548,2346],{"class":154},[127,2550,2349],{"class":274},[127,2552,2554,2556],{"class":129,"line":2553},55,[127,2555,2355],{"class":599},[127,2557,1792],{"class":274},[127,2559,2561],{"class":129,"line":2560},56,[127,2562,170],{"emptyLinePlaceholder":41},[127,2564,2566],{"class":129,"line":2565},57,[127,2567,2568],{"class":133},"      \u002F\u002F 各類別銷售 Top 10\n",[127,2570,2572,2574,2576,2578],{"class":129,"line":2571},58,[127,2573,2256],{"class":154},[127,2575,1711],{"class":274},[127,2577,2261],{"class":869},[127,2579,2264],{"class":599},[127,2581,2583],{"class":129,"line":2582},59,[127,2584,2269],{"class":274},[127,2586,2588],{"class":129,"line":2587},60,[127,2589,2590],{"class":144},"        SELECT p.category_id AS \"categoryId\", c.name AS \"categoryName\",\n",[127,2592,2594],{"class":129,"line":2593},61,[127,2595,2484],{"class":144},[127,2597,2599],{"class":129,"line":2598},62,[127,2600,2601],{"class":144},"        FROM order_items oi\n",[127,2603,2605],{"class":129,"line":2604},63,[127,2606,2607],{"class":144},"        JOIN orders o ON o.id = oi.order_id\n",[127,2609,2611],{"class":129,"line":2610},64,[127,2612,2613],{"class":144},"        JOIN products p ON p.id = oi.product_id\n",[127,2615,2617],{"class":129,"line":2616},65,[127,2618,2619],{"class":144},"        JOIN categories c ON c.id = p.category_id\n",[127,2621,2623],{"class":129,"line":2622},66,[127,2624,2496],{"class":144},[127,2626,2628],{"class":129,"line":2627},67,[127,2629,2630],{"class":144},"        GROUP BY p.category_id, c.name\n",[127,2632,2634],{"class":129,"line":2633},68,[127,2635,2508],{"class":144},[127,2637,2639,2641],{"class":129,"line":2638},69,[127,2640,2304],{"class":274},[127,2642,1792],{"class":274},[127,2644,2646,2648,2650,2652,2654,2656,2658,2660,2662,2664,2666,2668,2670,2672,2674,2676],{"class":129,"line":2645},70,[127,2647,2312],{"class":274},[127,2649,2315],{"class":599},[127,2651,618],{"class":274},[127,2653,1894],{"class":274},[127,2655,2085],{"class":154},[127,2657,1717],{"class":274},[127,2659,2138],{"class":154},[127,2661,2328],{"class":274},[127,2663,2331],{"class":599},[127,2665,618],{"class":274},[127,2667,2336],{"class":154},[127,2669,1711],{"class":274},[127,2671,2341],{"class":154},[127,2673,1711],{"class":274},[127,2675,2346],{"class":154},[127,2677,2349],{"class":274},[127,2679,2681,2683],{"class":129,"line":2680},71,[127,2682,2355],{"class":599},[127,2684,1792],{"class":274},[127,2686,2688],{"class":129,"line":2687},72,[127,2689,170],{"emptyLinePlaceholder":41},[127,2691,2693],{"class":129,"line":2692},73,[127,2694,2695],{"class":133},"      \u002F\u002F 付款方式分佈\n",[127,2697,2699,2701,2703,2705],{"class":129,"line":2698},74,[127,2700,2256],{"class":154},[127,2702,1711],{"class":274},[127,2704,2261],{"class":869},[127,2706,2264],{"class":599},[127,2708,2710],{"class":129,"line":2709},75,[127,2711,2269],{"class":274},[127,2713,2715],{"class":129,"line":2714},76,[127,2716,2717],{"class":144},"        SELECT pay.method, COUNT(*) AS count, SUM(pay.amount) AS amount\n",[127,2719,2721],{"class":129,"line":2720},77,[127,2722,2723],{"class":144},"        FROM payments pay JOIN orders o ON o.id = pay.order_id\n",[127,2725,2727],{"class":129,"line":2726},78,[127,2728,2729],{"class":144},"        WHERE o.created_at >= :start AND o.created_at \u003C :end\n",[127,2731,2733],{"class":129,"line":2732},79,[127,2734,2735],{"class":144},"          AND o.status IN ('paid','shipped','delivered')\n",[127,2737,2739],{"class":129,"line":2738},80,[127,2740,2741],{"class":144},"        GROUP BY pay.method\n",[127,2743,2745,2747],{"class":129,"line":2744},81,[127,2746,2304],{"class":274},[127,2748,1792],{"class":274},[127,2750,2752,2754,2756,2758,2760,2762,2764,2766,2768,2770,2772,2774,2776,2778,2780,2782],{"class":129,"line":2751},82,[127,2753,2312],{"class":274},[127,2755,2315],{"class":599},[127,2757,618],{"class":274},[127,2759,1894],{"class":274},[127,2761,2085],{"class":154},[127,2763,1717],{"class":274},[127,2765,2138],{"class":154},[127,2767,2328],{"class":274},[127,2769,2331],{"class":599},[127,2771,618],{"class":274},[127,2773,2336],{"class":154},[127,2775,1711],{"class":274},[127,2777,2341],{"class":154},[127,2779,1711],{"class":274},[127,2781,2346],{"class":154},[127,2783,2349],{"class":274},[127,2785,2787,2789],{"class":129,"line":2786},83,[127,2788,2355],{"class":599},[127,2790,1792],{"class":274},[127,2792,2794,2797],{"class":129,"line":2793},84,[127,2795,2796],{"class":599},"    ])",[127,2798,1887],{"class":274},[127,2800,2802],{"class":129,"line":2801},85,[127,2803,170],{"emptyLinePlaceholder":41},[127,2805,2807,2809,2812,2814,2817,2819,2821,2823,2825,2828,2831,2833],{"class":129,"line":2806},86,[127,2808,1952],{"class":1924},[127,2810,2811],{"class":154}," rev",[127,2813,1990],{"class":274},[127,2815,2816],{"class":869}," parseFloat",[127,2818,1934],{"class":599},[127,2820,2201],{"class":154},[127,2822,1711],{"class":274},[127,2824,2201],{"class":154},[127,2826,2827],{"class":599},") ",[127,2829,2830],{"class":274},"||",[127,2832,2119],{"class":2029},[127,2834,1887],{"class":274},[127,2836,2838,2840,2843,2845,2848,2850,2852,2854,2857,2859,2861,2863],{"class":129,"line":2837},87,[127,2839,1952],{"class":1924},[127,2841,2842],{"class":154}," paidCount",[127,2844,1990],{"class":274},[127,2846,2847],{"class":869}," parseInt",[127,2849,1934],{"class":599},[127,2851,2201],{"class":154},[127,2853,1711],{"class":274},[127,2855,2856],{"class":154},"paid_count",[127,2858,2827],{"class":599},[127,2860,2830],{"class":274},[127,2862,2119],{"class":2029},[127,2864,1887],{"class":274},[127,2866,2868],{"class":129,"line":2867},88,[127,2869,170],{"emptyLinePlaceholder":41},[127,2871,2873],{"class":129,"line":2872},89,[127,2874,2875],{"class":133},"  \u002F\u002F upsert：重跑不會產生重複資料\n",[127,2877,2879,2882,2884,2886,2889,2891],{"class":129,"line":2878},90,[127,2880,2881],{"class":1340},"  await",[127,2883,1897],{"class":154},[127,2885,1711],{"class":274},[127,2887,2888],{"class":869},"upsert",[127,2890,1934],{"class":599},[127,2892,2893],{"class":274},"{\n",[127,2895,2897,2900,2902,2904],{"class":129,"line":2896},91,[127,2898,2899],{"class":599},"    date",[127,2901,618],{"class":274},[127,2903,2064],{"class":154},[127,2905,1792],{"class":274},[127,2907,2909,2912,2914,2916],{"class":129,"line":2908},92,[127,2910,2911],{"class":599},"    revenue",[127,2913,618],{"class":274},[127,2915,2811],{"class":154},[127,2917,1792],{"class":274},[127,2919,2921,2924,2926,2928,2930,2932,2934,2937,2939,2941,2943],{"class":129,"line":2920},93,[127,2922,2923],{"class":599},"    orderCount",[127,2925,618],{"class":274},[127,2927,2847],{"class":869},[127,2929,1934],{"class":599},[127,2931,2201],{"class":154},[127,2933,1711],{"class":274},[127,2935,2936],{"class":154},"order_count",[127,2938,2827],{"class":599},[127,2940,2830],{"class":274},[127,2942,2119],{"class":2029},[127,2944,1792],{"class":274},[127,2946,2948,2951,2953,2955,2957,2959,2961,2964,2966,2968,2970],{"class":129,"line":2947},94,[127,2949,2950],{"class":599},"    newUserCount",[127,2952,618],{"class":274},[127,2954,2847],{"class":869},[127,2956,1934],{"class":599},[127,2958,2210],{"class":154},[127,2960,1711],{"class":274},[127,2962,2963],{"class":154},"count",[127,2965,2827],{"class":599},[127,2967,2830],{"class":274},[127,2969,2119],{"class":2029},[127,2971,1792],{"class":274},[127,2973,2975,2978,2980,2982,2985,2987,2990,2992,2995,2998,3001,3003,3005,3007,3010,3012,3015,3018,3020,3022],{"class":129,"line":2974},95,[127,2976,2977],{"class":599},"    avgOrderValue",[127,2979,618],{"class":274},[127,2981,2842],{"class":154},[127,2983,2984],{"class":274}," >",[127,2986,2119],{"class":2029},[127,2988,2989],{"class":274}," ?",[127,2991,2816],{"class":869},[127,2993,2994],{"class":599},"((",[127,2996,2997],{"class":154},"rev",[127,2999,3000],{"class":274}," \u002F",[127,3002,2842],{"class":154},[127,3004,885],{"class":599},[127,3006,1711],{"class":274},[127,3008,3009],{"class":869},"toFixed",[127,3011,1934],{"class":599},[127,3013,3014],{"class":2029},"2",[127,3016,3017],{"class":599},")) ",[127,3019,618],{"class":274},[127,3021,2119],{"class":2029},[127,3023,1792],{"class":274},[127,3025,3027,3030,3032,3034,3036,3039,3041,3043,3046,3048,3050,3053],{"class":129,"line":3026},96,[127,3028,3029],{"class":599},"    topProducts",[127,3031,618],{"class":274},[127,3033,2215],{"class":154},[127,3035,1711],{"class":274},[127,3037,3038],{"class":869},"map",[127,3040,1934],{"class":599},[127,3042,1934],{"class":274},[127,3044,3045],{"class":1937},"r",[127,3047,885],{"class":274},[127,3049,1977],{"class":1924},[127,3051,3052],{"class":599}," (",[127,3054,2893],{"class":274},[127,3056,3058,3061,3063],{"class":129,"line":3057},97,[127,3059,3060],{"class":274},"      ...",[127,3062,3045],{"class":154},[127,3064,1792],{"class":274},[127,3066,3068,3071,3073,3075,3077,3079,3081,3084,3086],{"class":129,"line":3067},98,[127,3069,3070],{"class":599},"      totalRevenue",[127,3072,618],{"class":274},[127,3074,2816],{"class":869},[127,3076,1934],{"class":599},[127,3078,3045],{"class":154},[127,3080,1711],{"class":274},[127,3082,3083],{"class":154},"totalRevenue",[127,3085,885],{"class":599},[127,3087,1792],{"class":274},[127,3089,3091,3094,3096,3098,3100,3102,3104,3107,3109],{"class":129,"line":3090},99,[127,3092,3093],{"class":599},"      totalQuantity",[127,3095,618],{"class":274},[127,3097,2847],{"class":869},[127,3099,1934],{"class":599},[127,3101,3045],{"class":154},[127,3103,1711],{"class":274},[127,3105,3106],{"class":154},"totalQuantity",[127,3108,885],{"class":599},[127,3110,1792],{"class":274},[127,3112,3114,3116,3119],{"class":129,"line":3113},100,[127,3115,2048],{"class":274},[127,3117,3118],{"class":599},"))",[127,3120,1792],{"class":274},[127,3122,3124,3127,3129,3131,3133,3135,3137,3139,3141,3143,3145,3147],{"class":129,"line":3123},101,[127,3125,3126],{"class":599},"    topCategories",[127,3128,618],{"class":274},[127,3130,2220],{"class":154},[127,3132,1711],{"class":274},[127,3134,3038],{"class":869},[127,3136,1934],{"class":599},[127,3138,1934],{"class":274},[127,3140,3045],{"class":1937},[127,3142,885],{"class":274},[127,3144,1977],{"class":1924},[127,3146,3052],{"class":599},[127,3148,2893],{"class":274},[127,3150,3152,3154,3156],{"class":129,"line":3151},102,[127,3153,3060],{"class":274},[127,3155,3045],{"class":154},[127,3157,1792],{"class":274},[127,3159,3161,3163,3165,3167,3169,3171,3173,3175,3177],{"class":129,"line":3160},103,[127,3162,3070],{"class":599},[127,3164,618],{"class":274},[127,3166,2816],{"class":869},[127,3168,1934],{"class":599},[127,3170,3045],{"class":154},[127,3172,1711],{"class":274},[127,3174,3083],{"class":154},[127,3176,885],{"class":599},[127,3178,1792],{"class":274},[127,3180,3182,3184,3186,3188,3190,3192,3194,3196,3198],{"class":129,"line":3181},104,[127,3183,3093],{"class":599},[127,3185,618],{"class":274},[127,3187,2847],{"class":869},[127,3189,1934],{"class":599},[127,3191,3045],{"class":154},[127,3193,1711],{"class":274},[127,3195,3106],{"class":154},[127,3197,885],{"class":599},[127,3199,1792],{"class":274},[127,3201,3203,3205,3207],{"class":129,"line":3202},105,[127,3204,2048],{"class":274},[127,3206,3118],{"class":599},[127,3208,1792],{"class":274},[127,3210,3212,3215,3217,3219,3221,3223,3225,3227,3229,3231,3233,3235],{"class":129,"line":3211},106,[127,3213,3214],{"class":599},"    paymentMethods",[127,3216,618],{"class":274},[127,3218,2225],{"class":154},[127,3220,1711],{"class":274},[127,3222,3038],{"class":869},[127,3224,1934],{"class":599},[127,3226,1934],{"class":274},[127,3228,3045],{"class":1937},[127,3230,885],{"class":274},[127,3232,1977],{"class":1924},[127,3234,3052],{"class":599},[127,3236,2893],{"class":274},[127,3238,3240,3243,3245,3248,3250,3253],{"class":129,"line":3239},107,[127,3241,3242],{"class":599},"      method",[127,3244,618],{"class":274},[127,3246,3247],{"class":154}," r",[127,3249,1711],{"class":274},[127,3251,3252],{"class":154},"method",[127,3254,1792],{"class":274},[127,3256,3258,3261,3263,3265,3267,3269,3271,3273,3275],{"class":129,"line":3257},108,[127,3259,3260],{"class":599},"      count",[127,3262,618],{"class":274},[127,3264,2847],{"class":869},[127,3266,1934],{"class":599},[127,3268,3045],{"class":154},[127,3270,1711],{"class":274},[127,3272,2963],{"class":154},[127,3274,885],{"class":599},[127,3276,1792],{"class":274},[127,3278,3280,3283,3285,3287,3289,3291,3293,3296,3298],{"class":129,"line":3279},109,[127,3281,3282],{"class":599},"      amount",[127,3284,618],{"class":274},[127,3286,2816],{"class":869},[127,3288,1934],{"class":599},[127,3290,3045],{"class":154},[127,3292,1711],{"class":274},[127,3294,3295],{"class":154},"amount",[127,3297,885],{"class":599},[127,3299,1792],{"class":274},[127,3301,3303,3305,3307],{"class":129,"line":3302},110,[127,3304,2048],{"class":274},[127,3306,3118],{"class":599},[127,3308,1792],{"class":274},[127,3310,3312,3315,3317],{"class":129,"line":3311},111,[127,3313,3314],{"class":274},"  }",[127,3316,885],{"class":599},[127,3318,1887],{"class":274},[127,3320,3322],{"class":129,"line":3321},112,[127,3323,3324],{"class":274},"}\n",[236,3326,3327],{},"幾個值得注意的設計細節：",[59,3329,3330,3338,3346,3353],{},[62,3331,3332,3337],{},[65,3333,3334],{},[124,3335,3336],{},"Promise.all","：四支查詢並行發出，不等前一支結束才跑下一支",[62,3339,3340,3345],{},[65,3341,3342],{},[124,3343,3344],{},"FILTER (WHERE status IN (...))","：只統計有效訂單的營業額，排除取消訂單",[62,3347,3348,3352],{},[65,3349,3350],{},[124,3351,2888],{},"：Cron Job 重跑（例如補跑失敗的日期）時不會產生重複記錄",[62,3354,3355,3360,3361,3364],{},[65,3356,3357],{},[124,3358,3359],{},"toLocalDateStr","：手動格式化本地日期，避免 ",[124,3362,3363],{},"toISOString()"," 因時區偏移導致日期錯誤",[106,3366],{},[55,3368,3370],{"id":3369},"cron-job-排程","Cron Job 排程",[117,3372,3374],{"className":1679,"code":3373,"language":1681,"meta":122,"style":122},"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",[124,3375,3376,3394,3415,3419,3424,3454,3464,3487],{"__ignoreMap":122},[127,3377,3378,3380,3383,3385,3387,3390,3392],{"class":129,"line":130},[127,3379,1871],{"class":1340},[127,3381,3382],{"class":154}," cron ",[127,3384,1877],{"class":1340},[127,3386,838],{"class":274},[127,3388,3389],{"class":144},"node-cron",[127,3391,275],{"class":274},[127,3393,1887],{"class":274},[127,3395,3396,3398,3400,3402,3404,3406,3408,3411,3413],{"class":129,"line":137},[127,3397,1871],{"class":1340},[127,3399,1894],{"class":274},[127,3401,1931],{"class":154},[127,3403,1900],{"class":274},[127,3405,1903],{"class":1340},[127,3407,838],{"class":274},[127,3409,3410],{"class":144},".\u002Fjobs\u002FdailySnapshot.js",[127,3412,275],{"class":274},[127,3414,1887],{"class":274},[127,3416,3417],{"class":129,"line":158},[127,3418,170],{"emptyLinePlaceholder":41},[127,3420,3421],{"class":129,"line":167},[127,3422,3423],{"class":133},"\u002F\u002F 每天凌晨 00:05 執行（留 5 分鐘緩衝確保跨日資料落庫）\n",[127,3425,3426,3429,3431,3434,3436,3438,3441,3443,3445,3447,3450,3452],{"class":129,"line":173},[127,3427,3428],{"class":154},"cron",[127,3430,1711],{"class":274},[127,3432,3433],{"class":869},"schedule",[127,3435,1934],{"class":154},[127,3437,275],{"class":274},[127,3439,3440],{"class":144},"5 0 * * *",[127,3442,275],{"class":274},[127,3444,1717],{"class":274},[127,3446,1925],{"class":1924},[127,3448,3449],{"class":274}," ()",[127,3451,1977],{"class":1924},[127,3453,1698],{"class":274},[127,3455,3456,3458,3460,3462],{"class":129,"line":179},[127,3457,2881],{"class":1340},[127,3459,1931],{"class":869},[127,3461,1974],{"class":599},[127,3463,1887],{"class":274},[127,3465,3466,3469,3471,3474,3476,3478,3481,3483,3485],{"class":129,"line":193},[127,3467,3468],{"class":154},"  console",[127,3470,1711],{"class":274},[127,3472,3473],{"class":869},"log",[127,3475,1934],{"class":599},[127,3477,275],{"class":274},[127,3479,3480],{"class":144},"[Snapshot] 昨日 snapshot 建立完成",[127,3482,275],{"class":274},[127,3484,885],{"class":599},[127,3486,1887],{"class":274},[127,3488,3489,3492,3494],{"class":129,"line":201},[127,3490,3491],{"class":274},"}",[127,3493,885],{"class":154},[127,3495,1887],{"class":274},[106,3497],{},[55,3499,3500],{"id":3500},"查詢方式",[236,3502,3503],{},"總覽 API 改成直接讀 snapshot，不再碰原始資料表：",[117,3505,3507],{"className":1679,"code":3506,"language":1681,"meta":122,"style":122},"\u002F\u002F ✅ 讀最新一筆 snapshot，毫秒級回應\nconst snapshot = await DailySnapshot.findOne({\n  order: [[\"date\", \"DESC\"]],\n});\n",[124,3508,3509,3514,3538,3567],{"__ignoreMap":122},[127,3510,3511],{"class":129,"line":130},[127,3512,3513],{"class":133},"\u002F\u002F ✅ 讀最新一筆 snapshot，毫秒級回應\n",[127,3515,3516,3519,3522,3524,3527,3529,3531,3534,3536],{"class":129,"line":137},[127,3517,3518],{"class":1924},"const",[127,3520,3521],{"class":154}," snapshot ",[127,3523,814],{"class":274},[127,3525,3526],{"class":1340}," await",[127,3528,1897],{"class":154},[127,3530,1711],{"class":274},[127,3532,3533],{"class":869},"findOne",[127,3535,1934],{"class":154},[127,3537,2893],{"class":274},[127,3539,3540,3543,3545,3547,3549,3551,3553,3555,3557,3560,3562,3565],{"class":129,"line":158},[127,3541,3542],{"class":599},"  order",[127,3544,618],{"class":274},[127,3546,2198],{"class":154},[127,3548,275],{"class":274},[127,3550,2074],{"class":144},[127,3552,275],{"class":274},[127,3554,1717],{"class":274},[127,3556,838],{"class":274},[127,3558,3559],{"class":144},"DESC",[127,3561,275],{"class":274},[127,3563,3564],{"class":154},"]]",[127,3566,1792],{"class":274},[127,3568,3569,3571,3573],{"class":129,"line":167},[127,3570,3491],{"class":274},[127,3572,885],{"class":154},[127,3574,1887],{"class":274},[106,3576],{},[55,3578,3580],{"id":3579},"訂單狀態會變動怎麼辦","訂單狀態會變動怎麼辦？",[236,3582,3583],{},"Snapshot 是某個時間點的快照，但訂單狀態會在那之後繼續變動，這是使用這個模式必須正視的問題。",[236,3585,3586],{},[65,3587,3588],{},"舉個例子：",[117,3590,3593],{"className":3591,"code":3592,"language":798},[796],"23:50  訂單建立，狀態 pending\n00:05  Cron Job 跑完 snapshot，這筆訂單未被計入營業額\n09:00  用戶付款，狀態變 paid\n",[124,3594,3592],{"__ignoreMap":122},[236,3596,3597],{},"昨天的 snapshot 永遠不會包含這筆訂單，數字就是錯的。",[112,3599,3601],{"id":3600},"解法一補跑近幾天的-snapshot","解法一：補跑近幾天的 Snapshot",[236,3603,3604],{},"每天除了算昨天，也重算過去 N 天，讓狀態更新能被追上：",[117,3606,3608],{"className":1679,"code":3607,"language":1681,"meta":122,"style":122},"\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",[124,3609,3610,3615,3641,3680,3697,3724,3738,3743],{"__ignoreMap":122},[127,3611,3612],{"class":129,"line":130},[127,3613,3614],{"class":133},"\u002F\u002F 每天重算最近 7 天\n",[127,3616,3617,3619,3621,3623,3625,3627,3629,3631,3633,3635,3637,3639],{"class":129,"line":137},[127,3618,3428],{"class":154},[127,3620,1711],{"class":274},[127,3622,3433],{"class":869},[127,3624,1934],{"class":154},[127,3626,275],{"class":274},[127,3628,3440],{"class":144},[127,3630,275],{"class":274},[127,3632,1717],{"class":274},[127,3634,1925],{"class":1924},[127,3636,3449],{"class":274},[127,3638,1977],{"class":1924},[127,3640,1698],{"class":274},[127,3642,3643,3646,3648,3651,3654,3656,3658,3661,3663,3666,3669,3671,3673,3676,3678],{"class":129,"line":158},[127,3644,3645],{"class":1340},"  for",[127,3647,3052],{"class":599},[127,3649,3650],{"class":1924},"let",[127,3652,3653],{"class":154}," i",[127,3655,1990],{"class":274},[127,3657,2030],{"class":2029},[127,3659,3660],{"class":274},";",[127,3662,3653],{"class":154},[127,3664,3665],{"class":274}," \u003C=",[127,3667,3668],{"class":2029}," 7",[127,3670,3660],{"class":274},[127,3672,3653],{"class":154},[127,3674,3675],{"class":274},"++",[127,3677,2827],{"class":599},[127,3679,2893],{"class":274},[127,3681,3682,3685,3687,3689,3691,3693,3695],{"class":129,"line":167},[127,3683,3684],{"class":1924},"    const",[127,3686,1987],{"class":154},[127,3688,1990],{"class":274},[127,3690,1993],{"class":274},[127,3692,1996],{"class":869},[127,3694,1974],{"class":599},[127,3696,1887],{"class":274},[127,3698,3699,3702,3704,3706,3708,3710,3712,3714,3716,3718,3720,3722],{"class":129,"line":173},[127,3700,3701],{"class":154},"    d",[127,3703,1711],{"class":274},[127,3705,2010],{"class":869},[127,3707,1934],{"class":599},[127,3709,2015],{"class":154},[127,3711,1711],{"class":274},[127,3713,2020],{"class":869},[127,3715,2023],{"class":599},[127,3717,2026],{"class":274},[127,3719,3653],{"class":154},[127,3721,885],{"class":599},[127,3723,1887],{"class":274},[127,3725,3726,3728,3730,3732,3734,3736],{"class":129,"line":179},[127,3727,2235],{"class":1340},[127,3729,1931],{"class":869},[127,3731,1934],{"class":599},[127,3733,2015],{"class":154},[127,3735,885],{"class":599},[127,3737,1887],{"class":274},[127,3739,3740],{"class":129,"line":193},[127,3741,3742],{"class":274},"  }\n",[127,3744,3745,3747,3749],{"class":129,"line":201},[127,3746,3491],{"class":274},[127,3748,885],{"class":154},[127,3750,1887],{"class":274},[236,3752,3753,3754,3756],{},"適合狀態變動集中在近期（例如大多數訂單在 3 天內完成付款）的情境。",[124,3755,2888],{}," 的設計讓補跑變得安全，不會產生重複資料。",[112,3758,3760],{"id":3759},"解法二只快照終態訂單","解法二：只快照終態訂單",[236,3762,3763,3764,1639,3767,3770,3771,1639,3774,3777],{},"只把 ",[124,3765,3766],{},"delivered",[124,3768,3769],{},"cancelled"," 這類不會再變動的訂單算進 snapshot，",[124,3772,3773],{},"paid",[124,3775,3776],{},"shipped"," 等還在流動中的訂單留給即時查詢。",[236,3779,3780],{},"代價是 snapshot 數字會比實際交易日期滯後幾天，但每一筆都是確定的終態數字。",[112,3782,3784],{"id":3783},"解法三接受誤差","解法三：接受誤差",[236,3786,3787],{},"如果這個總覽是給內部看的管理報表，T+1 有些許誤差通常可以接受。業務決策不需要精確到每一筆，數字的趨勢比精確值更重要。",[106,3789],{},[236,3791,3792,3793,3796],{},"目前採用的是",[65,3794,3795],{},"解法一","，每天重算最近 7 天，在資料準確性與實作複雜度之間取得平衡。",[106,3798],{},[55,3800,3801],{"id":3801},"效果對比",[920,3803,3804,3817],{},[923,3805,3806],{},[926,3807,3808,3811,3814],{},[929,3809,3810],{},"指標",[929,3812,3813],{},"改善前",[929,3815,3816],{},"改善後",[936,3818,3819,3830,3841],{},[926,3820,3821,3824,3827],{},[941,3822,3823],{},"查詢時間",[941,3825,3826],{},"數秒",[941,3828,3829],{},"\u003C 10ms",[926,3831,3832,3835,3838],{},[941,3833,3834],{},"資料庫負載",[941,3836,3837],{},"每次請求跨表掃描",[941,3839,3840],{},"每日一次聚合",[926,3842,3843,3846,3849],{},[941,3844,3845],{},"資料即時性",[941,3847,3848],{},"即時",[941,3850,3851],{},"前一天結算準確",[106,3853],{},[55,3855,3856],{"id":3856},"適用場景",[236,3858,3859,3860,3863],{},"這個模式適合",[65,3861,3862],{},"讀多寫少、資料量大、對即時性要求不高","的統計需求，例如：",[59,3865,3866,3869,3872],{},[62,3867,3868],{},"管理後台的訂單、用戶、收入總覽",[62,3870,3871],{},"報表系統的歷史趨勢圖",[62,3873,3874],{},"定期推播給管理員的每日摘要",[1564,3876,3877],{},"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":122,"searchDepth":137,"depth":137,"links":3879},[3880,3881,3882,3883,3884,3885,3886,3891,3892],{"id":1609,"depth":137,"text":1609},{"id":1654,"depth":137,"text":1655},{"id":1672,"depth":137,"text":1673},{"id":1857,"depth":137,"text":1858},{"id":3369,"depth":137,"text":3370},{"id":3500,"depth":137,"text":3500},{"id":3579,"depth":137,"text":3580,"children":3887},[3888,3889,3890],{"id":3600,"depth":158,"text":3601},{"id":3759,"depth":158,"text":3760},{"id":3783,"depth":158,"text":3784},{"id":3801,"depth":137,"text":3801},{"id":3856,"depth":137,"text":3856},"2026-03-10","資料量龐大導致查詢變慢，透過 Cron Job 將每日統計好的資料寫入 Snapshot Table，查詢不再需要跨表掃描改為單表讀取。","\u002Fimages\u002Fdaily-snapshot.jpg",{},{"title":10,"description":3894},"wWf7247tjW3NLW0rgckerIiTQ17DpIh6yqR3V0OJ3lE",{"id":3900,"title":22,"author":3901,"body":3903,"date":5066,"description":5067,"extension":1596,"externalUrl":37,"image":5068,"meta":5069,"minRead":201,"navigation":41,"path":23,"seo":5070,"stem":24,"__hash__":5071},"blog\u002Farticles\u002Fsecurity-best-practices.md",{"name":48,"avatar":3902},{"src":50,"alt":48},{"type":52,"value":3904,"toc":5043},[3905,3908,3912,3915,4017,4022,4041,4045,4048,4079,4081,4084,4088,4091,4173,4177,4180,4230,4235,4241,4244,4300,4302,4306,4309,4354,4356,4359,4362,4399,4404,4419,4421,4425,4429,4432,4567,4571,4578,4683,4685,4688,4692,4788,4792,4795,4834,4836,4839,4842,4962,4967,4978,4980,4984,5037,5040],[55,3906,3907],{"id":3907},"身份驗證與授權",[112,3909,3911],{"id":3910},"使用-jwt-的注意事項","使用 JWT 的注意事項",[236,3913,3914],{},"JWT（JSON Web Token）是常見的無狀態驗證方案，但實作細節很容易踩坑。",[117,3916,3920],{"className":3917,"code":3918,"language":3919,"meta":122,"style":122},"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",[124,3921,3922,3927,3971,3975,3980],{"__ignoreMap":122},[127,3923,3924],{"class":129,"line":130},[127,3925,3926],{"class":133},"\u002F\u002F ❌ 錯誤：演算法設為 none，任何人都能偽造 token\n",[127,3928,3929,3932,3934,3937,3940,3942,3944,3946,3948,3951,3953,3955,3958,3961,3963,3966,3968],{"class":129,"line":137},[127,3930,3931],{"class":154},"jwt",[127,3933,1711],{"class":274},[127,3935,3936],{"class":869},"verify",[127,3938,3939],{"class":154},"(token",[127,3941,1717],{"class":274},[127,3943,258],{"class":154},[127,3945,1717],{"class":274},[127,3947,1894],{"class":274},[127,3949,3950],{"class":599}," algorithms",[127,3952,618],{"class":274},[127,3954,2207],{"class":154},[127,3956,3957],{"class":274},"'",[127,3959,3960],{"class":144},"none",[127,3962,3957],{"class":274},[127,3964,3965],{"class":154},"] ",[127,3967,3491],{"class":274},[127,3969,3970],{"class":154},")\n",[127,3972,3973],{"class":129,"line":158},[127,3974,170],{"emptyLinePlaceholder":41},[127,3976,3977],{"class":129,"line":167},[127,3978,3979],{"class":133},"\u002F\u002F ✅ 正確：明確指定演算法\n",[127,3981,3982,3984,3986,3988,3990,3992,3994,3996,3998,4000,4002,4004,4006,4009,4011,4013,4015],{"class":129,"line":173},[127,3983,3931],{"class":154},[127,3985,1711],{"class":274},[127,3987,3936],{"class":869},[127,3989,3939],{"class":154},[127,3991,1717],{"class":274},[127,3993,258],{"class":154},[127,3995,1717],{"class":274},[127,3997,1894],{"class":274},[127,3999,3950],{"class":599},[127,4001,618],{"class":274},[127,4003,2207],{"class":154},[127,4005,3957],{"class":274},[127,4007,4008],{"class":144},"HS256",[127,4010,3957],{"class":274},[127,4012,3965],{"class":154},[127,4014,3491],{"class":274},[127,4016,3970],{"class":154},[236,4018,4019],{},[65,4020,4021],{},"常見錯誤：",[59,4023,4024,4035,4038],{},[62,4025,4026,4027,4030,4031,4034],{},"將 JWT 存在 ",[124,4028,4029],{},"localStorage","，容易被 XSS 竊取，應改用 ",[124,4032,4033],{},"httpOnly"," Cookie",[62,4036,4037],{},"Token 有效期設太長，應配合 Refresh Token 機制縮短 Access Token 壽命",[62,4039,4040],{},"沒有實作 Token 撤銷機制，登出後 token 仍然有效",[112,4042,4044],{"id":4043},"最小權限原則principle-of-least-privilege","最小權限原則（Principle of Least Privilege）",[236,4046,4047],{},"每個用戶、服務、資料庫帳號只給完成任務所需的最小權限。",[117,4049,4053],{"className":4050,"code":4051,"language":4052,"meta":122,"style":122},"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",[124,4054,4055,4060,4065,4069,4074],{"__ignoreMap":122},[127,4056,4057],{"class":129,"line":130},[127,4058,4059],{},"-- ❌ 給 API 帳號完整資料庫權限\n",[127,4061,4062],{"class":129,"line":137},[127,4063,4064],{},"GRANT ALL PRIVILEGES ON *.* TO 'api_user'@'%';\n",[127,4066,4067],{"class":129,"line":158},[127,4068,170],{"emptyLinePlaceholder":41},[127,4070,4071],{"class":129,"line":167},[127,4072,4073],{},"-- ✅ 只給必要的讀寫權限\n",[127,4075,4076],{"class":129,"line":173},[127,4077,4078],{},"GRANT SELECT, INSERT, UPDATE ON app_db.orders TO 'api_user'@'localhost';\n",[106,4080],{},[55,4082,4083],{"id":4083},"輸入驗證與防注入",[112,4085,4087],{"id":4086},"sql-injection","SQL Injection",[236,4089,4090],{},"永遠不要用字串拼接組 SQL 查詢。",[117,4092,4094],{"className":3917,"code":4093,"language":3919,"meta":122,"style":122},"\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",[124,4095,4096,4101,4129,4133,4138,4153],{"__ignoreMap":122},[127,4097,4098],{"class":129,"line":130},[127,4099,4100],{"class":133},"\u002F\u002F ❌ 危險：攻擊者輸入 ' OR '1'='1 即可繞過驗證\n",[127,4102,4103,4105,4108,4110,4113,4116,4119,4122,4124,4126],{"class":129,"line":137},[127,4104,3518],{"class":1924},[127,4106,4107],{"class":154}," query ",[127,4109,814],{"class":274},[127,4111,4112],{"class":274}," `",[127,4114,4115],{"class":144},"SELECT * FROM users WHERE email = '",[127,4117,4118],{"class":274},"${",[127,4120,4121],{"class":154},"email",[127,4123,3491],{"class":274},[127,4125,3957],{"class":144},[127,4127,4128],{"class":274},"`\n",[127,4130,4131],{"class":129,"line":158},[127,4132,170],{"emptyLinePlaceholder":41},[127,4134,4135],{"class":129,"line":167},[127,4136,4137],{"class":133},"\u002F\u002F ✅ 使用參數化查詢\n",[127,4139,4140,4142,4144,4146,4148,4151],{"class":129,"line":173},[127,4141,3518],{"class":1924},[127,4143,4107],{"class":154},[127,4145,814],{"class":274},[127,4147,762],{"class":274},[127,4149,4150],{"class":144},"SELECT * FROM users WHERE email = $1",[127,4152,768],{"class":274},[127,4154,4155,4158,4161,4163,4165,4168,4170],{"class":129,"line":179},[127,4156,4157],{"class":1340},"await",[127,4159,4160],{"class":154}," db",[127,4162,1711],{"class":274},[127,4164,2261],{"class":869},[127,4166,4167],{"class":154},"(query",[127,4169,1717],{"class":274},[127,4171,4172],{"class":154}," [email])\n",[112,4174,4176],{"id":4175},"xsscross-site-scripting","XSS（Cross-Site Scripting）",[236,4178,4179],{},"對所有用戶輸入進行跳脫處理，避免注入惡意腳本。",[117,4181,4183],{"className":3917,"code":4182,"language":3919,"meta":122,"style":122},"import DOMPurify from 'dompurify'\n\n\u002F\u002F ✅ 渲染用戶輸入的 HTML 前先清理\nconst clean = DOMPurify.sanitize(userInput)\n",[124,4184,4185,4201,4205,4210],{"__ignoreMap":122},[127,4186,4187,4189,4192,4194,4196,4199],{"class":129,"line":130},[127,4188,1871],{"class":1340},[127,4190,4191],{"class":154}," DOMPurify ",[127,4193,1877],{"class":1340},[127,4195,762],{"class":274},[127,4197,4198],{"class":144},"dompurify",[127,4200,768],{"class":274},[127,4202,4203],{"class":129,"line":137},[127,4204,170],{"emptyLinePlaceholder":41},[127,4206,4207],{"class":129,"line":158},[127,4208,4209],{"class":133},"\u002F\u002F ✅ 渲染用戶輸入的 HTML 前先清理\n",[127,4211,4212,4214,4217,4219,4222,4224,4227],{"class":129,"line":167},[127,4213,3518],{"class":1924},[127,4215,4216],{"class":154}," clean ",[127,4218,814],{"class":274},[127,4220,4221],{"class":154}," DOMPurify",[127,4223,1711],{"class":274},[127,4225,4226],{"class":869},"sanitize",[127,4228,4229],{"class":154},"(userInput)\n",[236,4231,4232],{},[65,4233,4234],{},"HTTP Header 防護：",[117,4236,4239],{"className":4237,"code":4238,"language":798},[796],"Content-Security-Policy: default-src 'self'\nX-Content-Type-Options: nosniff\nX-Frame-Options: DENY\n",[124,4240,4238],{"__ignoreMap":122},[112,4242,4243],{"id":4243},"環境變數管理",[117,4245,4247],{"className":119,"code":4246,"language":121,"meta":122,"style":122},"# ❌ 絕對不能 commit 進 git\nDB_PASSWORD=mysecretpassword\nJWT_SECRET=abc123\n\n# ✅ 用 .env 並加入 .gitignore\necho \".env\" >> .gitignore\n",[124,4248,4249,4254,4264,4274,4278,4283],{"__ignoreMap":122},[127,4250,4251],{"class":129,"line":130},[127,4252,4253],{"class":133},"# ❌ 絕對不能 commit 進 git\n",[127,4255,4256,4259,4261],{"class":129,"line":137},[127,4257,4258],{"class":154},"DB_PASSWORD",[127,4260,814],{"class":274},[127,4262,4263],{"class":144},"mysecretpassword\n",[127,4265,4266,4269,4271],{"class":129,"line":158},[127,4267,4268],{"class":154},"JWT_SECRET",[127,4270,814],{"class":274},[127,4272,4273],{"class":144},"abc123\n",[127,4275,4276],{"class":129,"line":167},[127,4277,170],{"emptyLinePlaceholder":41},[127,4279,4280],{"class":129,"line":173},[127,4281,4282],{"class":133},"# ✅ 用 .env 並加入 .gitignore\n",[127,4284,4285,4287,4289,4292,4294,4297],{"class":129,"line":179},[127,4286,870],{"class":869},[127,4288,838],{"class":274},[127,4290,4291],{"class":144},".env",[127,4293,275],{"class":274},[127,4295,4296],{"class":274}," >>",[127,4298,4299],{"class":144}," .gitignore\n",[106,4301],{},[55,4303,4305],{"id":4304},"https-與資料傳輸","HTTPS 與資料傳輸",[236,4307,4308],{},"所有生產環境流量必須走 HTTPS，並確保 TLS 設定正確。",[117,4310,4314],{"className":4311,"code":4312,"language":4313,"meta":122,"style":122},"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",[124,4315,4316,4321,4326,4331,4336,4340,4345,4350],{"__ignoreMap":122},[127,4317,4318],{"class":129,"line":130},[127,4319,4320],{},"server {\n",[127,4322,4323],{"class":129,"line":137},[127,4324,4325],{},"    listen 443 ssl;\n",[127,4327,4328],{"class":129,"line":158},[127,4329,4330],{},"    ssl_protocols TLSv1.2 TLSv1.3;   # 停用舊版 TLS\n",[127,4332,4333],{"class":129,"line":167},[127,4334,4335],{},"    ssl_ciphers HIGH:!aNULL:!MD5;\n",[127,4337,4338],{"class":129,"line":173},[127,4339,170],{"emptyLinePlaceholder":41},[127,4341,4342],{"class":129,"line":179},[127,4343,4344],{},"    # HSTS：強制瀏覽器只走 HTTPS\n",[127,4346,4347],{"class":129,"line":193},[127,4348,4349],{},"    add_header Strict-Transport-Security \"max-age=31536000\" always;\n",[127,4351,4352],{"class":129,"line":201},[127,4353,3324],{},[106,4355],{},[55,4357,4358],{"id":4358},"依賴管理",[236,4360,4361],{},"第三方套件是常見的攻擊入口，需要定期審查。",[117,4363,4365],{"className":119,"code":4364,"language":121,"meta":122,"style":122},"# 掃描已知漏洞\nnpm audit\n\n# 自動修復低風險漏洞\nnpm audit fix\n",[124,4366,4367,4372,4380,4384,4389],{"__ignoreMap":122},[127,4368,4369],{"class":129,"line":130},[127,4370,4371],{"class":133},"# 掃描已知漏洞\n",[127,4373,4374,4377],{"class":129,"line":137},[127,4375,4376],{"class":140},"npm",[127,4378,4379],{"class":144}," audit\n",[127,4381,4382],{"class":129,"line":158},[127,4383,170],{"emptyLinePlaceholder":41},[127,4385,4386],{"class":129,"line":167},[127,4387,4388],{"class":133},"# 自動修復低風險漏洞\n",[127,4390,4391,4393,4396],{"class":129,"line":173},[127,4392,4376],{"class":140},[127,4394,4395],{"class":144}," audit",[127,4397,4398],{"class":144}," fix\n",[236,4400,4401],{},[65,4402,4403],{},"最佳實踐：",[59,4405,4406,4413,4416],{},[62,4407,4408,4409,4412],{},"鎖定套件版本（",[124,4410,4411],{},"package-lock.json"," 要 commit）",[62,4414,4415],{},"定期更新依賴，訂閱安全通報（如 GitHub Dependabot）",[62,4417,4418],{},"不引入不必要的套件，減少攻擊面",[106,4420],{},[55,4422,4424],{"id":4423},"api-安全","API 安全",[112,4426,4428],{"id":4427},"rate-limiting","Rate Limiting",[236,4430,4431],{},"防止暴力破解與 DDoS。",[117,4433,4435],{"className":3917,"code":4434,"language":3919,"meta":122,"style":122},"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",[124,4436,4437,4453,4457,4473,4499,4514,4528,4534,4538],{"__ignoreMap":122},[127,4438,4439,4441,4444,4446,4448,4451],{"class":129,"line":130},[127,4440,1871],{"class":1340},[127,4442,4443],{"class":154}," rateLimit ",[127,4445,1877],{"class":1340},[127,4447,762],{"class":274},[127,4449,4450],{"class":144},"express-rate-limit",[127,4452,768],{"class":274},[127,4454,4455],{"class":129,"line":137},[127,4456,170],{"emptyLinePlaceholder":41},[127,4458,4459,4461,4464,4466,4469,4471],{"class":129,"line":158},[127,4460,3518],{"class":1924},[127,4462,4463],{"class":154}," loginLimiter ",[127,4465,814],{"class":274},[127,4467,4468],{"class":869}," rateLimit",[127,4470,1934],{"class":154},[127,4472,2893],{"class":274},[127,4474,4475,4478,4480,4483,4486,4489,4491,4494,4496],{"class":129,"line":167},[127,4476,4477],{"class":599},"  windowMs",[127,4479,618],{"class":274},[127,4481,4482],{"class":2029}," 15",[127,4484,4485],{"class":274}," *",[127,4487,4488],{"class":2029}," 60",[127,4490,4485],{"class":274},[127,4492,4493],{"class":2029}," 1000",[127,4495,1717],{"class":274},[127,4497,4498],{"class":133}," \u002F\u002F 15 分鐘\n",[127,4500,4501,4504,4506,4509,4511],{"class":129,"line":173},[127,4502,4503],{"class":599},"  max",[127,4505,618],{"class":274},[127,4507,4508],{"class":2029}," 10",[127,4510,1717],{"class":274},[127,4512,4513],{"class":133},"                   \u002F\u002F 最多 10 次嘗試\n",[127,4515,4516,4519,4521,4523,4526],{"class":129,"line":179},[127,4517,4518],{"class":599},"  message",[127,4520,618],{"class":274},[127,4522,762],{"class":274},[127,4524,4525],{"class":144},"嘗試次數過多，請稍後再試",[127,4527,768],{"class":274},[127,4529,4530,4532],{"class":129,"line":193},[127,4531,3491],{"class":274},[127,4533,3970],{"class":154},[127,4535,4536],{"class":129,"line":201},[127,4537,170],{"emptyLinePlaceholder":41},[127,4539,4540,4543,4545,4548,4550,4552,4555,4557,4559,4562,4564],{"class":129,"line":206},[127,4541,4542],{"class":154},"app",[127,4544,1711],{"class":274},[127,4546,4547],{"class":869},"post",[127,4549,1934],{"class":154},[127,4551,3957],{"class":274},[127,4553,4554],{"class":144},"\u002Fauth\u002Flogin",[127,4556,3957],{"class":274},[127,4558,1717],{"class":274},[127,4560,4561],{"class":154}," loginLimiter",[127,4563,1717],{"class":274},[127,4565,4566],{"class":154}," loginHandler)\n",[112,4568,4570],{"id":4569},"cors-設定","CORS 設定",[236,4572,4573,4574,4577],{},"不要用 ",[124,4575,4576],{},"*"," 允許所有來源。",[117,4579,4581],{"className":3917,"code":4580,"language":3919,"meta":122,"style":122},"\u002F\u002F ❌ 允許所有來源\napp.use(cors({ origin: '*' }))\n\n\u002F\u002F ✅ 明確指定允許的來源\napp.use(cors({\n  origin: ['https:\u002F\u002Fyourdomain.com'],\n  credentials: true\n}))\n",[124,4582,4583,4588,4622,4626,4631,4647,4667,4677],{"__ignoreMap":122},[127,4584,4585],{"class":129,"line":130},[127,4586,4587],{"class":133},"\u002F\u002F ❌ 允許所有來源\n",[127,4589,4590,4592,4594,4597,4599,4602,4604,4606,4609,4611,4613,4615,4617,4619],{"class":129,"line":137},[127,4591,4542],{"class":154},[127,4593,1711],{"class":274},[127,4595,4596],{"class":869},"use",[127,4598,1934],{"class":154},[127,4600,4601],{"class":869},"cors",[127,4603,1934],{"class":154},[127,4605,841],{"class":274},[127,4607,4608],{"class":599}," origin",[127,4610,618],{"class":274},[127,4612,762],{"class":274},[127,4614,4576],{"class":144},[127,4616,3957],{"class":274},[127,4618,1900],{"class":274},[127,4620,4621],{"class":154},"))\n",[127,4623,4624],{"class":129,"line":158},[127,4625,170],{"emptyLinePlaceholder":41},[127,4627,4628],{"class":129,"line":167},[127,4629,4630],{"class":133},"\u002F\u002F ✅ 明確指定允許的來源\n",[127,4632,4633,4635,4637,4639,4641,4643,4645],{"class":129,"line":173},[127,4634,4542],{"class":154},[127,4636,1711],{"class":274},[127,4638,4596],{"class":869},[127,4640,1934],{"class":154},[127,4642,4601],{"class":869},[127,4644,1934],{"class":154},[127,4646,2893],{"class":274},[127,4648,4649,4652,4654,4656,4658,4661,4663,4665],{"class":129,"line":179},[127,4650,4651],{"class":599},"  origin",[127,4653,618],{"class":274},[127,4655,2207],{"class":154},[127,4657,3957],{"class":274},[127,4659,4660],{"class":144},"https:\u002F\u002Fyourdomain.com",[127,4662,3957],{"class":274},[127,4664,2228],{"class":154},[127,4666,1792],{"class":274},[127,4668,4669,4672,4674],{"class":129,"line":193},[127,4670,4671],{"class":599},"  credentials",[127,4673,618],{"class":274},[127,4675,4676],{"class":1189}," true\n",[127,4678,4679,4681],{"class":129,"line":201},[127,4680,3491],{"class":274},[127,4682,4621],{"class":154},[106,4684],{},[55,4686,4687],{"id":4687},"基礎設施安全",[112,4689,4691],{"id":4690},"kubernetes-gke","Kubernetes \u002F GKE",[117,4693,4695],{"className":590,"code":4694,"language":592,"meta":122,"style":122},"# ✅ 不以 root 身份執行容器\nsecurityContext:\n  runAsNonRoot: true\n  runAsUser: 1000\n  readOnlyRootFilesystem: true\n\n# ✅ 限制資源，防止單一 Pod 吃光資源\nresources:\n  limits:\n    cpu: \"500m\"\n    memory: \"256Mi\"\n",[124,4696,4697,4702,4709,4718,4728,4737,4741,4746,4753,4760,4774],{"__ignoreMap":122},[127,4698,4699],{"class":129,"line":130},[127,4700,4701],{"class":133},"# ✅ 不以 root 身份執行容器\n",[127,4703,4704,4707],{"class":129,"line":137},[127,4705,4706],{"class":599},"securityContext",[127,4708,603],{"class":274},[127,4710,4711,4714,4716],{"class":129,"line":158},[127,4712,4713],{"class":599},"  runAsNonRoot",[127,4715,618],{"class":274},[127,4717,4676],{"class":1189},[127,4719,4720,4723,4725],{"class":129,"line":167},[127,4721,4722],{"class":599},"  runAsUser",[127,4724,618],{"class":274},[127,4726,4727],{"class":2029}," 1000\n",[127,4729,4730,4733,4735],{"class":129,"line":173},[127,4731,4732],{"class":599},"  readOnlyRootFilesystem",[127,4734,618],{"class":274},[127,4736,4676],{"class":1189},[127,4738,4739],{"class":129,"line":179},[127,4740,170],{"emptyLinePlaceholder":41},[127,4742,4743],{"class":129,"line":193},[127,4744,4745],{"class":133},"# ✅ 限制資源，防止單一 Pod 吃光資源\n",[127,4747,4748,4751],{"class":129,"line":201},[127,4749,4750],{"class":599},"resources",[127,4752,603],{"class":274},[127,4754,4755,4758],{"class":129,"line":206},[127,4756,4757],{"class":599},"  limits",[127,4759,603],{"class":274},[127,4761,4762,4765,4767,4769,4772],{"class":129,"line":212},[127,4763,4764],{"class":599},"    cpu",[127,4766,618],{"class":274},[127,4768,838],{"class":274},[127,4770,4771],{"class":144},"500m",[127,4773,320],{"class":274},[127,4775,4776,4779,4781,4783,4786],{"class":129,"line":226},[127,4777,4778],{"class":599},"    memory",[127,4780,618],{"class":274},[127,4782,838],{"class":274},[127,4784,4785],{"class":144},"256Mi",[127,4787,320],{"class":274},[112,4789,4791],{"id":4790},"secret-管理","Secret 管理",[236,4793,4794],{},"不要把 secret 直接寫在 YAML 裡。",[117,4796,4798],{"className":119,"code":4797,"language":121,"meta":122,"style":122},"# ✅ 使用 Kubernetes Secret\nkubectl create secret generic db-secret \\\n  --from-literal=password=mysecretpassword\n\n# 或使用 Google Secret Manager \u002F AWS Secrets Manager\n",[124,4799,4800,4805,4820,4825,4829],{"__ignoreMap":122},[127,4801,4802],{"class":129,"line":130},[127,4803,4804],{"class":133},"# ✅ 使用 Kubernetes Secret\n",[127,4806,4807,4809,4811,4813,4815,4818],{"class":129,"line":137},[127,4808,252],{"class":140},[127,4810,255],{"class":144},[127,4812,258],{"class":144},[127,4814,261],{"class":144},[127,4816,4817],{"class":144}," db-secret",[127,4819,155],{"class":154},[127,4821,4822],{"class":129,"line":158},[127,4823,4824],{"class":144},"  --from-literal=password=mysecretpassword\n",[127,4826,4827],{"class":129,"line":167},[127,4828,170],{"emptyLinePlaceholder":41},[127,4830,4831],{"class":129,"line":173},[127,4832,4833],{"class":133},"# 或使用 Google Secret Manager \u002F AWS Secrets Manager\n",[106,4835],{},[55,4837,4838],{"id":4838},"日誌與監控",[236,4840,4841],{},"記錄足夠的資訊幫助事後調查，但避免記錄敏感資料。",[117,4843,4845],{"className":3917,"code":4844,"language":3919,"meta":122,"style":122},"\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",[124,4846,4847,4852,4889,4893,4898],{"__ignoreMap":122},[127,4848,4849],{"class":129,"line":130},[127,4850,4851],{"class":133},"\u002F\u002F ❌ 日誌包含密碼\n",[127,4853,4854,4857,4859,4862,4864,4867,4870,4872,4874,4876,4879,4881,4884,4887],{"class":129,"line":137},[127,4855,4856],{"class":154},"logger",[127,4858,1711],{"class":274},[127,4860,4861],{"class":869},"info",[127,4863,1934],{"class":154},[127,4865,4866],{"class":274},"`",[127,4868,4869],{"class":144},"Login attempt: ",[127,4871,4118],{"class":274},[127,4873,4121],{"class":154},[127,4875,3491],{"class":274},[127,4877,4878],{"class":144}," \u002F ",[127,4880,4118],{"class":274},[127,4882,4883],{"class":154},"password",[127,4885,4886],{"class":274},"}`",[127,4888,3970],{"class":154},[127,4890,4891],{"class":129,"line":158},[127,4892,170],{"emptyLinePlaceholder":41},[127,4894,4895],{"class":129,"line":167},[127,4896,4897],{"class":133},"\u002F\u002F ✅ 只記錄必要資訊\n",[127,4899,4900,4902,4904,4906,4908,4910,4912,4914,4916,4918,4920,4922,4925,4927,4930,4932,4935,4937,4940,4942,4944,4946,4949,4951,4954,4956,4958,4960],{"class":129,"line":173},[127,4901,4856],{"class":154},[127,4903,1711],{"class":274},[127,4905,4861],{"class":869},[127,4907,1934],{"class":154},[127,4909,4866],{"class":274},[127,4911,4869],{"class":144},[127,4913,4118],{"class":274},[127,4915,4121],{"class":154},[127,4917,4886],{"class":274},[127,4919,1717],{"class":274},[127,4921,1894],{"class":274},[127,4923,4924],{"class":599}," ip",[127,4926,618],{"class":274},[127,4928,4929],{"class":154}," req",[127,4931,1711],{"class":274},[127,4933,4934],{"class":154},"ip",[127,4936,1717],{"class":274},[127,4938,4939],{"class":599}," userAgent",[127,4941,618],{"class":274},[127,4943,4929],{"class":154},[127,4945,1711],{"class":274},[127,4947,4948],{"class":154},"headers[",[127,4950,3957],{"class":274},[127,4952,4953],{"class":144},"user-agent",[127,4955,3957],{"class":274},[127,4957,3965],{"class":154},[127,4959,3491],{"class":274},[127,4961,3970],{"class":154},[236,4963,4964],{},[65,4965,4966],{},"應該監控的指標：",[59,4968,4969,4972,4975],{},[62,4970,4971],{},"異常登入失敗次數",[62,4973,4974],{},"非預期的 API 錯誤率上升",[62,4976,4977],{},"非工作時間的大量資料存取",[106,4979],{},[55,4981,4983],{"id":4982},"安全開發流程devsecops","安全開發流程（DevSecOps）",[920,4985,4986,4996],{},[923,4987,4988],{},[926,4989,4990,4993],{},[929,4991,4992],{},"階段",[929,4994,4995],{},"實踐",[936,4997,4998,5006,5021,5029],{},[926,4999,5000,5003],{},[941,5001,5002],{},"開發",[941,5004,5005],{},"Code Review、靜態分析（ESLint security rules）",[926,5007,5008,5011],{},[941,5009,5010],{},"CI\u002FCD",[941,5012,5013,5014,1639,5017,5020],{},"自動化掃描（",[124,5015,5016],{},"npm audit",[124,5018,5019],{},"trivy"," 掃 Docker image）",[926,5022,5023,5026],{},[941,5024,5025],{},"部署",[941,5027,5028],{},"最小權限、Secret Manager、網路隔離",[926,5030,5031,5034],{},[941,5032,5033],{},"運營",[941,5035,5036],{},"日誌監控、定期滲透測試、漏洞回報機制",[236,5038,5039],{},"資安不是一次性的工作，而是需要在整個開發流程中持續落實的文化。",[1564,5041,5042],{},"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":122,"searchDepth":137,"depth":137,"links":5044},[5045,5049,5054,5055,5056,5060,5064,5065],{"id":3907,"depth":137,"text":3907,"children":5046},[5047,5048],{"id":3910,"depth":158,"text":3911},{"id":4043,"depth":158,"text":4044},{"id":4083,"depth":137,"text":4083,"children":5050},[5051,5052,5053],{"id":4086,"depth":158,"text":4087},{"id":4175,"depth":158,"text":4176},{"id":4243,"depth":158,"text":4243},{"id":4304,"depth":137,"text":4305},{"id":4358,"depth":137,"text":4358},{"id":4423,"depth":137,"text":4424,"children":5057},[5058,5059],{"id":4427,"depth":158,"text":4428},{"id":4569,"depth":158,"text":4570},{"id":4687,"depth":137,"text":4687,"children":5061},[5062,5063],{"id":4690,"depth":158,"text":4691},{"id":4790,"depth":158,"text":4791},{"id":4838,"depth":137,"text":4838},{"id":4982,"depth":137,"text":4983},"2025-12-05","整理日常開發中常用的資安觀念與實踐方式。","\u002Fimages\u002Fsecurity.jpg",{},{"title":22,"description":5067},"zWX3ru6e2VKC3sHo1ft7LY1UaT_FOrgnGi5WvJmxyJU",{"id":5073,"title":26,"author":5074,"body":5076,"date":5783,"description":5784,"extension":1596,"externalUrl":37,"image":5785,"meta":5786,"minRead":179,"navigation":41,"path":27,"seo":5787,"stem":28,"__hash__":5788},"blog\u002Farticles\u002Fsingle-machine-performance.md",{"name":48,"avatar":5075},{"src":50,"alt":48},{"type":52,"value":5077,"toc":5764},[5078,5082,5085,5090,5092,5095,5098,5104,5107,5113,5116,5118,5121,5127,5130,5133,5136,5147,5151,5154,5264,5269,5289,5291,5294,5297,5301,5304,5333,5339,5343,5350,5380,5383,5385,5389,5392,5398,5553,5558,5566,5568,5572,5575,5581,5584,5595,5597,5600,5604,5611,5614,5620,5627,5631,5637,5640,5689,5694,5700,5703,5705,5708,5754,5761],[55,5079,5081],{"id":5080},"為什麼從單機開始","為什麼從單機開始？",[236,5083,5084],{},"不少談高性能的書籍直接跳到分散式架構，但如果連單機都處理不好，分散式只是把問題放大。",[236,5086,5087],{},[65,5088,5089],{},"單機處理好，才是高性能的前提。",[106,5091],{},[55,5093,5094],{"id":5094},"基本架構",[236,5096,5097],{},"最簡單的系統只有兩層：",[117,5099,5102],{"className":5100,"code":5101,"language":798},[796],"Client → 應用服務（Web Server）→ 資料庫服務（DB）\n",[124,5103,5101],{"__ignoreMap":122},[236,5105,5106],{},"大多數系統成長後，會再加入兩個服務：",[117,5108,5111],{"className":5109,"code":5110,"language":798},[796],"Client → CDN → 應用服務 → Cache → 資料庫服務\n",[124,5112,5110],{"__ignoreMap":122},[236,5114,5115],{},"這個架構覆蓋了世界上 80% 以上的系統，從這裡出發優化，是最務實的路線。",[106,5117],{},[55,5119,5120],{"id":5120},"應用層優化",[236,5122,5123,5124],{},"應用層的目標：",[65,5125,5126],{},"用最少的資源處理請求，並最快回應客戶。",[236,5128,5129],{},"可以拆成兩個方向：",[112,5131,5132],{"id":5132},"運算優化",[236,5134,5135],{},"減少不必要的 CPU 消耗：",[59,5137,5138,5141,5144],{},[62,5139,5140],{},"避免重複計算，善用 memoization",[62,5142,5143],{},"演算法與資料結構的選擇（時間複雜度）",[62,5145,5146],{},"避免同步阻塞的操作佔用主執行緒",[112,5148,5150],{"id":5149},"io-優化","I\u002FO 優化",[236,5152,5153],{},"大多數 Web 應用的瓶頸都在 I\u002FO，而不是運算。",[117,5155,5157],{"className":3917,"code":5156,"language":3919,"meta":122,"style":122},"\u002F\u002F ❌ 循環內逐一查詢，N 次 I\u002FO\nfor (const id of ids) {\n  const user = await db.query('SELECT * FROM users WHERE id = $1', [id])\n}\n\n\u002F\u002F ✅ 一次批量查詢，1 次 I\u002FO\nconst users = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids])\n",[124,5158,5159,5164,5184,5220,5224,5228,5233],{"__ignoreMap":122},[127,5160,5161],{"class":129,"line":130},[127,5162,5163],{"class":133},"\u002F\u002F ❌ 循環內逐一查詢，N 次 I\u002FO\n",[127,5165,5166,5169,5171,5173,5176,5179,5182],{"class":129,"line":137},[127,5167,5168],{"class":1340},"for",[127,5170,3052],{"class":154},[127,5172,3518],{"class":1924},[127,5174,5175],{"class":154}," id ",[127,5177,5178],{"class":274},"of",[127,5180,5181],{"class":154}," ids) ",[127,5183,2893],{"class":274},[127,5185,5186,5188,5191,5193,5195,5197,5199,5201,5203,5205,5208,5210,5212,5214,5217],{"class":129,"line":158},[127,5187,1952],{"class":1924},[127,5189,5190],{"class":154}," user",[127,5192,1990],{"class":274},[127,5194,3526],{"class":1340},[127,5196,4160],{"class":154},[127,5198,1711],{"class":274},[127,5200,2261],{"class":869},[127,5202,1934],{"class":599},[127,5204,3957],{"class":274},[127,5206,5207],{"class":144},"SELECT * FROM users WHERE id = $1",[127,5209,3957],{"class":274},[127,5211,1717],{"class":274},[127,5213,2207],{"class":599},[127,5215,5216],{"class":154},"id",[127,5218,5219],{"class":599},"])\n",[127,5221,5222],{"class":129,"line":167},[127,5223,3324],{"class":274},[127,5225,5226],{"class":129,"line":173},[127,5227,170],{"emptyLinePlaceholder":41},[127,5229,5230],{"class":129,"line":179},[127,5231,5232],{"class":133},"\u002F\u002F ✅ 一次批量查詢，1 次 I\u002FO\n",[127,5234,5235,5237,5240,5242,5244,5246,5248,5250,5252,5254,5257,5259,5261],{"class":129,"line":193},[127,5236,3518],{"class":1924},[127,5238,5239],{"class":154}," users ",[127,5241,814],{"class":274},[127,5243,3526],{"class":1340},[127,5245,4160],{"class":154},[127,5247,1711],{"class":274},[127,5249,2261],{"class":869},[127,5251,1934],{"class":154},[127,5253,3957],{"class":274},[127,5255,5256],{"class":144},"SELECT * FROM users WHERE id = ANY($1)",[127,5258,3957],{"class":274},[127,5260,1717],{"class":274},[127,5262,5263],{"class":154}," [ids])\n",[236,5265,5266],{},[65,5267,5268],{},"常見 I\u002FO 加速技術：",[59,5270,5271,5277,5283],{},[62,5272,5273,5276],{},[65,5274,5275],{},"連線池（Connection Pool）","：複用資料庫連線，避免每次請求都重新建立連線",[62,5278,5279,5282],{},[65,5280,5281],{},"Stream","：處理大檔案時逐段讀取，避免一次性載入記憶體",[62,5284,5285,5288],{},[65,5286,5287],{},"非同步 I\u002FO","：Node.js 的事件驅動模型天生適合高併發 I\u002FO 場景",[106,5290],{},[55,5292,5293],{"id":5293},"資料庫層優化",[236,5295,5296],{},"資料庫層的目標同應用層，以最少的資源最快完成查詢。",[112,5298,5300],{"id":5299},"索引index","索引（Index）",[236,5302,5303],{},"索引是影響資料庫性能最大的變數。用得好，查詢從全表掃描（O(n)）變成索引掃描（O(log n)）。",[117,5305,5307],{"className":4050,"code":5306,"language":4052,"meta":122,"style":122},"-- 沒有索引：全表掃描，資料量大時極慢\nSELECT * FROM orders WHERE user_id = 123;\n\n-- 建立索引後：直接定位\nCREATE INDEX idx_orders_user_id ON orders(user_id);\n",[124,5308,5309,5314,5319,5323,5328],{"__ignoreMap":122},[127,5310,5311],{"class":129,"line":130},[127,5312,5313],{},"-- 沒有索引：全表掃描，資料量大時極慢\n",[127,5315,5316],{"class":129,"line":137},[127,5317,5318],{},"SELECT * FROM orders WHERE user_id = 123;\n",[127,5320,5321],{"class":129,"line":158},[127,5322,170],{"emptyLinePlaceholder":41},[127,5324,5325],{"class":129,"line":167},[127,5326,5327],{},"-- 建立索引後：直接定位\n",[127,5329,5330],{"class":129,"line":173},[127,5331,5332],{},"CREATE INDEX idx_orders_user_id ON orders(user_id);\n",[236,5334,5335,5338],{},[65,5336,5337],{},"注意："," 索引不是越多越好，寫入時需要維護索引，會拖慢 INSERT \u002F UPDATE。",[112,5340,5342],{"id":5341},"鎖與事務lock-transaction","鎖與事務（Lock & Transaction）",[236,5344,5345,5346,5349],{},"鎖與事務是保障資料",[65,5347,5348],{},"一致性","的必要機制，但也是性能的隱患。",[117,5351,5353],{"className":4050,"code":5352,"language":4052,"meta":122,"style":122},"-- 事務確保原子性：轉帳要嘛全成功，要嘛全失敗\nBEGIN;\n  UPDATE accounts SET balance = balance - 100 WHERE id = 1;\n  UPDATE accounts SET balance = balance + 100 WHERE id = 2;\nCOMMIT;\n",[124,5354,5355,5360,5365,5370,5375],{"__ignoreMap":122},[127,5356,5357],{"class":129,"line":130},[127,5358,5359],{},"-- 事務確保原子性：轉帳要嘛全成功，要嘛全失敗\n",[127,5361,5362],{"class":129,"line":137},[127,5363,5364],{},"BEGIN;\n",[127,5366,5367],{"class":129,"line":158},[127,5368,5369],{},"  UPDATE accounts SET balance = balance - 100 WHERE id = 1;\n",[127,5371,5372],{"class":129,"line":167},[127,5373,5374],{},"  UPDATE accounts SET balance = balance + 100 WHERE id = 2;\n",[127,5376,5377],{"class":129,"line":173},[127,5378,5379],{},"COMMIT;\n",[236,5381,5382],{},"鎖的範圍越小、持有時間越短，對性能的影響越低。",[106,5384],{},[55,5386,5388],{"id":5387},"cache-服務","Cache 服務",[236,5390,5391],{},"Cache 的目的是將資料庫的常用查詢結果提前存起來，讓讀取繞過資料庫。",[117,5393,5396],{"className":5394,"code":5395,"language":798},[796],"Client → 應用服務 → Cache（Redis）→ DB（Cache Miss 時才查）\n",[124,5397,5395],{"__ignoreMap":122},[117,5399,5401],{"className":3917,"code":5400,"language":3919,"meta":122,"style":122},"const cacheKey = `user:${userId}`\nlet user = await redis.get(cacheKey)\n\nif (!user) {\n  user = await db.query('SELECT * FROM users WHERE id = $1', [userId])\n  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600) \u002F\u002F 快取 1 小時\n}\n",[124,5402,5403,5425,5447,5451,5466,5497,5549],{"__ignoreMap":122},[127,5404,5405,5407,5410,5412,5414,5417,5419,5422],{"class":129,"line":130},[127,5406,3518],{"class":1924},[127,5408,5409],{"class":154}," cacheKey ",[127,5411,814],{"class":274},[127,5413,4112],{"class":274},[127,5415,5416],{"class":144},"user:",[127,5418,4118],{"class":274},[127,5420,5421],{"class":154},"userId",[127,5423,5424],{"class":274},"}`\n",[127,5426,5427,5429,5432,5434,5436,5439,5441,5444],{"class":129,"line":137},[127,5428,3650],{"class":1924},[127,5430,5431],{"class":154}," user ",[127,5433,814],{"class":274},[127,5435,3526],{"class":1340},[127,5437,5438],{"class":154}," redis",[127,5440,1711],{"class":274},[127,5442,5443],{"class":869},"get",[127,5445,5446],{"class":154},"(cacheKey)\n",[127,5448,5449],{"class":129,"line":158},[127,5450,170],{"emptyLinePlaceholder":41},[127,5452,5453,5456,5458,5461,5464],{"class":129,"line":167},[127,5454,5455],{"class":1340},"if",[127,5457,3052],{"class":154},[127,5459,5460],{"class":274},"!",[127,5462,5463],{"class":154},"user) ",[127,5465,2893],{"class":274},[127,5467,5468,5471,5473,5475,5477,5479,5481,5483,5485,5487,5489,5491,5493,5495],{"class":129,"line":173},[127,5469,5470],{"class":154},"  user",[127,5472,1990],{"class":274},[127,5474,3526],{"class":1340},[127,5476,4160],{"class":154},[127,5478,1711],{"class":274},[127,5480,2261],{"class":869},[127,5482,1934],{"class":599},[127,5484,3957],{"class":274},[127,5486,5207],{"class":144},[127,5488,3957],{"class":274},[127,5490,1717],{"class":274},[127,5492,2207],{"class":599},[127,5494,5421],{"class":154},[127,5496,5219],{"class":599},[127,5498,5499,5501,5503,5505,5508,5510,5513,5515,5518,5520,5523,5525,5528,5530,5532,5534,5537,5539,5541,5544,5546],{"class":129,"line":179},[127,5500,2881],{"class":1340},[127,5502,5438],{"class":154},[127,5504,1711],{"class":274},[127,5506,5507],{"class":869},"set",[127,5509,1934],{"class":599},[127,5511,5512],{"class":154},"cacheKey",[127,5514,1717],{"class":274},[127,5516,5517],{"class":154}," JSON",[127,5519,1711],{"class":274},[127,5521,5522],{"class":869},"stringify",[127,5524,1934],{"class":599},[127,5526,5527],{"class":154},"user",[127,5529,885],{"class":599},[127,5531,1717],{"class":274},[127,5533,762],{"class":274},[127,5535,5536],{"class":144},"EX",[127,5538,3957],{"class":274},[127,5540,1717],{"class":274},[127,5542,5543],{"class":2029}," 3600",[127,5545,2827],{"class":599},[127,5547,5548],{"class":133},"\u002F\u002F 快取 1 小時\n",[127,5550,5551],{"class":129,"line":193},[127,5552,3324],{"class":274},[236,5554,5555],{},[65,5556,5557],{},"Cache 是雙刃劍：",[59,5559,5560,5563],{},[62,5561,5562],{},"用得好：讀取性能大幅提升，資料庫壓力下降",[62,5564,5565],{},"用不好：Cache 與 DB 資料不一致，用戶看到過期資料",[106,5567],{},[55,5569,5571],{"id":5570},"cdn-服務","CDN 服務",[236,5573,5574],{},"CDN 放在用戶與應用服務之間，讓靜態資源（圖片、JS、CSS）從距離用戶最近的節點回應，而不必每次都打到源站。",[117,5576,5579],{"className":5577,"code":5578,"language":798},[796],"台灣用戶 → 台灣 CDN 節點 → （Cache Hit）直接回應\n                           → （Cache Miss）回源站取得後快取\n",[124,5580,5578],{"__ignoreMap":122},[236,5582,5583],{},"適合放上 CDN 的內容：",[59,5585,5586,5589,5592],{},[62,5587,5588],{},"圖片、影片、字型",[62,5590,5591],{},"前端 JS \u002F CSS bundle",[62,5593,5594],{},"不常變動的 API 回應",[106,5596],{},[55,5598,5599],{"id":5599},"性能評估指標",[112,5601,5603],{"id":5602},"回應時間latency","回應時間（Latency）",[236,5605,5606,5607,5610],{},"用戶視角的指標，",[65,5608,5609],{},"越低越好","。",[236,5612,5613],{},"從用戶發出請求到收到回應的完整時間，包含：",[117,5615,5618],{"className":5616,"code":5617,"language":798},[796],"網路傳輸 → 應用服務處理 → DB 查詢 → 回傳結果\n",[124,5619,5617],{"__ignoreMap":122},[236,5621,5622,5623,5626],{},"實務上常用 ",[65,5624,5625],{},"p99","（第 99 百分位）來衡量，代表 99% 的請求都在這個時間內完成，比平均值更能反映真實體驗。",[112,5628,5630],{"id":5629},"吞吐量throughput-qps","吞吐量（Throughput \u002F QPS）",[236,5632,5633,5634,5610],{},"開發者視角的指標，",[65,5635,5636],{},"越高越好",[236,5638,5639],{},"代表系統在單位時間內能處理的請求數量：",[920,5641,5642,5654],{},[923,5643,5644],{},[926,5645,5646,5649,5652],{},[929,5647,5648],{},"單位",[929,5650,5651],{},"全名",[929,5653,3856],{},[936,5655,5656,5667,5678],{},[926,5657,5658,5661,5664],{},[941,5659,5660],{},"QPS",[941,5662,5663],{},"Queries Per Second",[941,5665,5666],{},"一般 Web API",[926,5668,5669,5672,5675],{},[941,5670,5671],{},"TPS",[941,5673,5674],{},"Transactions Per Second",[941,5676,5677],{},"資料庫、金融系統",[926,5679,5680,5683,5686],{},[941,5681,5682],{},"HPS",[941,5684,5685],{},"HTTP Requests Per Second",[941,5687,5688],{},"HTTP 服務",[236,5690,5691],{},[65,5692,5693],{},"計算公式：",[117,5695,5698],{"className":5696,"code":5697,"language":798},[796],"QPS = 併發數 ÷ 平均回應時間\n",[124,5699,5697],{"__ignoreMap":122},[236,5701,5702],{},"例如系統可承受 1000 併發、平均回應時間 1 秒，則 QPS = 1000。",[106,5704],{},[55,5706,5707],{"id":5707},"優化路線總結",[920,5709,5710,5720],{},[923,5711,5712],{},[926,5713,5714,5717],{},[929,5715,5716],{},"層級",[929,5718,5719],{},"優化方向",[936,5721,5722,5730,5738,5746],{},[926,5723,5724,5727],{},[941,5725,5726],{},"應用層",[941,5728,5729],{},"減少運算、優化 I\u002FO、連線池、非同步處理",[926,5731,5732,5735],{},[941,5733,5734],{},"資料庫層",[941,5736,5737],{},"索引設計、減少鎖的範圍與持有時間",[926,5739,5740,5743],{},[941,5741,5742],{},"Cache 層",[941,5744,5745],{},"熱點資料快取、合理設定 TTL",[926,5747,5748,5751],{},[941,5749,5750],{},"CDN 層",[941,5752,5753],{},"靜態資源卸載、縮短傳輸路徑",[236,5755,5756,5757,5760],{},"高性能系統的本質很簡單：",[65,5758,5759],{},"以最少的資源做最多的事情。"," 從單機把每一層調好，才有資格談分散式。",[1564,5762,5763],{},"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 .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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":122,"searchDepth":137,"depth":137,"links":5765},[5766,5767,5768,5772,5776,5777,5778,5782],{"id":5080,"depth":137,"text":5081},{"id":5094,"depth":137,"text":5094},{"id":5120,"depth":137,"text":5120,"children":5769},[5770,5771],{"id":5132,"depth":158,"text":5132},{"id":5149,"depth":158,"text":5150},{"id":5293,"depth":137,"text":5293,"children":5773},[5774,5775],{"id":5299,"depth":158,"text":5300},{"id":5341,"depth":158,"text":5342},{"id":5387,"depth":137,"text":5388},{"id":5570,"depth":137,"text":5571},{"id":5599,"depth":137,"text":5599,"children":5779},[5780,5781],{"id":5602,"depth":158,"text":5603},{"id":5629,"depth":158,"text":5630},{"id":5707,"depth":137,"text":5707},"2025-09-18","拆解單機系統的優化路線，以及用回應時間與吞吐量來衡量系統性能。","\u002Fimages\u002Fsingle-machine.jpg",{},{"title":26,"description":5784},"9ChEms2oPjTPbuK8gwhXh4BcJkN8XFANcZEtaLR2hu4",{"id":5790,"title":30,"author":5791,"body":5793,"date":6413,"description":6414,"extension":1596,"externalUrl":37,"image":6415,"meta":6416,"minRead":173,"navigation":41,"path":31,"seo":6417,"stem":32,"__hash__":6418},"blog\u002Farticles\u002Fssr.md",{"name":48,"avatar":5792},{"src":50,"alt":48},{"type":52,"value":5794,"toc":6397},[5795,5799,5810,5812,5815,5886,5888,5891,5897,5899,5902,5922,5924,5927,5954,5956,5959,6143,6145,6148,6152,6158,6162,6169,6173,6176,6178,6180,6191,6193,6196,6261,6263,6267,6386,6394],[55,5796,5798],{"id":5797},"什麼是-ssr","什麼是 SSR？",[236,5800,5801,5802,5805,5806,5809],{},"SSR 即",[65,5803,5804],{},"伺服器端渲染（Server-Side Rendering）","，指網頁的 HTML 在",[65,5807,5808],{},"伺服器","上產生完整內容後，再傳送給瀏覽器顯示。",[106,5811],{},[55,5813,5814],{"id":5814},"渲染方式比較",[920,5816,5817,5830],{},[923,5818,5819],{},[926,5820,5821,5824,5827],{},[929,5822,5823],{},"項目",[929,5825,5826],{},"SSR",[929,5828,5829],{},"CSR（Client-Side Rendering）",[936,5831,5832,5842,5853,5864,5875],{},[926,5833,5834,5837,5839],{},[941,5835,5836],{},"HTML 生成位置",[941,5838,5808],{},[941,5840,5841],{},"瀏覽器",[926,5843,5844,5847,5850],{},[941,5845,5846],{},"首屏速度",[941,5848,5849],{},"快",[941,5851,5852],{},"慢（需等 JS 執行）",[926,5854,5855,5858,5861],{},[941,5856,5857],{},"SEO",[941,5859,5860],{},"友好",[941,5862,5863],{},"不友好（爬蟲難以讀取）",[926,5865,5866,5869,5872],{},[941,5867,5868],{},"伺服器負擔",[941,5870,5871],{},"較重",[941,5873,5874],{},"較輕",[926,5876,5877,5880,5883],{},[941,5878,5879],{},"互動性",[941,5881,5882],{},"較低（需搭配 Hydration）",[941,5884,5885],{},"高",[106,5887],{},[55,5889,5890],{"id":5890},"運作流程",[117,5892,5895],{"className":5893,"code":5894,"language":798},[796],"使用者請求頁面\n      ↓\n伺服器執行程式碼，取得資料\n      ↓\n伺服器產生完整 HTML\n      ↓\n回傳 HTML 給瀏覽器\n      ↓\n瀏覽器顯示頁面（可立即看到內容）\n      ↓\nJavaScript 載入，進行 Hydration（賦予互動能力）\n",[124,5896,5894],{"__ignoreMap":122},[106,5898],{},[55,5900,5901],{"id":5901},"優點",[59,5903,5904,5910,5916],{},[62,5905,5906,5909],{},[65,5907,5908],{},"首屏載入快","：用戶更快看到內容，體驗好",[62,5911,5912,5915],{},[65,5913,5914],{},"SEO 友好","：搜尋引擎爬蟲可直接讀取完整 HTML",[62,5917,5918,5921],{},[65,5919,5920],{},"社群分享預覽正常","：Open Graph 標籤可被正確解析",[106,5923],{},[55,5925,5926],{"id":5926},"缺點",[59,5928,5929,5935,5941],{},[62,5930,5931,5934],{},[65,5932,5933],{},"伺服器負擔重","：每次請求都需伺服器運算",[62,5936,5937,5940],{},[65,5938,5939],{},"TTFB 較慢","（Time To First Byte）：伺服器需先處理完才回傳",[62,5942,5943,5946,5947,1639,5950,5953],{},[65,5944,5945],{},"開發複雜度高","：需注意程式碼在伺服器與瀏覽器環境的差異（如 ",[124,5948,5949],{},"window",[124,5951,5952],{},"document"," 不存在於伺服器端）",[106,5955],{},[55,5957,5958],{"id":5958},"開發需注意",[59,5960,5961],{},[62,5962,5963,5966,5967,5969,5970,5973,5976,5977,5979,5980,5983,5984,5995],{},[65,5964,5965],{},"儲存 Token 的環境差異","：",[124,5968,4029],{}," 只存在於瀏覽器，伺服器端無法存取。",[5971,5972],"br",{},[65,5974,5975],{},"解法：改用 Cookie 存 token","\nCookie 會隨每個 HTTP request 自動帶到伺服器，server 和 client 都能讀取。",[5971,5978],{},"Nuxt 提供內建的 ",[124,5981,5982],{},"useCookie","，統一處理兩端差異：",[59,5985,5986,5989],{},[62,5987,5988],{},"Server：從 request header 讀取 cookie，寫入放進 response header",[62,5990,5991,5992],{},"Client：直接讀寫 ",[124,5993,5994],{},"document.cookie",[117,5996,5998],{"className":3917,"code":5997,"language":3919,"meta":122,"style":122},"const token = useCookie('token', {\n  maxAge: 60 * 60 * 24, \u002F\u002F 1 天\n  httpOnly: true,        \u002F\u002F 防 XSS\n  secure: true,          \u002F\u002F 只走 HTTPS\n})\n\ntoken.value = 'abc123' \u002F\u002F 寫入\nconsole.log(token.value) \u002F\u002F 讀取\ntoken.value = null       \u002F\u002F 清除\n",[124,5999,6000,6025,6048,6063,6077,6083,6087,6108,6127],{"__ignoreMap":122},[127,6001,6002,6004,6007,6009,6012,6014,6016,6019,6021,6023],{"class":129,"line":130},[127,6003,3518],{"class":1924},[127,6005,6006],{"class":154}," token ",[127,6008,814],{"class":274},[127,6010,6011],{"class":869}," useCookie",[127,6013,1934],{"class":154},[127,6015,3957],{"class":274},[127,6017,6018],{"class":144},"token",[127,6020,3957],{"class":274},[127,6022,1717],{"class":274},[127,6024,1698],{"class":274},[127,6026,6027,6030,6032,6034,6036,6038,6040,6043,6045],{"class":129,"line":137},[127,6028,6029],{"class":599},"  maxAge",[127,6031,618],{"class":274},[127,6033,4488],{"class":2029},[127,6035,4485],{"class":274},[127,6037,4488],{"class":2029},[127,6039,4485],{"class":274},[127,6041,6042],{"class":2029}," 24",[127,6044,1717],{"class":274},[127,6046,6047],{"class":133}," \u002F\u002F 1 天\n",[127,6049,6050,6053,6055,6058,6060],{"class":129,"line":158},[127,6051,6052],{"class":599},"  httpOnly",[127,6054,618],{"class":274},[127,6056,6057],{"class":1189}," true",[127,6059,1717],{"class":274},[127,6061,6062],{"class":133},"        \u002F\u002F 防 XSS\n",[127,6064,6065,6068,6070,6072,6074],{"class":129,"line":167},[127,6066,6067],{"class":599},"  secure",[127,6069,618],{"class":274},[127,6071,6057],{"class":1189},[127,6073,1717],{"class":274},[127,6075,6076],{"class":133},"          \u002F\u002F 只走 HTTPS\n",[127,6078,6079,6081],{"class":129,"line":173},[127,6080,3491],{"class":274},[127,6082,3970],{"class":154},[127,6084,6085],{"class":129,"line":179},[127,6086,170],{"emptyLinePlaceholder":41},[127,6088,6089,6091,6093,6096,6098,6100,6103,6105],{"class":129,"line":193},[127,6090,6018],{"class":154},[127,6092,1711],{"class":274},[127,6094,6095],{"class":154},"value ",[127,6097,814],{"class":274},[127,6099,762],{"class":274},[127,6101,6102],{"class":144},"abc123",[127,6104,3957],{"class":274},[127,6106,6107],{"class":133}," \u002F\u002F 寫入\n",[127,6109,6110,6113,6115,6117,6119,6121,6124],{"class":129,"line":201},[127,6111,6112],{"class":154},"console",[127,6114,1711],{"class":274},[127,6116,3473],{"class":869},[127,6118,3939],{"class":154},[127,6120,1711],{"class":274},[127,6122,6123],{"class":154},"value) ",[127,6125,6126],{"class":133},"\u002F\u002F 讀取\n",[127,6128,6129,6131,6133,6135,6137,6140],{"class":129,"line":206},[127,6130,6018],{"class":154},[127,6132,1711],{"class":274},[127,6134,6095],{"class":154},[127,6136,814],{"class":274},[127,6138,6139],{"class":274}," null",[127,6141,6142],{"class":133},"       \u002F\u002F 清除\n",[106,6144],{},[55,6146,6147],{"id":6147},"相關概念",[112,6149,6151],{"id":6150},"hydration","Hydration",[236,6153,6154,6155,5610],{},"SSR 傳回靜態 HTML 後，瀏覽器的 JavaScript 接管並賦予互動能力的過程，稱為 ",[65,6156,6157],{},"Hydration（水合）",[112,6159,6161],{"id":6160},"ssgstatic-site-generation","SSG（Static Site Generation）",[236,6163,6164,6165,6168],{},"在",[65,6166,6167],{},"建置時","預先產生 HTML，而非每次請求時才產生。適合內容不常變動的頁面。",[112,6170,6172],{"id":6171},"isrincremental-static-regeneration","ISR（Incremental Static Regeneration）",[236,6174,6175],{},"Next.js 提供的功能，結合 SSG 與 SSR，可設定頁面定期重新產生。",[106,6177],{},[55,6179,3856],{"id":3856},[59,6181,6182,6185,6188],{},[62,6183,6184],{},"需要 SEO 的網站（部落格、電商、新聞）",[62,6186,6187],{},"首屏效能要求高的頁面",[62,6189,6190],{},"內容依賴使用者身份或即時資料的頁面",[106,6192],{},[55,6194,6195],{"id":6195},"常用框架",[920,6197,6198,6211],{},[923,6199,6200],{},[926,6201,6202,6205,6208],{},[929,6203,6204],{},"框架",[929,6206,6207],{},"基於",[929,6209,6210],{},"語言",[936,6212,6213,6226,6238,6250],{},[926,6214,6215,6220,6223],{},[941,6216,6217],{},[65,6218,6219],{},"Nuxt.js",[941,6221,6222],{},"Vue",[941,6224,6225],{},"JavaScript \u002F TypeScript",[926,6227,6228,6233,6236],{},[941,6229,6230],{},[65,6231,6232],{},"SvelteKit",[941,6234,6235],{},"Svelte",[941,6237,6225],{},[926,6239,6240,6245,6248],{},[941,6241,6242],{},[65,6243,6244],{},"Next.js",[941,6246,6247],{},"React",[941,6249,6225],{},[926,6251,6252,6257,6259],{},[941,6253,6254],{},[65,6255,6256],{},"Remix",[941,6258,6247],{},[941,6260,6225],{},[106,6262],{},[55,6264,6266],{"id":6265},"程式碼範例nuxtjs","程式碼範例（Nuxt.js）",[117,6268,6272],{"className":6269,"code":6270,"language":6271,"meta":122,"style":122},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003C!-- pages\u002Findex.vue -->\n\n\u003Cscript setup lang=\"ts\">\nconst { data } = await useFetch('https:\u002F\u002Fapi.example.com\u002Fdata')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>{{ data.title }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","vue",[124,6273,6274,6279,6283,6308,6337,6346,6350,6359,6378],{"__ignoreMap":122},[127,6275,6276],{"class":129,"line":130},[127,6277,6278],{"class":133},"\u003C!-- pages\u002Findex.vue -->\n",[127,6280,6281],{"class":129,"line":137},[127,6282,170],{"emptyLinePlaceholder":41},[127,6284,6285,6288,6291,6294,6297,6299,6301,6303,6305],{"class":129,"line":158},[127,6286,6287],{"class":274},"\u003C",[127,6289,6290],{"class":599},"script",[127,6292,6293],{"class":1924}," setup",[127,6295,6296],{"class":1924}," lang",[127,6298,814],{"class":274},[127,6300,275],{"class":274},[127,6302,3919],{"class":144},[127,6304,275],{"class":274},[127,6306,6307],{"class":274},">\n",[127,6309,6310,6312,6314,6317,6319,6321,6323,6326,6328,6330,6333,6335],{"class":129,"line":167},[127,6311,3518],{"class":1924},[127,6313,1894],{"class":274},[127,6315,6316],{"class":154}," data ",[127,6318,3491],{"class":274},[127,6320,1990],{"class":274},[127,6322,3526],{"class":1340},[127,6324,6325],{"class":869}," useFetch",[127,6327,1934],{"class":154},[127,6329,3957],{"class":274},[127,6331,6332],{"class":144},"https:\u002F\u002Fapi.example.com\u002Fdata",[127,6334,3957],{"class":274},[127,6336,3970],{"class":154},[127,6338,6339,6342,6344],{"class":129,"line":173},[127,6340,6341],{"class":274},"\u003C\u002F",[127,6343,6290],{"class":599},[127,6345,6307],{"class":274},[127,6347,6348],{"class":129,"line":179},[127,6349,170],{"emptyLinePlaceholder":41},[127,6351,6352,6354,6357],{"class":129,"line":193},[127,6353,6287],{"class":274},[127,6355,6356],{"class":599},"template",[127,6358,6307],{"class":274},[127,6360,6361,6364,6367,6369,6372,6374,6376],{"class":129,"line":201},[127,6362,6363],{"class":274},"  \u003C",[127,6365,6366],{"class":599},"div",[127,6368,1516],{"class":274},[127,6370,6371],{"class":154},"{{ data.title }}",[127,6373,6341],{"class":274},[127,6375,6366],{"class":599},[127,6377,6307],{"class":274},[127,6379,6380,6382,6384],{"class":129,"line":206},[127,6381,6341],{"class":274},[127,6383,6356],{"class":599},[127,6385,6307],{"class":274},[233,6387,6388],{},[236,6389,6390,6393],{},[124,6391,6392],{},"useFetch"," 在 SSR 模式下會於伺服器端執行，取得資料後渲染完整 HTML 再回傳給瀏覽器；切換頁面時則在客戶端執行。",[1564,6395,6396],{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}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 .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}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 .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}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}",{"title":122,"searchDepth":137,"depth":137,"links":6398},[6399,6400,6401,6402,6403,6404,6405,6410,6411,6412],{"id":5797,"depth":137,"text":5798},{"id":5814,"depth":137,"text":5814},{"id":5890,"depth":137,"text":5890},{"id":5901,"depth":137,"text":5901},{"id":5926,"depth":137,"text":5926},{"id":5958,"depth":137,"text":5958},{"id":6147,"depth":137,"text":6147,"children":6406},[6407,6408,6409],{"id":6150,"depth":158,"text":6151},{"id":6160,"depth":158,"text":6161},{"id":6171,"depth":158,"text":6172},{"id":3856,"depth":137,"text":3856},{"id":6195,"depth":137,"text":6195},{"id":6265,"depth":137,"text":6266},"2025-08-02","介紹伺服器端渲染的運作原理、與 CSR 的差異、優缺點，以及在 Nuxt.js 開發中需要注意的事項。","\u002Fimages\u002Fssr.jpg",{},{"title":30,"description":6414},"hw4A-FXUQKCuF4DmsKU8bH8c8xlfqReYj9Uq6rQgsQc",{"id":6420,"title":18,"author":6421,"body":6423,"date":7139,"description":7140,"extension":1596,"externalUrl":37,"image":7141,"meta":7142,"minRead":1128,"navigation":41,"path":19,"seo":7143,"stem":20,"__hash__":7144},"blog\u002Farticles\u002Fllm-to-agent.md",{"name":48,"avatar":6422},{"src":50,"alt":48},{"type":52,"value":6424,"toc":7124},[6425,6429,6435,6438,6441,6447,6458,6460,6464,6470,6476,6479,6482,6516,6519,6521,6525,6534,6537,6543,6546,6549,6551,6555,6558,6562,6565,6571,6575,6578,6584,6588,6591,6597,6600,6602,6606,6613,6616,6622,6625,6747,6750,6806,6809,6812,6814,6818,6821,6827,6833,6836,6892,6895,6897,6901,6907,6910,6916,6922,6925,6928,6987,6989,6993,7000,7006,7009,7023,7026,7028,7031,7034,7115,7118,7121],[55,6426,6428],{"id":6427},"llm-是什麼","LLM 是什麼？",[236,6430,6431,6432,5610],{},"LLM（Large Language Model，大型語言模型）的核心能力只有一件事：",[65,6433,6434],{},"預測下一個 token",[236,6436,6437],{},"給定一段輸入文字，模型計算「下一個最可能出現的詞」，再把這個詞接到輸入後面，繼續預測下下一個，如此循環，直到產生完整回覆。",[236,6439,6440],{},"這個過程看似簡單，但在海量語料上訓練後，模型學會了語言規律、邏輯推理、知識記憶。GPT、Claude、Gemini，背後都是這套機制。",[117,6442,6445],{"className":6443,"code":6444,"language":798},[796],"輸入：「台灣的首都是」\n預測：「台」→「北」→「市」→「。」\n輸出：「台北市。」\n",[124,6446,6444],{"__ignoreMap":122},[236,6448,6449,6450,6453,6454,6457],{},"關鍵在於，LLM ",[65,6451,6452],{},"沒有記憶","，也",[65,6455,6456],{},"不會主動做任何事","。它只是一個函式：給輸入，回傳輸出。所有「智能」的外觀，都是在這個函式之上建構的。",[106,6459],{},[55,6461,6463],{"id":6462},"token模型的基本單位","Token：模型的基本單位",[236,6465,6466,6467,6469],{},"模型不是逐字元讀文字，而是把文字切成 ",[65,6468,6018],{},"（語言片段）再處理。",[117,6471,6474],{"className":6472,"code":6473,"language":798},[796],"\"Hello, world!\" → [\"Hello\", \",\", \" world\", \"!\"]  → 4 tokens\n\"你好世界\"       → [\"你\", \"好\", \"世\", \"界\"]        → 4 tokens\n\"PostgreSQL\"     → [\"Post\", \"gre\", \"SQL\"]          → 3 tokens\n",[124,6475,6473],{"__ignoreMap":122},[236,6477,6478],{},"Token 的切分取決於模型使用的 tokenizer，不同語言的效率差很多。英文大約 1 個單字 = 1～2 tokens；中文則通常每個字就是 1 token。",[236,6480,6481],{},"Token 在工程上有兩個重要影響：",[920,6483,6484,6494],{},[923,6485,6486],{},[926,6487,6488,6491],{},[929,6489,6490],{},"面向",[929,6492,6493],{},"影響",[936,6495,6496,6506],{},[926,6497,6498,6503],{},[941,6499,6500],{},[65,6501,6502],{},"費用",[941,6504,6505],{},"API 通常按 token 計費（輸入 + 輸出分開算）",[926,6507,6508,6513],{},[941,6509,6510],{},[65,6511,6512],{},"限制",[941,6514,6515],{},"每次呼叫有 context window 上限（例如 200K tokens）",[236,6517,6518],{},"理解 token 讓你在設計 prompt 時更有感知，知道什麼樣的寫法會讓費用暴增。",[106,6520],{},[55,6522,6524],{"id":6523},"context模型唯一能看見的東西","Context：模型唯一能看見的東西",[236,6526,6527,6528,6530,6531,5610],{},"LLM ",[65,6529,6452],{},"。每次呼叫，模型只能看到你這次傳進去的內容，稱為 ",[65,6532,6533],{},"context window",[236,6535,6536],{},"Context 的內容通常包含：",[117,6538,6541],{"className":6539,"code":6540,"language":798},[796],"System Prompt   → 角色設定、行為規則\n對話歷史        → 過去幾輪的 user \u002F assistant 訊息\n當前輸入        → 使用者這次說的話\n工具回傳結果    → 如果有用到 tool，結果也會塞進來\n",[124,6542,6540],{"__ignoreMap":122},[236,6544,6545],{},"「AI 記得你說過什麼」其實是應用層把歷史對話每次都一起送進去，不是模型真的記得。",[236,6547,6548],{},"Context window 的大小決定了模型能「記住」多長的對話。超過上限，最早的訊息就會被截掉。這是 AI 應用設計時最常踩的坑之一：對話太長導致模型「忘記」前面的指令或資料。",[106,6550],{},[55,6552,6554],{"id":6553},"prompt和模型溝通的介面","Prompt：和模型溝通的介面",[236,6556,6557],{},"Prompt 是你傳給模型的輸入。寫得好，模型表現好；寫得差，結果一塌糊塗。",[112,6559,6561],{"id":6560},"system-prompt","System Prompt",[236,6563,6564],{},"定義模型的角色與行為邊界：",[117,6566,6569],{"className":6567,"code":6568,"language":798},[796],"你是一個後端工程師助手，用繁體中文回答問題。\n回答要簡潔，只給關鍵結論，不用解釋廢話。\n如果不確定，就說不知道，不要亂猜。\n",[124,6570,6568],{"__ignoreMap":122},[112,6572,6574],{"id":6573},"few-shot-prompting","Few-shot Prompting",[236,6576,6577],{},"提供幾個範例，讓模型學習你期望的輸出格式：",[117,6579,6582],{"className":6580,"code":6581,"language":798},[796],"將以下錯誤訊息翻譯成繁體中文，格式如下：\n原文：Connection refused\n翻譯：連線被拒絕\n\n原文：Timeout exceeded\n翻譯：連線逾時\n\n原文：Permission denied\n翻譯：\n",[124,6583,6581],{"__ignoreMap":122},[112,6585,6587],{"id":6586},"chain-of-thought","Chain-of-Thought",[236,6589,6590],{},"要求模型逐步推理，再給出答案，對複雜問題準確率明顯提升：",[117,6592,6595],{"className":6593,"code":6594,"language":798},[796],"請一步一步思考，然後給出結論。\n",[124,6596,6594],{"__ignoreMap":122},[236,6598,6599],{},"Prompt 工程本質上是在告訴模型「你是誰、你要做什麼、你怎麼做」。越清楚，模型越可靠。",[106,6601],{},[55,6603,6605],{"id":6604},"tool讓模型有手腳","Tool：讓模型有手腳",[236,6607,6608,6609,6612],{},"LLM 本身只能輸出文字，無法執行任何操作。",[65,6610,6611],{},"Tool（工具調用）"," 讓模型能呼叫外部函式，突破這個限制。",[236,6614,6615],{},"流程如下：",[117,6617,6620],{"className":6618,"code":6619,"language":798},[796],"1. 你定義一組工具（函式簽名 + 描述）\n2. 把工具定義連同 prompt 一起送給模型\n3. 模型決定要呼叫哪個工具，輸出結構化的呼叫指令\n4. 應用層執行這個函式，取得結果\n5. 把結果塞回 context，模型繼續生成回覆\n",[124,6621,6619],{"__ignoreMap":122},[236,6623,6624],{},"範例：定義一個查天氣的工具",[117,6626,6630],{"className":6627,"code":6628,"language":6629,"meta":122,"style":122},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"name\": \"get_weather\",\n  \"description\": \"查詢指定城市的目前天氣\",\n  \"parameters\": {\n    \"city\": { \"type\": \"string\", \"description\": \"城市名稱\" }\n  }\n}\n","json",[124,6631,6632,6636,6656,6676,6689,6739,6743],{"__ignoreMap":122},[127,6633,6634],{"class":129,"line":130},[127,6635,2893],{"class":274},[127,6637,6638,6641,6643,6645,6647,6649,6652,6654],{"class":129,"line":137},[127,6639,6640],{"class":274},"  \"",[127,6642,1175],{"class":1924},[127,6644,275],{"class":274},[127,6646,618],{"class":274},[127,6648,838],{"class":274},[127,6650,6651],{"class":144},"get_weather",[127,6653,275],{"class":274},[127,6655,1792],{"class":274},[127,6657,6658,6660,6663,6665,6667,6669,6672,6674],{"class":129,"line":158},[127,6659,6640],{"class":274},[127,6661,6662],{"class":1924},"description",[127,6664,275],{"class":274},[127,6666,618],{"class":274},[127,6668,838],{"class":274},[127,6670,6671],{"class":144},"查詢指定城市的目前天氣",[127,6673,275],{"class":274},[127,6675,1792],{"class":274},[127,6677,6678,6680,6683,6685,6687],{"class":129,"line":167},[127,6679,6640],{"class":274},[127,6681,6682],{"class":1924},"parameters",[127,6684,275],{"class":274},[127,6686,618],{"class":274},[127,6688,1698],{"class":274},[127,6690,6691,6694,6697,6699,6701,6703,6705,6708,6710,6712,6714,6717,6719,6721,6723,6725,6727,6729,6731,6734,6736],{"class":129,"line":173},[127,6692,6693],{"class":274},"    \"",[127,6695,6696],{"class":140},"city",[127,6698,275],{"class":274},[127,6700,618],{"class":274},[127,6702,1894],{"class":274},[127,6704,838],{"class":274},[127,6706,6707],{"class":2029},"type",[127,6709,275],{"class":274},[127,6711,618],{"class":274},[127,6713,838],{"class":274},[127,6715,6716],{"class":144},"string",[127,6718,275],{"class":274},[127,6720,1717],{"class":274},[127,6722,838],{"class":274},[127,6724,6662],{"class":2029},[127,6726,275],{"class":274},[127,6728,618],{"class":274},[127,6730,838],{"class":274},[127,6732,6733],{"class":144},"城市名稱",[127,6735,275],{"class":274},[127,6737,6738],{"class":274}," }\n",[127,6740,6741],{"class":129,"line":179},[127,6742,3742],{"class":274},[127,6744,6745],{"class":129,"line":193},[127,6746,3324],{"class":274},[236,6748,6749],{},"模型收到「台北現在幾度？」，就會輸出：",[117,6751,6753],{"className":6627,"code":6752,"language":6629,"meta":122,"style":122},"{ \"tool\": \"get_weather\", \"arguments\": { \"city\": \"台北\" } }\n",[124,6754,6755],{"__ignoreMap":122},[127,6756,6757,6759,6761,6764,6766,6768,6770,6772,6774,6776,6778,6781,6783,6785,6787,6789,6791,6793,6795,6797,6800,6802,6804],{"class":129,"line":130},[127,6758,841],{"class":274},[127,6760,838],{"class":274},[127,6762,6763],{"class":1924},"tool",[127,6765,275],{"class":274},[127,6767,618],{"class":274},[127,6769,838],{"class":274},[127,6771,6651],{"class":144},[127,6773,275],{"class":274},[127,6775,1717],{"class":274},[127,6777,838],{"class":274},[127,6779,6780],{"class":1924},"arguments",[127,6782,275],{"class":274},[127,6784,618],{"class":274},[127,6786,1894],{"class":274},[127,6788,838],{"class":274},[127,6790,6696],{"class":140},[127,6792,275],{"class":274},[127,6794,618],{"class":274},[127,6796,838],{"class":274},[127,6798,6799],{"class":144},"台北",[127,6801,275],{"class":274},[127,6803,1900],{"class":274},[127,6805,6738],{"class":274},[236,6807,6808],{},"應用層執行後把結果傳回去，模型再組合成自然語言回覆給用戶。",[236,6810,6811],{},"Tool 讓 LLM 從「只會說話」變成「說話 + 做事」。",[106,6813],{},[55,6815,6817],{"id":6816},"mcptool-的標準化協議","MCP：Tool 的標準化協議",[236,6819,6820],{},"當你的系統有幾十個工具，每個都要自己寫定義、串接、維護，會很累。",[236,6822,6823,6826],{},[65,6824,6825],{},"MCP（Model Context Protocol）"," 是 Anthropic 提出的開放協議，定義了「模型如何和外部工具溝通」的標準格式，讓工具可以被任何支援 MCP 的模型複用。",[117,6828,6831],{"className":6829,"code":6830,"language":798},[796],"LLM ←→ MCP Client ←→ MCP Server ←→ 資料庫 \u002F API \u002F 檔案系統\n",[124,6832,6830],{"__ignoreMap":122},[236,6834,6835],{},"MCP Server 暴露三種能力：",[920,6837,6838,6851],{},[923,6839,6840],{},[926,6841,6842,6845,6848],{},[929,6843,6844],{},"類型",[929,6846,6847],{},"說明",[929,6849,6850],{},"範例",[936,6852,6853,6866,6879],{},[926,6854,6855,6860,6863],{},[941,6856,6857],{},[65,6858,6859],{},"Tools",[941,6861,6862],{},"模型可以呼叫的函式",[941,6864,6865],{},"查資料庫、送 API",[926,6867,6868,6873,6876],{},[941,6869,6870],{},[65,6871,6872],{},"Resources",[941,6874,6875],{},"模型可以讀取的資料",[941,6877,6878],{},"讀檔案、讀文件",[926,6880,6881,6886,6889],{},[941,6882,6883],{},[65,6884,6885],{},"Prompts",[941,6887,6888],{},"預設的 prompt 範本",[941,6890,6891],{},"分析報表的固定格式",[236,6893,6894],{},"有了 MCP，工具開發者只需要寫一次 MCP Server，任何相容的 AI 應用都能直接使用。類似 USB 統一了硬體接口的概念。",[106,6896],{},[55,6898,6900],{"id":6899},"agent自主完成任務的執行者","Agent：自主完成任務的執行者",[236,6902,6903,6904,5610],{},"把上面所有東西組合起來，就是 ",[65,6905,6906],{},"Agent",[236,6908,6909],{},"Agent 的核心是一個循環（ReAct Loop）：",[117,6911,6914],{"className":6912,"code":6913,"language":798},[796],"思考（Think）→ 行動（Act）→ 觀察結果（Observe）→ 再思考 → …\n",[124,6915,6913],{"__ignoreMap":122},[117,6917,6920],{"className":6918,"code":6919,"language":798},[796],"用戶：「幫我分析這週的銷售數據，找出異常。」\n\nAgent 循環：\n  第 1 輪：呼叫 get_sales_data(week=\"this_week\")\n  第 2 輪：讀取資料，呼叫 calculate_stats()\n  第 3 輪：發現週三數字異常低，呼叫 get_order_logs(date=\"週三\")\n  第 4 輪：分析日誌，整理出結論\n  輸出：「週三下午 3 點系統有 2 小時停機，導致 47 筆訂單流失，影響營業額約 NT$12 萬。」\n",[124,6921,6919],{"__ignoreMap":122},[236,6923,6924],{},"Agent 不需要人在每一步指示，它會自己決定下一步要做什麼，一直到任務完成。",[236,6926,6927],{},"與一般的「問答模型」最大的差別：",[920,6929,6930,6941],{},[923,6931,6932],{},[926,6933,6934,6936,6939],{},[929,6935],{},[929,6937,6938],{},"問答模型",[929,6940,6906],{},[936,6942,6943,6954,6965,6976],{},[926,6944,6945,6948,6951],{},[941,6946,6947],{},"執行方式",[941,6949,6950],{},"一問一答",[941,6952,6953],{},"自主多步執行",[926,6955,6956,6959,6962],{},[941,6957,6958],{},"工具使用",[941,6960,6961],{},"可以",[941,6963,6964],{},"可以，且自主決定何時用",[926,6966,6967,6970,6973],{},[941,6968,6969],{},"任務長度",[941,6971,6972],{},"單輪",[941,6974,6975],{},"多輪，直到完成",[926,6977,6978,6981,6984],{},[941,6979,6980],{},"需要人介入",[941,6982,6983],{},"每步都需要",[941,6985,6986],{},"只需給目標",[106,6988],{},[55,6990,6992],{"id":6991},"agent-skill可組合的專業能力","Agent Skill：可組合的專業能力",[236,6994,6995,6996,6999],{},"隨著 Agent 的應用越來越複雜，出現了 ",[65,6997,6998],{},"Agent Skill"," 的概念：把某個專業領域的能力封裝成獨立模組，讓 Agent 可以像呼叫工具一樣呼叫「技能」。",[117,7001,7004],{"className":7002,"code":7003,"language":798},[796],"主 Agent（協調者）\n  ├── Skill: 資料分析   → 專門處理數據、統計、圖表\n  ├── Skill: 程式碼生成 → 專門寫 \u002F 審 \u002F 解釋程式碼\n  ├── Skill: 網路搜尋   → 專門搜尋、整理外部資訊\n  └── Skill: 報告撰寫   → 專門把結果整理成文件\n",[124,7005,7003],{"__ignoreMap":122},[236,7007,7008],{},"Skill 和 Tool 的差別：",[59,7010,7011,7017],{},[62,7012,7013,7016],{},[65,7014,7015],{},"Tool","：一個具體的函式，做一件事（查天氣、送 email）",[62,7018,7019,7022],{},[65,7020,7021],{},"Skill","：一個包含 prompt + tools + 邏輯的完整能力模組（分析報表、產生程式碼）",[236,7024,7025],{},"Skill 讓複雜任務可以被分解、分工，不同 Skill 可以平行執行，最後由主 Agent 整合結果。這是目前 AI 應用走向「Multi-Agent 系統」的基礎架構。",[106,7027],{},[55,7029,7030],{"id":7030},"總結",[236,7032,7033],{},"從 LLM 到 Agent，每一層都在解決上一層的限制：",[920,7035,7036,7046],{},[923,7037,7038],{},[926,7039,7040,7043],{},[929,7041,7042],{},"層次",[929,7044,7045],{},"解決的問題",[936,7047,7048,7058,7068,7078,7087,7097,7106],{},[926,7049,7050,7055],{},[941,7051,7052],{},[65,7053,7054],{},"LLM",[941,7056,7057],{},"理解語言、推理、生成文字",[926,7059,7060,7065],{},[941,7061,7062],{},[65,7063,7064],{},"Token \u002F Context",[941,7066,7067],{},"理解模型的輸入邊界與費用結構",[926,7069,7070,7075],{},[941,7071,7072],{},[65,7073,7074],{},"Prompt",[941,7076,7077],{},"讓模型的行為可預測、可控制",[926,7079,7080,7084],{},[941,7081,7082],{},[65,7083,7015],{},[941,7085,7086],{},"讓模型能操作外部系統",[926,7088,7089,7094],{},[941,7090,7091],{},[65,7092,7093],{},"MCP",[941,7095,7096],{},"標準化工具協議，可重複使用",[926,7098,7099,7103],{},[941,7100,7101],{},[65,7102,6906],{},[941,7104,7105],{},"自主多步執行，完成複雜任務",[926,7107,7108,7112],{},[941,7109,7110],{},[65,7111,6998],{},[941,7113,7114],{},"模組化能力，支撐 Multi-Agent 架構",[236,7116,7117],{},"理解這條鏈路，才能在實際開發 AI 應用時做出正確的架構決策：什麼情況下用單輪問答就夠了、什麼情況下需要 Agent、工具要怎麼設計、context 要怎麼管理。",[236,7119,7120],{},"LLM 只是起點，Agent 才是終點。",[1564,7122,7123],{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}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);}",{"title":122,"searchDepth":137,"depth":137,"links":7125},[7126,7127,7128,7129,7134,7135,7136,7137,7138],{"id":6427,"depth":137,"text":6428},{"id":6462,"depth":137,"text":6463},{"id":6523,"depth":137,"text":6524},{"id":6553,"depth":137,"text":6554,"children":7130},[7131,7132,7133],{"id":6560,"depth":158,"text":6561},{"id":6573,"depth":158,"text":6574},{"id":6586,"depth":158,"text":6587},{"id":6604,"depth":137,"text":6605},{"id":6816,"depth":137,"text":6817},{"id":6899,"depth":137,"text":6900},{"id":6991,"depth":137,"text":6992},{"id":7030,"depth":137,"text":7030},"2025-07-20","從 Token、Context、Prompt，到 Tool、MCP、Agent，完整梳理 AI 應用開發的核心概念與底層運作原理。","\u002Fimages\u002Fdata-flow.jpg",{},{"title":18,"description":7140},"i52aRjvH8JEP_i6dc3uplneiyxvN_ougNgYexSfwOks",1781661890755]