509 lines
13 KiB
Go
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
|
|
}
|