[{"data":1,"prerenderedAt":2402},["ShallowReactive",2],{"navigation":3,"\u002Farticles\u002Fdaily-snapshot":34,"\u002Farticles\u002Fdaily-snapshot-surround":2399},[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":10,"author":36,"body":40,"date":2391,"description":2392,"extension":2393,"externalUrl":2394,"image":2395,"meta":2396,"minRead":201,"navigation":387,"path":11,"seo":2397,"stem":12,"__hash__":2398},"blog\u002Farticles\u002Fdaily-snapshot.md",{"name":37,"avatar":38},"Gary",{"src":39,"alt":37},"\u002Fimages\u002Fselfie.webp",{"type":41,"value":42,"toc":2376},"minimark",[43,47,51,73,88,91,94,98,105,115,117,121,124,319,321,325,328,1815,1818,1855,1857,1861,1986,1988,1991,1994,2065,2067,2071,2074,2079,2085,2088,2093,2096,2242,2248,2252,2269,2272,2276,2279,2281,2288,2290,2293,2349,2351,2354,2361,2372],[44,45,46],"h2",{"id":46},"問題背景",[48,49,50],"p",{},"訂單管理後台有一個總覽頁面，需要顯示以下統計數字：",[52,53,54,58,61,64,67,70],"ul",{},[55,56,57],"li",{},"當日營業額與訂單數",[55,59,60],{},"新增用戶數",[55,62,63],{},"平均客單價",[55,65,66],{},"熱銷商品 Top 10",[55,68,69],{},"各類別銷售佔比",[55,71,72],{},"各付款方式分佈",[48,74,75,76,80,81,80,84,87],{},"起初資料量小，直接對 ",[77,78,79],"code",{},"orders","、",[77,82,83],{},"order_items",[77,85,86],{},"payments"," 做聚合查詢沒有問題。",[48,89,90],{},"隨著訂單累積，這些查詢開始拖慢整個頁面，每次載入需要數秒，而且每個進入後台的管理員都會觸發一次跨表掃描。",[92,93],"hr",{},[44,95,97],{"id":96},"解法daily-snapshot","解法：Daily Snapshot",[48,99,100,101],{},"核心思路：",[102,103,104],"strong",{},"不在用戶請求時計算，改成每天固定時間預先算好，存進一張 Snapshot Table，查詢時直接讀一筆記錄。",[106,107,112],"pre",{"className":108,"code":110,"language":111},[109],"language-text","每天 00:00 Cron Job 執行\n      ↓\n讀取昨日訂單、商品、付款資料，執行聚合計算\n      ↓\n將結果寫入 daily_snapshots 表（一天一筆）\n      ↓\n前端請求總覽時，直接 SELECT 最新一筆 snapshot\n","text",[77,113,110],{"__ignoreMap":114},"",[92,116],{},[44,118,120],{"id":119},"snapshot-table-設計","Snapshot Table 設計",[48,122,123],{},"Snapshot 除了基本的金額與訂單數，還用 JSON 欄位儲存熱銷商品、類別、付款方式等排行資料：",[106,125,129],{"className":126,"code":127,"language":128,"meta":114,"style":114},"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",[77,130,131,140,154,179,199,219,238,255,275,294,313],{"__ignoreMap":114},[132,133,136],"span",{"class":134,"line":135},"line",1,[132,137,139],{"class":138},"sHwdD","\u002F\u002F Sequelize Model\n",[132,141,143,147,151],{"class":134,"line":142},2,[132,144,146],{"class":145},"sTEyZ","DailySnapshot ",[132,148,150],{"class":149},"sMK4o","=",[132,152,153],{"class":149}," {\n",[132,155,157,161,164,167,170,173,176],{"class":134,"line":156},3,[132,158,160],{"class":159},"swJcz","  date",[132,162,163],{"class":149},":",[132,165,166],{"class":145}," DataTypes",[132,168,169],{"class":149},".",[132,171,172],{"class":145},"DATEONLY",[132,174,175],{"class":149},",",[132,177,178],{"class":138}," \u002F\u002F 唯一鍵，一天一筆\n",[132,180,182,185,187,189,191,194,196],{"class":134,"line":181},4,[132,183,184],{"class":159},"  revenue",[132,186,163],{"class":149},[132,188,166],{"class":145},[132,190,169],{"class":149},[132,192,193],{"class":145},"DECIMAL",[132,195,175],{"class":149},[132,197,198],{"class":138}," \u002F\u002F 當日營業額（paid\u002Fshipped\u002Fdelivered）\n",[132,200,202,205,207,209,211,214,216],{"class":134,"line":201},5,[132,203,204],{"class":159},"  orderCount",[132,206,163],{"class":149},[132,208,166],{"class":145},[132,210,169],{"class":149},[132,212,213],{"class":145},"INTEGER",[132,215,175],{"class":149},[132,217,218],{"class":138}," \u002F\u002F 當日總訂單數\n",[132,220,222,225,227,229,231,233,235],{"class":134,"line":221},6,[132,223,224],{"class":159},"  newUserCount",[132,226,163],{"class":149},[132,228,166],{"class":145},[132,230,169],{"class":149},[132,232,213],{"class":145},[132,234,175],{"class":149},[132,236,237],{"class":138}," \u002F\u002F 當日新增用戶數\n",[132,239,241,244,246,248,250,252],{"class":134,"line":240},7,[132,242,243],{"class":159},"  avgOrderValue",[132,245,163],{"class":149},[132,247,166],{"class":145},[132,249,169],{"class":149},[132,251,193],{"class":145},[132,253,254],{"class":149},",\n",[132,256,258,261,263,265,267,270,272],{"class":134,"line":257},8,[132,259,260],{"class":159},"  topProducts",[132,262,163],{"class":149},[132,264,166],{"class":145},[132,266,169],{"class":149},[132,268,269],{"class":145},"JSON",[132,271,175],{"class":149},[132,273,274],{"class":138}," \u002F\u002F [{ productId, productName, totalRevenue, totalQuantity }]\n",[132,276,278,281,283,285,287,289,291],{"class":134,"line":277},9,[132,279,280],{"class":159},"  topCategories",[132,282,163],{"class":149},[132,284,166],{"class":145},[132,286,169],{"class":149},[132,288,269],{"class":145},[132,290,175],{"class":149},[132,292,293],{"class":138}," \u002F\u002F [{ categoryId, categoryName, totalRevenue, totalQuantity }]\n",[132,295,297,300,302,304,306,308,310],{"class":134,"line":296},10,[132,298,299],{"class":159},"  paymentMethods",[132,301,163],{"class":149},[132,303,166],{"class":145},[132,305,169],{"class":149},[132,307,269],{"class":145},[132,309,175],{"class":149},[132,311,312],{"class":138}," \u002F\u002F [{ method, count, amount }]\n",[132,314,316],{"class":134,"line":315},11,[132,317,318],{"class":149},"};\n",[92,320],{},[44,322,324],{"id":323},"builddailysnapshot-實作","buildDailySnapshot 實作",[48,326,327],{},"以下是實際的計算函式，對四張表同時發出查詢後一次 upsert 進 snapshot table：",[106,329,331],{"className":126,"code":330,"language":128,"meta":114,"style":114},"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",[77,332,333,358,383,389,417,422,433,441,454,475,509,518,529,534,556,578,611,634,664,669,675,715,733,739,753,759,765,771,777,783,789,797,840,848,853,859,870,875,881,887,894,929,936,941,947,958,963,969,975,981,987,993,999,1006,1041,1048,1053,1059,1070,1075,1081,1086,1092,1098,1104,1110,1115,1121,1126,1133,1168,1175,1180,1186,1197,1202,1208,1214,1220,1226,1232,1239,1274,1281,1289,1294,1325,1355,1360,1366,1384,1396,1408,1435,1462,1514,1545,1555,1578,1601,1611,1639,1648,1669,1690,1699,1727,1745,1767,1790,1799,1809],{"__ignoreMap":114},[132,334,335,339,342,345,348,352,355],{"class":134,"line":135},[132,336,338],{"class":337},"s7zQu","import",[132,340,341],{"class":145}," sequelize ",[132,343,344],{"class":337},"from",[132,346,347],{"class":149}," \"",[132,349,351],{"class":350},"sfazB","..\u002Fconfig\u002Fdb.js",[132,353,354],{"class":149},"\"",[132,356,357],{"class":149},";\n",[132,359,360,362,365,368,371,374,376,379,381],{"class":134,"line":142},[132,361,338],{"class":337},[132,363,364],{"class":149}," {",[132,366,367],{"class":145}," DailySnapshot",[132,369,370],{"class":149}," }",[132,372,373],{"class":337}," from",[132,375,347],{"class":149},[132,377,378],{"class":350},"..\u002Fmodels\u002Findex.js",[132,380,354],{"class":149},[132,382,357],{"class":149},[132,384,385],{"class":134,"line":156},[132,386,388],{"emptyLinePlaceholder":387},true,"\n",[132,390,391,394,398,401,405,408,412,415],{"class":134,"line":181},[132,392,393],{"class":337},"export",[132,395,397],{"class":396},"spNyl"," async",[132,399,400],{"class":396}," function",[132,402,404],{"class":403},"s2Zo4"," buildDailySnapshot",[132,406,407],{"class":149},"(",[132,409,411],{"class":410},"sHdIc","targetDate",[132,413,414],{"class":149},")",[132,416,153],{"class":149},[132,418,419],{"class":134,"line":201},[132,420,421],{"class":138},"  \u002F\u002F 預設計算昨天\n",[132,423,424,427,430],{"class":134,"line":221},[132,425,426],{"class":396},"  const",[132,428,429],{"class":145}," date",[132,431,432],{"class":149}," =\n",[132,434,435,438],{"class":134,"line":240},[132,436,437],{"class":145},"    targetDate",[132,439,440],{"class":149}," ||\n",[132,442,443,446,449,452],{"class":134,"line":257},[132,444,445],{"class":159},"    (",[132,447,448],{"class":149},"()",[132,450,451],{"class":396}," =>",[132,453,153],{"class":149},[132,455,456,459,462,465,468,471,473],{"class":134,"line":277},[132,457,458],{"class":396},"      const",[132,460,461],{"class":145}," d",[132,463,464],{"class":149}," =",[132,466,467],{"class":149}," new",[132,469,470],{"class":403}," Date",[132,472,448],{"class":159},[132,474,357],{"class":149},[132,476,477,480,482,485,487,490,492,495,498,501,505,507],{"class":134,"line":296},[132,478,479],{"class":145},"      d",[132,481,169],{"class":149},[132,483,484],{"class":403},"setDate",[132,486,407],{"class":159},[132,488,489],{"class":145},"d",[132,491,169],{"class":149},[132,493,494],{"class":403},"getDate",[132,496,497],{"class":159},"() ",[132,499,500],{"class":149},"-",[132,502,504],{"class":503},"sbssI"," 1",[132,506,414],{"class":159},[132,508,357],{"class":149},[132,510,511,514,516],{"class":134,"line":315},[132,512,513],{"class":337},"      return",[132,515,461],{"class":145},[132,517,357],{"class":149},[132,519,521,524,527],{"class":134,"line":520},12,[132,522,523],{"class":149},"    }",[132,525,526],{"class":159},")()",[132,528,357],{"class":149},[132,530,532],{"class":134,"line":531},13,[132,533,388],{"emptyLinePlaceholder":387},[132,535,537,539,542,544,547,549,552,554],{"class":134,"line":536},14,[132,538,426],{"class":396},[132,540,541],{"class":145}," dateStr",[132,543,464],{"class":149},[132,545,546],{"class":403}," toLocalDateStr",[132,548,407],{"class":159},[132,550,551],{"class":145},"date",[132,553,414],{"class":159},[132,555,357],{"class":149},[132,557,559,561,564,566,568,570,572,574,576],{"class":134,"line":558},15,[132,560,426],{"class":396},[132,562,563],{"class":145}," start",[132,565,464],{"class":149},[132,567,467],{"class":149},[132,569,470],{"class":403},[132,571,407],{"class":159},[132,573,551],{"class":145},[132,575,414],{"class":159},[132,577,357],{"class":149},[132,579,581,584,586,589,591,594,596,599,601,603,605,607,609],{"class":134,"line":580},16,[132,582,583],{"class":145},"  start",[132,585,169],{"class":149},[132,587,588],{"class":403},"setHours",[132,590,407],{"class":159},[132,592,593],{"class":503},"0",[132,595,175],{"class":149},[132,597,598],{"class":503}," 0",[132,600,175],{"class":149},[132,602,598],{"class":503},[132,604,175],{"class":149},[132,606,598],{"class":503},[132,608,414],{"class":159},[132,610,357],{"class":149},[132,612,614,616,619,621,623,625,627,630,632],{"class":134,"line":613},17,[132,615,426],{"class":396},[132,617,618],{"class":145}," end",[132,620,464],{"class":149},[132,622,467],{"class":149},[132,624,470],{"class":403},[132,626,407],{"class":159},[132,628,629],{"class":145},"start",[132,631,414],{"class":159},[132,633,357],{"class":149},[132,635,637,640,642,644,646,649,651,653,655,658,660,662],{"class":134,"line":636},18,[132,638,639],{"class":145},"  end",[132,641,169],{"class":149},[132,643,484],{"class":403},[132,645,407],{"class":159},[132,647,648],{"class":145},"end",[132,650,169],{"class":149},[132,652,494],{"class":403},[132,654,497],{"class":159},[132,656,657],{"class":149},"+",[132,659,504],{"class":503},[132,661,414],{"class":159},[132,663,357],{"class":149},[132,665,667],{"class":134,"line":666},19,[132,668,388],{"emptyLinePlaceholder":387},[132,670,672],{"class":134,"line":671},20,[132,673,674],{"class":138},"  \u002F\u002F 四支查詢並行執行，減少等待時間\n",[132,676,678,680,683,686,689,692,695,697,700,702,705,707,710,713],{"class":134,"line":677},21,[132,679,426],{"class":396},[132,681,682],{"class":149}," [[",[132,684,685],{"class":145},"revenue",[132,687,688],{"class":149},"],",[132,690,691],{"class":149}," [",[132,693,694],{"class":145},"userRow",[132,696,688],{"class":149},[132,698,699],{"class":145}," topProducts",[132,701,175],{"class":149},[132,703,704],{"class":145}," topCategories",[132,706,175],{"class":149},[132,708,709],{"class":145}," paymentMethods",[132,711,712],{"class":149},"]",[132,714,432],{"class":149},[132,716,718,721,725,727,730],{"class":134,"line":717},22,[132,719,720],{"class":337},"    await",[132,722,724],{"class":723},"sBMFI"," Promise",[132,726,169],{"class":149},[132,728,729],{"class":403},"all",[132,731,732],{"class":159},"([\n",[132,734,736],{"class":134,"line":735},23,[132,737,738],{"class":138},"      \u002F\u002F 營業額、訂單數\n",[132,740,742,745,747,750],{"class":134,"line":741},24,[132,743,744],{"class":145},"      sequelize",[132,746,169],{"class":149},[132,748,749],{"class":403},"query",[132,751,752],{"class":159},"(\n",[132,754,756],{"class":134,"line":755},25,[132,757,758],{"class":149},"        `\n",[132,760,762],{"class":134,"line":761},26,[132,763,764],{"class":350},"        SELECT\n",[132,766,768],{"class":134,"line":767},27,[132,769,770],{"class":350},"          COALESCE(SUM(total_amount) FILTER (WHERE status IN ('paid','shipped','delivered')), 0) AS revenue,\n",[132,772,774],{"class":134,"line":773},28,[132,775,776],{"class":350},"          COUNT(*) FILTER (WHERE status IN ('paid','shipped','delivered')) AS paid_count,\n",[132,778,780],{"class":134,"line":779},29,[132,781,782],{"class":350},"          COUNT(*) AS order_count\n",[132,784,786],{"class":134,"line":785},30,[132,787,788],{"class":350},"        FROM orders WHERE created_at >= :start AND created_at \u003C :end\n",[132,790,792,795],{"class":134,"line":791},31,[132,793,794],{"class":149},"      `",[132,796,254],{"class":149},[132,798,800,803,806,808,810,812,814,816,819,822,824,827,829,832,834,837],{"class":134,"line":799},32,[132,801,802],{"class":149},"        {",[132,804,805],{"class":159}," replacements",[132,807,163],{"class":149},[132,809,364],{"class":149},[132,811,563],{"class":145},[132,813,175],{"class":149},[132,815,618],{"class":145},[132,817,818],{"class":149}," },",[132,820,821],{"class":159}," type",[132,823,163],{"class":149},[132,825,826],{"class":145}," sequelize",[132,828,169],{"class":149},[132,830,831],{"class":145},"QueryTypes",[132,833,169],{"class":149},[132,835,836],{"class":145},"SELECT",[132,838,839],{"class":149}," },\n",[132,841,843,846],{"class":134,"line":842},33,[132,844,845],{"class":159},"      )",[132,847,254],{"class":149},[132,849,851],{"class":134,"line":850},34,[132,852,388],{"emptyLinePlaceholder":387},[132,854,856],{"class":134,"line":855},35,[132,857,858],{"class":138},"      \u002F\u002F 新增用戶數\n",[132,860,862,864,866,868],{"class":134,"line":861},36,[132,863,744],{"class":145},[132,865,169],{"class":149},[132,867,749],{"class":403},[132,869,752],{"class":159},[132,871,873],{"class":134,"line":872},37,[132,874,758],{"class":149},[132,876,878],{"class":134,"line":877},38,[132,879,880],{"class":350},"        SELECT COUNT(*) AS count FROM users\n",[132,882,884],{"class":134,"line":883},39,[132,885,886],{"class":350},"        WHERE created_at >= :start AND created_at \u003C :end\n",[132,888,890,892],{"class":134,"line":889},40,[132,891,794],{"class":149},[132,893,254],{"class":149},[132,895,897,899,901,903,905,907,909,911,913,915,917,919,921,923,925,927],{"class":134,"line":896},41,[132,898,802],{"class":149},[132,900,805],{"class":159},[132,902,163],{"class":149},[132,904,364],{"class":149},[132,906,563],{"class":145},[132,908,175],{"class":149},[132,910,618],{"class":145},[132,912,818],{"class":149},[132,914,821],{"class":159},[132,916,163],{"class":149},[132,918,826],{"class":145},[132,920,169],{"class":149},[132,922,831],{"class":145},[132,924,169],{"class":149},[132,926,836],{"class":145},[132,928,839],{"class":149},[132,930,932,934],{"class":134,"line":931},42,[132,933,845],{"class":159},[132,935,254],{"class":149},[132,937,939],{"class":134,"line":938},43,[132,940,388],{"emptyLinePlaceholder":387},[132,942,944],{"class":134,"line":943},44,[132,945,946],{"class":138},"      \u002F\u002F 熱銷商品 Top 10\n",[132,948,950,952,954,956],{"class":134,"line":949},45,[132,951,744],{"class":145},[132,953,169],{"class":149},[132,955,749],{"class":403},[132,957,752],{"class":159},[132,959,961],{"class":134,"line":960},46,[132,962,758],{"class":149},[132,964,966],{"class":134,"line":965},47,[132,967,968],{"class":350},"        SELECT oi.product_id AS \"productId\", oi.product_name AS \"productName\",\n",[132,970,972],{"class":134,"line":971},48,[132,973,974],{"class":350},"               SUM(oi.subtotal) AS \"totalRevenue\", SUM(oi.quantity) AS \"totalQuantity\"\n",[132,976,978],{"class":134,"line":977},49,[132,979,980],{"class":350},"        FROM order_items oi JOIN orders o ON o.id = oi.order_id\n",[132,982,984],{"class":134,"line":983},50,[132,985,986],{"class":350},"        WHERE o.created_at >= :start AND o.created_at \u003C :end AND o.status != 'cancelled'\n",[132,988,990],{"class":134,"line":989},51,[132,991,992],{"class":350},"        GROUP BY oi.product_id, oi.product_name\n",[132,994,996],{"class":134,"line":995},52,[132,997,998],{"class":350},"        ORDER BY \"totalRevenue\" DESC LIMIT 10\n",[132,1000,1002,1004],{"class":134,"line":1001},53,[132,1003,794],{"class":149},[132,1005,254],{"class":149},[132,1007,1009,1011,1013,1015,1017,1019,1021,1023,1025,1027,1029,1031,1033,1035,1037,1039],{"class":134,"line":1008},54,[132,1010,802],{"class":149},[132,1012,805],{"class":159},[132,1014,163],{"class":149},[132,1016,364],{"class":149},[132,1018,563],{"class":145},[132,1020,175],{"class":149},[132,1022,618],{"class":145},[132,1024,818],{"class":149},[132,1026,821],{"class":159},[132,1028,163],{"class":149},[132,1030,826],{"class":145},[132,1032,169],{"class":149},[132,1034,831],{"class":145},[132,1036,169],{"class":149},[132,1038,836],{"class":145},[132,1040,839],{"class":149},[132,1042,1044,1046],{"class":134,"line":1043},55,[132,1045,845],{"class":159},[132,1047,254],{"class":149},[132,1049,1051],{"class":134,"line":1050},56,[132,1052,388],{"emptyLinePlaceholder":387},[132,1054,1056],{"class":134,"line":1055},57,[132,1057,1058],{"class":138},"      \u002F\u002F 各類別銷售 Top 10\n",[132,1060,1062,1064,1066,1068],{"class":134,"line":1061},58,[132,1063,744],{"class":145},[132,1065,169],{"class":149},[132,1067,749],{"class":403},[132,1069,752],{"class":159},[132,1071,1073],{"class":134,"line":1072},59,[132,1074,758],{"class":149},[132,1076,1078],{"class":134,"line":1077},60,[132,1079,1080],{"class":350},"        SELECT p.category_id AS \"categoryId\", c.name AS \"categoryName\",\n",[132,1082,1084],{"class":134,"line":1083},61,[132,1085,974],{"class":350},[132,1087,1089],{"class":134,"line":1088},62,[132,1090,1091],{"class":350},"        FROM order_items oi\n",[132,1093,1095],{"class":134,"line":1094},63,[132,1096,1097],{"class":350},"        JOIN orders o ON o.id = oi.order_id\n",[132,1099,1101],{"class":134,"line":1100},64,[132,1102,1103],{"class":350},"        JOIN products p ON p.id = oi.product_id\n",[132,1105,1107],{"class":134,"line":1106},65,[132,1108,1109],{"class":350},"        JOIN categories c ON c.id = p.category_id\n",[132,1111,1113],{"class":134,"line":1112},66,[132,1114,986],{"class":350},[132,1116,1118],{"class":134,"line":1117},67,[132,1119,1120],{"class":350},"        GROUP BY p.category_id, c.name\n",[132,1122,1124],{"class":134,"line":1123},68,[132,1125,998],{"class":350},[132,1127,1129,1131],{"class":134,"line":1128},69,[132,1130,794],{"class":149},[132,1132,254],{"class":149},[132,1134,1136,1138,1140,1142,1144,1146,1148,1150,1152,1154,1156,1158,1160,1162,1164,1166],{"class":134,"line":1135},70,[132,1137,802],{"class":149},[132,1139,805],{"class":159},[132,1141,163],{"class":149},[132,1143,364],{"class":149},[132,1145,563],{"class":145},[132,1147,175],{"class":149},[132,1149,618],{"class":145},[132,1151,818],{"class":149},[132,1153,821],{"class":159},[132,1155,163],{"class":149},[132,1157,826],{"class":145},[132,1159,169],{"class":149},[132,1161,831],{"class":145},[132,1163,169],{"class":149},[132,1165,836],{"class":145},[132,1167,839],{"class":149},[132,1169,1171,1173],{"class":134,"line":1170},71,[132,1172,845],{"class":159},[132,1174,254],{"class":149},[132,1176,1178],{"class":134,"line":1177},72,[132,1179,388],{"emptyLinePlaceholder":387},[132,1181,1183],{"class":134,"line":1182},73,[132,1184,1185],{"class":138},"      \u002F\u002F 付款方式分佈\n",[132,1187,1189,1191,1193,1195],{"class":134,"line":1188},74,[132,1190,744],{"class":145},[132,1192,169],{"class":149},[132,1194,749],{"class":403},[132,1196,752],{"class":159},[132,1198,1200],{"class":134,"line":1199},75,[132,1201,758],{"class":149},[132,1203,1205],{"class":134,"line":1204},76,[132,1206,1207],{"class":350},"        SELECT pay.method, COUNT(*) AS count, SUM(pay.amount) AS amount\n",[132,1209,1211],{"class":134,"line":1210},77,[132,1212,1213],{"class":350},"        FROM payments pay JOIN orders o ON o.id = pay.order_id\n",[132,1215,1217],{"class":134,"line":1216},78,[132,1218,1219],{"class":350},"        WHERE o.created_at >= :start AND o.created_at \u003C :end\n",[132,1221,1223],{"class":134,"line":1222},79,[132,1224,1225],{"class":350},"          AND o.status IN ('paid','shipped','delivered')\n",[132,1227,1229],{"class":134,"line":1228},80,[132,1230,1231],{"class":350},"        GROUP BY pay.method\n",[132,1233,1235,1237],{"class":134,"line":1234},81,[132,1236,794],{"class":149},[132,1238,254],{"class":149},[132,1240,1242,1244,1246,1248,1250,1252,1254,1256,1258,1260,1262,1264,1266,1268,1270,1272],{"class":134,"line":1241},82,[132,1243,802],{"class":149},[132,1245,805],{"class":159},[132,1247,163],{"class":149},[132,1249,364],{"class":149},[132,1251,563],{"class":145},[132,1253,175],{"class":149},[132,1255,618],{"class":145},[132,1257,818],{"class":149},[132,1259,821],{"class":159},[132,1261,163],{"class":149},[132,1263,826],{"class":145},[132,1265,169],{"class":149},[132,1267,831],{"class":145},[132,1269,169],{"class":149},[132,1271,836],{"class":145},[132,1273,839],{"class":149},[132,1275,1277,1279],{"class":134,"line":1276},83,[132,1278,845],{"class":159},[132,1280,254],{"class":149},[132,1282,1284,1287],{"class":134,"line":1283},84,[132,1285,1286],{"class":159},"    ])",[132,1288,357],{"class":149},[132,1290,1292],{"class":134,"line":1291},85,[132,1293,388],{"emptyLinePlaceholder":387},[132,1295,1297,1299,1302,1304,1307,1309,1311,1313,1315,1318,1321,1323],{"class":134,"line":1296},86,[132,1298,426],{"class":396},[132,1300,1301],{"class":145}," rev",[132,1303,464],{"class":149},[132,1305,1306],{"class":403}," parseFloat",[132,1308,407],{"class":159},[132,1310,685],{"class":145},[132,1312,169],{"class":149},[132,1314,685],{"class":145},[132,1316,1317],{"class":159},") ",[132,1319,1320],{"class":149},"||",[132,1322,598],{"class":503},[132,1324,357],{"class":149},[132,1326,1328,1330,1333,1335,1338,1340,1342,1344,1347,1349,1351,1353],{"class":134,"line":1327},87,[132,1329,426],{"class":396},[132,1331,1332],{"class":145}," paidCount",[132,1334,464],{"class":149},[132,1336,1337],{"class":403}," parseInt",[132,1339,407],{"class":159},[132,1341,685],{"class":145},[132,1343,169],{"class":149},[132,1345,1346],{"class":145},"paid_count",[132,1348,1317],{"class":159},[132,1350,1320],{"class":149},[132,1352,598],{"class":503},[132,1354,357],{"class":149},[132,1356,1358],{"class":134,"line":1357},88,[132,1359,388],{"emptyLinePlaceholder":387},[132,1361,1363],{"class":134,"line":1362},89,[132,1364,1365],{"class":138},"  \u002F\u002F upsert：重跑不會產生重複資料\n",[132,1367,1369,1372,1374,1376,1379,1381],{"class":134,"line":1368},90,[132,1370,1371],{"class":337},"  await",[132,1373,367],{"class":145},[132,1375,169],{"class":149},[132,1377,1378],{"class":403},"upsert",[132,1380,407],{"class":159},[132,1382,1383],{"class":149},"{\n",[132,1385,1387,1390,1392,1394],{"class":134,"line":1386},91,[132,1388,1389],{"class":159},"    date",[132,1391,163],{"class":149},[132,1393,541],{"class":145},[132,1395,254],{"class":149},[132,1397,1399,1402,1404,1406],{"class":134,"line":1398},92,[132,1400,1401],{"class":159},"    revenue",[132,1403,163],{"class":149},[132,1405,1301],{"class":145},[132,1407,254],{"class":149},[132,1409,1411,1414,1416,1418,1420,1422,1424,1427,1429,1431,1433],{"class":134,"line":1410},93,[132,1412,1413],{"class":159},"    orderCount",[132,1415,163],{"class":149},[132,1417,1337],{"class":403},[132,1419,407],{"class":159},[132,1421,685],{"class":145},[132,1423,169],{"class":149},[132,1425,1426],{"class":145},"order_count",[132,1428,1317],{"class":159},[132,1430,1320],{"class":149},[132,1432,598],{"class":503},[132,1434,254],{"class":149},[132,1436,1438,1441,1443,1445,1447,1449,1451,1454,1456,1458,1460],{"class":134,"line":1437},94,[132,1439,1440],{"class":159},"    newUserCount",[132,1442,163],{"class":149},[132,1444,1337],{"class":403},[132,1446,407],{"class":159},[132,1448,694],{"class":145},[132,1450,169],{"class":149},[132,1452,1453],{"class":145},"count",[132,1455,1317],{"class":159},[132,1457,1320],{"class":149},[132,1459,598],{"class":503},[132,1461,254],{"class":149},[132,1463,1465,1468,1470,1472,1475,1477,1480,1482,1485,1488,1491,1493,1495,1497,1500,1502,1505,1508,1510,1512],{"class":134,"line":1464},95,[132,1466,1467],{"class":159},"    avgOrderValue",[132,1469,163],{"class":149},[132,1471,1332],{"class":145},[132,1473,1474],{"class":149}," >",[132,1476,598],{"class":503},[132,1478,1479],{"class":149}," ?",[132,1481,1306],{"class":403},[132,1483,1484],{"class":159},"((",[132,1486,1487],{"class":145},"rev",[132,1489,1490],{"class":149}," \u002F",[132,1492,1332],{"class":145},[132,1494,414],{"class":159},[132,1496,169],{"class":149},[132,1498,1499],{"class":403},"toFixed",[132,1501,407],{"class":159},[132,1503,1504],{"class":503},"2",[132,1506,1507],{"class":159},")) ",[132,1509,163],{"class":149},[132,1511,598],{"class":503},[132,1513,254],{"class":149},[132,1515,1517,1520,1522,1524,1526,1529,1531,1533,1536,1538,1540,1543],{"class":134,"line":1516},96,[132,1518,1519],{"class":159},"    topProducts",[132,1521,163],{"class":149},[132,1523,699],{"class":145},[132,1525,169],{"class":149},[132,1527,1528],{"class":403},"map",[132,1530,407],{"class":159},[132,1532,407],{"class":149},[132,1534,1535],{"class":410},"r",[132,1537,414],{"class":149},[132,1539,451],{"class":396},[132,1541,1542],{"class":159}," (",[132,1544,1383],{"class":149},[132,1546,1548,1551,1553],{"class":134,"line":1547},97,[132,1549,1550],{"class":149},"      ...",[132,1552,1535],{"class":145},[132,1554,254],{"class":149},[132,1556,1558,1561,1563,1565,1567,1569,1571,1574,1576],{"class":134,"line":1557},98,[132,1559,1560],{"class":159},"      totalRevenue",[132,1562,163],{"class":149},[132,1564,1306],{"class":403},[132,1566,407],{"class":159},[132,1568,1535],{"class":145},[132,1570,169],{"class":149},[132,1572,1573],{"class":145},"totalRevenue",[132,1575,414],{"class":159},[132,1577,254],{"class":149},[132,1579,1581,1584,1586,1588,1590,1592,1594,1597,1599],{"class":134,"line":1580},99,[132,1582,1583],{"class":159},"      totalQuantity",[132,1585,163],{"class":149},[132,1587,1337],{"class":403},[132,1589,407],{"class":159},[132,1591,1535],{"class":145},[132,1593,169],{"class":149},[132,1595,1596],{"class":145},"totalQuantity",[132,1598,414],{"class":159},[132,1600,254],{"class":149},[132,1602,1604,1606,1609],{"class":134,"line":1603},100,[132,1605,523],{"class":149},[132,1607,1608],{"class":159},"))",[132,1610,254],{"class":149},[132,1612,1614,1617,1619,1621,1623,1625,1627,1629,1631,1633,1635,1637],{"class":134,"line":1613},101,[132,1615,1616],{"class":159},"    topCategories",[132,1618,163],{"class":149},[132,1620,704],{"class":145},[132,1622,169],{"class":149},[132,1624,1528],{"class":403},[132,1626,407],{"class":159},[132,1628,407],{"class":149},[132,1630,1535],{"class":410},[132,1632,414],{"class":149},[132,1634,451],{"class":396},[132,1636,1542],{"class":159},[132,1638,1383],{"class":149},[132,1640,1642,1644,1646],{"class":134,"line":1641},102,[132,1643,1550],{"class":149},[132,1645,1535],{"class":145},[132,1647,254],{"class":149},[132,1649,1651,1653,1655,1657,1659,1661,1663,1665,1667],{"class":134,"line":1650},103,[132,1652,1560],{"class":159},[132,1654,163],{"class":149},[132,1656,1306],{"class":403},[132,1658,407],{"class":159},[132,1660,1535],{"class":145},[132,1662,169],{"class":149},[132,1664,1573],{"class":145},[132,1666,414],{"class":159},[132,1668,254],{"class":149},[132,1670,1672,1674,1676,1678,1680,1682,1684,1686,1688],{"class":134,"line":1671},104,[132,1673,1583],{"class":159},[132,1675,163],{"class":149},[132,1677,1337],{"class":403},[132,1679,407],{"class":159},[132,1681,1535],{"class":145},[132,1683,169],{"class":149},[132,1685,1596],{"class":145},[132,1687,414],{"class":159},[132,1689,254],{"class":149},[132,1691,1693,1695,1697],{"class":134,"line":1692},105,[132,1694,523],{"class":149},[132,1696,1608],{"class":159},[132,1698,254],{"class":149},[132,1700,1702,1705,1707,1709,1711,1713,1715,1717,1719,1721,1723,1725],{"class":134,"line":1701},106,[132,1703,1704],{"class":159},"    paymentMethods",[132,1706,163],{"class":149},[132,1708,709],{"class":145},[132,1710,169],{"class":149},[132,1712,1528],{"class":403},[132,1714,407],{"class":159},[132,1716,407],{"class":149},[132,1718,1535],{"class":410},[132,1720,414],{"class":149},[132,1722,451],{"class":396},[132,1724,1542],{"class":159},[132,1726,1383],{"class":149},[132,1728,1730,1733,1735,1738,1740,1743],{"class":134,"line":1729},107,[132,1731,1732],{"class":159},"      method",[132,1734,163],{"class":149},[132,1736,1737],{"class":145}," r",[132,1739,169],{"class":149},[132,1741,1742],{"class":145},"method",[132,1744,254],{"class":149},[132,1746,1748,1751,1753,1755,1757,1759,1761,1763,1765],{"class":134,"line":1747},108,[132,1749,1750],{"class":159},"      count",[132,1752,163],{"class":149},[132,1754,1337],{"class":403},[132,1756,407],{"class":159},[132,1758,1535],{"class":145},[132,1760,169],{"class":149},[132,1762,1453],{"class":145},[132,1764,414],{"class":159},[132,1766,254],{"class":149},[132,1768,1770,1773,1775,1777,1779,1781,1783,1786,1788],{"class":134,"line":1769},109,[132,1771,1772],{"class":159},"      amount",[132,1774,163],{"class":149},[132,1776,1306],{"class":403},[132,1778,407],{"class":159},[132,1780,1535],{"class":145},[132,1782,169],{"class":149},[132,1784,1785],{"class":145},"amount",[132,1787,414],{"class":159},[132,1789,254],{"class":149},[132,1791,1793,1795,1797],{"class":134,"line":1792},110,[132,1794,523],{"class":149},[132,1796,1608],{"class":159},[132,1798,254],{"class":149},[132,1800,1802,1805,1807],{"class":134,"line":1801},111,[132,1803,1804],{"class":149},"  }",[132,1806,414],{"class":159},[132,1808,357],{"class":149},[132,1810,1812],{"class":134,"line":1811},112,[132,1813,1814],{"class":149},"}\n",[48,1816,1817],{},"幾個值得注意的設計細節：",[52,1819,1820,1828,1836,1843],{},[55,1821,1822,1827],{},[102,1823,1824],{},[77,1825,1826],{},"Promise.all","：四支查詢並行發出，不等前一支結束才跑下一支",[55,1829,1830,1835],{},[102,1831,1832],{},[77,1833,1834],{},"FILTER (WHERE status IN (...))","：只統計有效訂單的營業額，排除取消訂單",[55,1837,1838,1842],{},[102,1839,1840],{},[77,1841,1378],{},"：Cron Job 重跑（例如補跑失敗的日期）時不會產生重複記錄",[55,1844,1845,1850,1851,1854],{},[102,1846,1847],{},[77,1848,1849],{},"toLocalDateStr","：手動格式化本地日期，避免 ",[77,1852,1853],{},"toISOString()"," 因時區偏移導致日期錯誤",[92,1856],{},[44,1858,1860],{"id":1859},"cron-job-排程","Cron Job 排程",[106,1862,1864],{"className":126,"code":1863,"language":128,"meta":114,"style":114},"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",[77,1865,1866,1884,1905,1909,1914,1944,1954,1977],{"__ignoreMap":114},[132,1867,1868,1870,1873,1875,1877,1880,1882],{"class":134,"line":135},[132,1869,338],{"class":337},[132,1871,1872],{"class":145}," cron ",[132,1874,344],{"class":337},[132,1876,347],{"class":149},[132,1878,1879],{"class":350},"node-cron",[132,1881,354],{"class":149},[132,1883,357],{"class":149},[132,1885,1886,1888,1890,1892,1894,1896,1898,1901,1903],{"class":134,"line":142},[132,1887,338],{"class":337},[132,1889,364],{"class":149},[132,1891,404],{"class":145},[132,1893,370],{"class":149},[132,1895,373],{"class":337},[132,1897,347],{"class":149},[132,1899,1900],{"class":350},".\u002Fjobs\u002FdailySnapshot.js",[132,1902,354],{"class":149},[132,1904,357],{"class":149},[132,1906,1907],{"class":134,"line":156},[132,1908,388],{"emptyLinePlaceholder":387},[132,1910,1911],{"class":134,"line":181},[132,1912,1913],{"class":138},"\u002F\u002F 每天凌晨 00:05 執行（留 5 分鐘緩衝確保跨日資料落庫）\n",[132,1915,1916,1919,1921,1924,1926,1928,1931,1933,1935,1937,1940,1942],{"class":134,"line":201},[132,1917,1918],{"class":145},"cron",[132,1920,169],{"class":149},[132,1922,1923],{"class":403},"schedule",[132,1925,407],{"class":145},[132,1927,354],{"class":149},[132,1929,1930],{"class":350},"5 0 * * *",[132,1932,354],{"class":149},[132,1934,175],{"class":149},[132,1936,397],{"class":396},[132,1938,1939],{"class":149}," ()",[132,1941,451],{"class":396},[132,1943,153],{"class":149},[132,1945,1946,1948,1950,1952],{"class":134,"line":221},[132,1947,1371],{"class":337},[132,1949,404],{"class":403},[132,1951,448],{"class":159},[132,1953,357],{"class":149},[132,1955,1956,1959,1961,1964,1966,1968,1971,1973,1975],{"class":134,"line":240},[132,1957,1958],{"class":145},"  console",[132,1960,169],{"class":149},[132,1962,1963],{"class":403},"log",[132,1965,407],{"class":159},[132,1967,354],{"class":149},[132,1969,1970],{"class":350},"[Snapshot] 昨日 snapshot 建立完成",[132,1972,354],{"class":149},[132,1974,414],{"class":159},[132,1976,357],{"class":149},[132,1978,1979,1982,1984],{"class":134,"line":257},[132,1980,1981],{"class":149},"}",[132,1983,414],{"class":145},[132,1985,357],{"class":149},[92,1987],{},[44,1989,1990],{"id":1990},"查詢方式",[48,1992,1993],{},"總覽 API 改成直接讀 snapshot，不再碰原始資料表：",[106,1995,1997],{"className":126,"code":1996,"language":128,"meta":114,"style":114},"\u002F\u002F ✅ 讀最新一筆 snapshot，毫秒級回應\nconst snapshot = await DailySnapshot.findOne({\n  order: [[\"date\", \"DESC\"]],\n});\n",[77,1998,1999,2004,2028,2057],{"__ignoreMap":114},[132,2000,2001],{"class":134,"line":135},[132,2002,2003],{"class":138},"\u002F\u002F ✅ 讀最新一筆 snapshot，毫秒級回應\n",[132,2005,2006,2009,2012,2014,2017,2019,2021,2024,2026],{"class":134,"line":142},[132,2007,2008],{"class":396},"const",[132,2010,2011],{"class":145}," snapshot ",[132,2013,150],{"class":149},[132,2015,2016],{"class":337}," await",[132,2018,367],{"class":145},[132,2020,169],{"class":149},[132,2022,2023],{"class":403},"findOne",[132,2025,407],{"class":145},[132,2027,1383],{"class":149},[132,2029,2030,2033,2035,2037,2039,2041,2043,2045,2047,2050,2052,2055],{"class":134,"line":156},[132,2031,2032],{"class":159},"  order",[132,2034,163],{"class":149},[132,2036,682],{"class":145},[132,2038,354],{"class":149},[132,2040,551],{"class":350},[132,2042,354],{"class":149},[132,2044,175],{"class":149},[132,2046,347],{"class":149},[132,2048,2049],{"class":350},"DESC",[132,2051,354],{"class":149},[132,2053,2054],{"class":145},"]]",[132,2056,254],{"class":149},[132,2058,2059,2061,2063],{"class":134,"line":181},[132,2060,1981],{"class":149},[132,2062,414],{"class":145},[132,2064,357],{"class":149},[92,2066],{},[44,2068,2070],{"id":2069},"訂單狀態會變動怎麼辦","訂單狀態會變動怎麼辦？",[48,2072,2073],{},"Snapshot 是某個時間點的快照，但訂單狀態會在那之後繼續變動，這是使用這個模式必須正視的問題。",[48,2075,2076],{},[102,2077,2078],{},"舉個例子：",[106,2080,2083],{"className":2081,"code":2082,"language":111},[109],"23:50  訂單建立，狀態 pending\n00:05  Cron Job 跑完 snapshot，這筆訂單未被計入營業額\n09:00  用戶付款，狀態變 paid\n",[77,2084,2082],{"__ignoreMap":114},[48,2086,2087],{},"昨天的 snapshot 永遠不會包含這筆訂單，數字就是錯的。",[2089,2090,2092],"h3",{"id":2091},"解法一補跑近幾天的-snapshot","解法一：補跑近幾天的 Snapshot",[48,2094,2095],{},"每天除了算昨天，也重算過去 N 天，讓狀態更新能被追上：",[106,2097,2099],{"className":126,"code":2098,"language":128,"meta":114,"style":114},"\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",[77,2100,2101,2106,2132,2171,2188,2215,2229,2234],{"__ignoreMap":114},[132,2102,2103],{"class":134,"line":135},[132,2104,2105],{"class":138},"\u002F\u002F 每天重算最近 7 天\n",[132,2107,2108,2110,2112,2114,2116,2118,2120,2122,2124,2126,2128,2130],{"class":134,"line":142},[132,2109,1918],{"class":145},[132,2111,169],{"class":149},[132,2113,1923],{"class":403},[132,2115,407],{"class":145},[132,2117,354],{"class":149},[132,2119,1930],{"class":350},[132,2121,354],{"class":149},[132,2123,175],{"class":149},[132,2125,397],{"class":396},[132,2127,1939],{"class":149},[132,2129,451],{"class":396},[132,2131,153],{"class":149},[132,2133,2134,2137,2139,2142,2145,2147,2149,2152,2154,2157,2160,2162,2164,2167,2169],{"class":134,"line":156},[132,2135,2136],{"class":337},"  for",[132,2138,1542],{"class":159},[132,2140,2141],{"class":396},"let",[132,2143,2144],{"class":145}," i",[132,2146,464],{"class":149},[132,2148,504],{"class":503},[132,2150,2151],{"class":149},";",[132,2153,2144],{"class":145},[132,2155,2156],{"class":149}," \u003C=",[132,2158,2159],{"class":503}," 7",[132,2161,2151],{"class":149},[132,2163,2144],{"class":145},[132,2165,2166],{"class":149},"++",[132,2168,1317],{"class":159},[132,2170,1383],{"class":149},[132,2172,2173,2176,2178,2180,2182,2184,2186],{"class":134,"line":181},[132,2174,2175],{"class":396},"    const",[132,2177,461],{"class":145},[132,2179,464],{"class":149},[132,2181,467],{"class":149},[132,2183,470],{"class":403},[132,2185,448],{"class":159},[132,2187,357],{"class":149},[132,2189,2190,2193,2195,2197,2199,2201,2203,2205,2207,2209,2211,2213],{"class":134,"line":201},[132,2191,2192],{"class":145},"    d",[132,2194,169],{"class":149},[132,2196,484],{"class":403},[132,2198,407],{"class":159},[132,2200,489],{"class":145},[132,2202,169],{"class":149},[132,2204,494],{"class":403},[132,2206,497],{"class":159},[132,2208,500],{"class":149},[132,2210,2144],{"class":145},[132,2212,414],{"class":159},[132,2214,357],{"class":149},[132,2216,2217,2219,2221,2223,2225,2227],{"class":134,"line":221},[132,2218,720],{"class":337},[132,2220,404],{"class":403},[132,2222,407],{"class":159},[132,2224,489],{"class":145},[132,2226,414],{"class":159},[132,2228,357],{"class":149},[132,2230,2231],{"class":134,"line":240},[132,2232,2233],{"class":149},"  }\n",[132,2235,2236,2238,2240],{"class":134,"line":257},[132,2237,1981],{"class":149},[132,2239,414],{"class":145},[132,2241,357],{"class":149},[48,2243,2244,2245,2247],{},"適合狀態變動集中在近期（例如大多數訂單在 3 天內完成付款）的情境。",[77,2246,1378],{}," 的設計讓補跑變得安全，不會產生重複資料。",[2089,2249,2251],{"id":2250},"解法二只快照終態訂單","解法二：只快照終態訂單",[48,2253,2254,2255,80,2258,2261,2262,80,2265,2268],{},"只把 ",[77,2256,2257],{},"delivered",[77,2259,2260],{},"cancelled"," 這類不會再變動的訂單算進 snapshot，",[77,2263,2264],{},"paid",[77,2266,2267],{},"shipped"," 等還在流動中的訂單留給即時查詢。",[48,2270,2271],{},"代價是 snapshot 數字會比實際交易日期滯後幾天，但每一筆都是確定的終態數字。",[2089,2273,2275],{"id":2274},"解法三接受誤差","解法三：接受誤差",[48,2277,2278],{},"如果這個總覽是給內部看的管理報表，T+1 有些許誤差通常可以接受。業務決策不需要精確到每一筆，數字的趨勢比精確值更重要。",[92,2280],{},[48,2282,2283,2284,2287],{},"目前採用的是",[102,2285,2286],{},"解法一","，每天重算最近 7 天，在資料準確性與實作複雜度之間取得平衡。",[92,2289],{},[44,2291,2292],{"id":2292},"效果對比",[2294,2295,2296,2312],"table",{},[2297,2298,2299],"thead",{},[2300,2301,2302,2306,2309],"tr",{},[2303,2304,2305],"th",{},"指標",[2303,2307,2308],{},"改善前",[2303,2310,2311],{},"改善後",[2313,2314,2315,2327,2338],"tbody",{},[2300,2316,2317,2321,2324],{},[2318,2319,2320],"td",{},"查詢時間",[2318,2322,2323],{},"數秒",[2318,2325,2326],{},"\u003C 10ms",[2300,2328,2329,2332,2335],{},[2318,2330,2331],{},"資料庫負載",[2318,2333,2334],{},"每次請求跨表掃描",[2318,2336,2337],{},"每日一次聚合",[2300,2339,2340,2343,2346],{},[2318,2341,2342],{},"資料即時性",[2318,2344,2345],{},"即時",[2318,2347,2348],{},"前一天結算準確",[92,2350],{},[44,2352,2353],{"id":2353},"適用場景",[48,2355,2356,2357,2360],{},"這個模式適合",[102,2358,2359],{},"讀多寫少、資料量大、對即時性要求不高","的統計需求，例如：",[52,2362,2363,2366,2369],{},[55,2364,2365],{},"管理後台的訂單、用戶、收入總覽",[55,2367,2368],{},"報表系統的歷史趨勢圖",[55,2370,2371],{},"定期推播給管理員的每日摘要",[2373,2374,2375],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .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":114,"searchDepth":142,"depth":142,"links":2377},[2378,2379,2380,2381,2382,2383,2384,2389,2390],{"id":46,"depth":142,"text":46},{"id":96,"depth":142,"text":97},{"id":119,"depth":142,"text":120},{"id":323,"depth":142,"text":324},{"id":1859,"depth":142,"text":1860},{"id":1990,"depth":142,"text":1990},{"id":2069,"depth":142,"text":2070,"children":2385},[2386,2387,2388],{"id":2091,"depth":156,"text":2092},{"id":2250,"depth":156,"text":2251},{"id":2274,"depth":156,"text":2275},{"id":2292,"depth":142,"text":2292},{"id":2353,"depth":142,"text":2353},"2026-03-10","資料量龐大導致查詢變慢，透過 Cron Job 將每日統計好的資料寫入 Snapshot Table，查詢不再需要跨表掃描改為單表讀取。","md",null,"\u002Fimages\u002Fdaily-snapshot.jpg",{},{"title":10,"description":2392},"wWf7247tjW3NLW0rgckerIiTQ17DpIh6yqR3V0OJ3lE",[2394,2400],{"title":14,"path":15,"stem":16,"description":2401,"children":-1},"記錄將個人電商（GShop）部署到 GKE Autopilot 的完整流程。",1781661890756]