package api import ( "fmt" "net/http" "strconv" "strings" "time" "cockpit/internal/domain" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" ) func (h *Handler) OverviewKPIs(c *gin.Context) { year, err := parseYearOpt(c.Query("year")) if err != nil { c.JSON(http.StatusBadRequest, domain.Fail("year 参数错误")) return } type agg struct { SumA decimal.Decimal SumB decimal.Decimal Cnt int64 } var orders agg ordersSQL := ` SELECT COALESCE(SUM(amount_a),0) AS sum_a, COALESCE(SUM(amount_b),0) AS sum_b, COUNT(*) AS cnt FROM orders` var ordersArgs []any if year != nil { start := time.Date(*year, 1, 1, 0, 0, 0, 0, time.Local) end := time.Date(*year+1, 1, 1, 0, 0, 0, 0, time.Local) ordersSQL += ` WHERE order_date >= ? AND order_date < ?` ordersArgs = append(ordersArgs, start, end) } _ = h.db.Raw(` `+ordersSQL, ordersArgs...).Scan(&orders).Error var shipped agg shippedSQL := ` SELECT COALESCE(SUM(amount_a),0) AS sum_a, COALESCE(SUM(amount_b),0) AS sum_b, COUNT(*) AS cnt FROM orders WHERE ship_date IS NOT NULL` var shippedArgs []any if year != nil { start := time.Date(*year, 1, 1, 0, 0, 0, 0, time.Local) end := time.Date(*year+1, 1, 1, 0, 0, 0, 0, time.Local) shippedSQL += ` AND ship_date >= ? AND ship_date < ?` shippedArgs = append(shippedArgs, start, end) } _ = h.db.Raw(` `+shippedSQL, shippedArgs...).Scan(&shipped).Error var unshipped agg unshippedSQL := ` SELECT COALESCE(SUM(amount_a),0) AS sum_a, COALESCE(SUM(amount_b),0) AS sum_b, COUNT(*) AS cnt FROM orders WHERE ship_date IS NULL` var unshippedArgs []any if year != nil { start := time.Date(*year, 1, 1, 0, 0, 0, 0, time.Local) end := time.Date(*year+1, 1, 1, 0, 0, 0, 0, time.Local) unshippedSQL += ` AND order_date >= ? AND order_date < ?` unshippedArgs = append(unshippedArgs, start, end) } _ = h.db.Raw(` `+unshippedSQL, unshippedArgs...).Scan(&unshipped).Error avgA := decimal.Zero avgB := decimal.Zero if orders.Cnt > 0 { avgA = orders.SumA.Div(decimal.NewFromInt(orders.Cnt)).Round(2) avgB = orders.SumB.Div(decimal.NewFromInt(orders.Cnt)).Round(2) } c.JSON(http.StatusOK, domain.OK(gin.H{ "year": year, "kpis": gin.H{ "orderTotal": gin.H{"amountA": orders.SumA, "amountB": orders.SumB, "count": orders.Cnt}, "shipTotal": gin.H{"amountA": shipped.SumA, "amountB": shipped.SumB, "count": shipped.Cnt}, "unshipped": gin.H{"amountA": unshipped.SumA, "amountB": unshipped.SumB, "count": unshipped.Cnt}, "avgOrder": gin.H{"amountA": avgA, "amountB": avgB}, }, })) } func (h *Handler) OverviewMonthlyTrend(c *gin.Context) { year, err := parseYearOpt(c.Query("year")) if err != nil { c.JSON(http.StatusBadRequest, domain.Fail("year 参数错误")) return } type row struct { Year int Month int SumA decimal.Decimal SumB decimal.Decimal Cnt int64 } var out []gin.H if year != nil { start := time.Date(*year, 1, 1, 0, 0, 0, 0, time.Local) end := time.Date(*year+1, 1, 1, 0, 0, 0, 0, time.Local) var orderRows []row _ = h.db.Raw(` SELECT ? AS year, MONTH(order_date) AS month, COALESCE(SUM(amount_a),0) AS sum_a, COALESCE(SUM(amount_b),0) AS sum_b, COUNT(*) AS cnt FROM orders WHERE order_date >= ? AND order_date < ? GROUP BY MONTH(order_date) ORDER BY MONTH(order_date)`, *year, start, end).Scan(&orderRows).Error var shipRows []row _ = h.db.Raw(` SELECT ? AS year, MONTH(ship_date) AS month, COALESCE(SUM(amount_a),0) AS sum_a, COALESCE(SUM(amount_b),0) AS sum_b, COUNT(*) AS cnt FROM orders WHERE ship_date IS NOT NULL AND ship_date >= ? AND ship_date < ? GROUP BY MONTH(ship_date) ORDER BY MONTH(ship_date)`, *year, start, end).Scan(&shipRows).Error orderMap := map[int]row{} for _, r := range orderRows { orderMap[r.Month] = r } shipMap := map[int]row{} for _, r := range shipRows { shipMap[r.Month] = r } out = make([]gin.H, 0, 12) for m := 1; m <= 12; m++ { or := orderMap[m] sr := shipMap[m] out = append(out, gin.H{ "year": *year, "month": m, "order": gin.H{"amountA": or.SumA, "amountB": or.SumB, "count": or.Cnt}, "ship": gin.H{"amountA": sr.SumA, "amountB": sr.SumB, "count": sr.Cnt}, }) } } else { var orderRows []row _ = h.db.Raw(` SELECT YEAR(order_date) AS year, MONTH(order_date) AS month, COALESCE(SUM(amount_a),0) AS sum_a, COALESCE(SUM(amount_b),0) AS sum_b, COUNT(*) AS cnt FROM orders GROUP BY YEAR(order_date), MONTH(order_date) ORDER BY YEAR(order_date), MONTH(order_date)`).Scan(&orderRows).Error var shipRows []row _ = h.db.Raw(` SELECT YEAR(ship_date) AS year, MONTH(ship_date) AS month, COALESCE(SUM(amount_a),0) AS sum_a, COALESCE(SUM(amount_b),0) AS sum_b, COUNT(*) AS cnt FROM orders WHERE ship_date IS NOT NULL GROUP BY YEAR(ship_date), MONTH(ship_date) ORDER BY YEAR(ship_date), MONTH(ship_date)`).Scan(&shipRows).Error type ym struct{ y, m int } orderMap := map[ym]row{} shipMap := map[ym]row{} var minT *time.Time var maxT *time.Time observe := func(y, m int) { t := time.Date(y, time.Month(m), 1, 0, 0, 0, 0, time.Local) if minT == nil || t.Before(*minT) { minT = &t } if maxT == nil || t.After(*maxT) { maxT = &t } } for _, r := range orderRows { k := ym{y: r.Year, m: r.Month} orderMap[k] = r observe(r.Year, r.Month) } for _, r := range shipRows { k := ym{y: r.Year, m: r.Month} shipMap[k] = r observe(r.Year, r.Month) } out = make([]gin.H, 0) if minT != nil && maxT != nil { for t := *minT; !t.After(*maxT); t = t.AddDate(0, 1, 0) { y := t.Year() m := int(t.Month()) k := ym{y: y, m: m} or := orderMap[k] sr := shipMap[k] out = append(out, gin.H{ "year": y, "month": m, "order": gin.H{"amountA": or.SumA, "amountB": or.SumB, "count": or.Cnt}, "ship": gin.H{"amountA": sr.SumA, "amountB": sr.SumB, "count": sr.Cnt}, }) } } } c.JSON(http.StatusOK, domain.OK(gin.H{ "year": year, "months": out, })) } func (h *Handler) OverviewByCustomer(c *gin.Context) { year, err := parseYearOpt(c.Query("year")) if err != nil { c.JSON(http.StatusBadRequest, domain.Fail("year 参数错误")) return } month, err := parseMonthOpt(c.Query("month")) if err != nil { c.JSON(http.StatusBadRequest, domain.Fail("month 参数错误")) return } if month != nil && year == nil { c.JSON(http.StatusBadRequest, domain.Fail("month 需要配合 year")) return } type row struct { CustomerID uint64 `json:"customerId"` Name string `json:"customerName"` Cnt int64 `json:"count"` SumA decimal.Decimal `json:"amountA"` SumB decimal.Decimal `json:"amountB"` } var out []gin.H if month != nil { y := *year m := *month curStart := time.Date(y, time.Month(m), 1, 0, 0, 0, 0, time.Local) curEnd := curStart.AddDate(0, 1, 0) prevStart := curStart.AddDate(0, -1, 0) prevEnd := curStart var cur []row _ = h.db.Raw(` SELECT c.id AS customer_id, c.name AS name, COUNT(o.id) AS cnt, COALESCE(SUM(o.amount_a),0) AS sum_a, COALESCE(SUM(o.amount_b),0) AS sum_b FROM customers c LEFT JOIN orders o ON o.customer_id = c.id AND o.order_date >= ? AND o.order_date < ? GROUP BY c.id, c.name ORDER BY cnt DESC`, curStart, curEnd).Scan(&cur).Error var prev []row _ = h.db.Raw(` SELECT c.id AS customer_id, c.name AS name, COUNT(o.id) AS cnt, COALESCE(SUM(o.amount_a),0) AS sum_a, COALESCE(SUM(o.amount_b),0) AS sum_b FROM customers c LEFT JOIN orders o ON o.customer_id = c.id AND o.order_date >= ? AND o.order_date < ? GROUP BY c.id, c.name`, prevStart, prevEnd).Scan(&prev).Error prevMap := map[uint64]row{} for _, r := range prev { prevMap[r.CustomerID] = r } out = make([]gin.H, 0, len(cur)) for _, r := range cur { p := prevMap[r.CustomerID] diff := r.Cnt - p.Cnt var rate *decimal.Decimal if p.Cnt > 0 { v := decimal.NewFromInt(diff).Div(decimal.NewFromInt(p.Cnt)).Mul(decimal.NewFromInt(100)).Round(2) rate = &v } out = append(out, gin.H{ "customerId": r.CustomerID, "customerName": r.Name, "count": r.Cnt, "momDiff": diff, "momRatePct": rate, "amountA": r.SumA, "amountB": r.SumB, "prevCount": p.Cnt, }) } } else { baseSQL := ` SELECT c.id AS customer_id, c.name AS name, COUNT(o.id) AS cnt, COALESCE(SUM(o.amount_a),0) AS sum_a, COALESCE(SUM(o.amount_b),0) AS sum_b FROM customers c LEFT JOIN orders o ON o.customer_id = c.id` var args []any if year != nil { start := time.Date(*year, 1, 1, 0, 0, 0, 0, time.Local) end := time.Date(*year+1, 1, 1, 0, 0, 0, 0, time.Local) baseSQL += ` AND o.order_date >= ? AND o.order_date < ?` args = append(args, start, end) } baseSQL += ` GROUP BY c.id, c.name ORDER BY cnt DESC` var cur []row _ = h.db.Raw(baseSQL, args...).Scan(&cur).Error out = make([]gin.H, 0, len(cur)) for _, r := range cur { out = append(out, gin.H{ "customerId": r.CustomerID, "customerName": r.Name, "count": r.Cnt, "momDiff": nil, "momRatePct": nil, "amountA": r.SumA, "amountB": r.SumB, "prevCount": nil, }) } } c.JSON(http.StatusOK, domain.OK(gin.H{ "year": year, "month": month, "list": out, })) } func (h *Handler) OverviewTopN(c *gin.Context) { dimension := strings.TrimSpace(c.DefaultQuery("dimension", "customer")) // customer/status by := strings.TrimSpace(c.DefaultQuery("by", "amount_a")) // amount_a/amount_b/count n, _ := strconv.Atoi(c.DefaultQuery("n", "10")) if n <= 0 || n > 100 { n = 10 } year, err := parseYearOpt(c.Query("year")) if err != nil { c.JSON(http.StatusBadRequest, domain.Fail("year 参数错误")) return } var metricExpr string switch by { case "amount_b", "amountB": metricExpr = "COALESCE(SUM(o.amount_b),0)" case "count": metricExpr = "COUNT(o.id)" default: metricExpr = "COALESCE(SUM(o.amount_a),0)" } type row struct { ID uint64 `json:"id"` Name string `json:"name"` Value decimal.Decimal `json:"value"` Cnt int64 `json:"count"` } var rows []row switch dimension { case "status": sql := ` SELECT s.id AS id, s.name AS name, `+metricExpr+` AS value, COUNT(o.id) AS cnt FROM statuses s LEFT JOIN orders o ON o.status_id = s.id` var args []any if year != nil { start := time.Date(*year, 1, 1, 0, 0, 0, 0, time.Local) end := time.Date(*year+1, 1, 1, 0, 0, 0, 0, time.Local) sql += ` AND o.order_date >= ? AND o.order_date < ?` args = append(args, start, end) } sql += ` GROUP BY s.id, s.name ORDER BY value DESC LIMIT ?` args = append(args, n) _ = h.db.Raw(sql, args...).Scan(&rows).Error default: sql := ` SELECT c.id AS id, c.name AS name, `+metricExpr+` AS value, COUNT(o.id) AS cnt FROM customers c LEFT JOIN orders o ON o.customer_id = c.id` var args []any if year != nil { start := time.Date(*year, 1, 1, 0, 0, 0, 0, time.Local) end := time.Date(*year+1, 1, 1, 0, 0, 0, 0, time.Local) sql += ` AND o.order_date >= ? AND o.order_date < ?` args = append(args, start, end) } sql += ` GROUP BY c.id, c.name ORDER BY value DESC LIMIT ?` args = append(args, n) _ = h.db.Raw(sql, args...).Scan(&rows).Error } c.JSON(http.StatusOK, domain.OK(gin.H{ "year": year, "dimension": dimension, "by": by, "n": n, "list": rows, })) } func (h *Handler) OverviewStatusDistribution(c *gin.Context) { year, err := parseYearOpt(c.Query("year")) if err != nil { c.JSON(http.StatusBadRequest, domain.Fail("year 参数错误")) return } type row struct { StatusID uint64 `json:"statusId"` Name string `json:"name"` Cnt int64 `json:"count"` SumA decimal.Decimal `json:"amountA"` SumB decimal.Decimal `json:"amountB"` } var rows []row sql := ` SELECT s.id AS status_id, s.name AS name, COUNT(o.id) AS cnt, COALESCE(SUM(o.amount_a),0) AS sum_a, COALESCE(SUM(o.amount_b),0) AS sum_b FROM statuses s LEFT JOIN orders o ON o.status_id = s.id` var args []any if year != nil { start := time.Date(*year, 1, 1, 0, 0, 0, 0, time.Local) end := time.Date(*year+1, 1, 1, 0, 0, 0, 0, time.Local) sql += ` AND o.order_date >= ? AND o.order_date < ?` args = append(args, start, end) } sql += ` GROUP BY s.id, s.name ORDER BY cnt DESC` _ = h.db.Raw(sql, args...).Scan(&rows).Error c.JSON(http.StatusOK, domain.OK(gin.H{ "year": year, "list": rows, })) } func parseYearOpt(v string) (*int, error) { v = strings.TrimSpace(v) if v == "" { return nil, nil } y, err := strconv.Atoi(v) if err != nil || y < 2000 || y > 2100 { return nil, fmt.Errorf("invalid year") } return &y, nil } func parseMonthOpt(v string) (*int, error) { v = strings.TrimSpace(v) if v == "" { return nil, nil } m, err := strconv.Atoi(v) if err != nil || m < 1 || m > 12 { return nil, fmt.Errorf("invalid month") } return &m, nil }