cockpit-source/backend/internal/api/overview.go
2026-04-02 14:12:43 +08:00

509 lines
13 KiB
Go

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
}