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