首页接口实现
This commit is contained in:
522
docs/api.yaml
522
docs/api.yaml
File diff suppressed because it is too large
Load Diff
56
web/handlers/home.go
Normal file
56
web/handlers/home.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"platform/web/auth"
|
||||||
|
g "platform/web/globals"
|
||||||
|
s "platform/web/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HomeOverview 后台首页概览统计
|
||||||
|
func HomeOverview(c *fiber.Ctx) error {
|
||||||
|
if _, err := auth.GetAuthCtx(c).PermitAdmin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.Home.Overview()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HomeTrend 后台首页每日趋势统计
|
||||||
|
func HomeTrend(c *fiber.Ctx) error {
|
||||||
|
if _, err := auth.GetAuthCtx(c).PermitAdmin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var req s.HomeTrendReq
|
||||||
|
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.Home.Trend(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HomeExtractable 后台首页全局可提取统计
|
||||||
|
func HomeExtractable(c *fiber.Ctx) error {
|
||||||
|
if _, err := auth.GetAuthCtx(c).PermitAdmin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.Home.Extractable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(resp)
|
||||||
|
}
|
||||||
@@ -170,6 +170,12 @@ func userRouter(api fiber.Router) {
|
|||||||
func adminRouter(api fiber.Router) {
|
func adminRouter(api fiber.Router) {
|
||||||
api = api.Group("/admin")
|
api = api.Group("/admin")
|
||||||
|
|
||||||
|
// home 后台首页
|
||||||
|
var home = api.Group("/home")
|
||||||
|
home.Post("/overview", handlers.HomeOverview)
|
||||||
|
home.Post("/trend", handlers.HomeTrend)
|
||||||
|
home.Post("/extractable", handlers.HomeExtractable)
|
||||||
|
|
||||||
// admin 管理员
|
// admin 管理员
|
||||||
var admin = api.Group("/admin")
|
var admin = api.Group("/admin")
|
||||||
admin.Post("/all", handlers.AllAdminByAdmin)
|
admin.Post("/all", handlers.AllAdminByAdmin)
|
||||||
|
|||||||
424
web/services/home.go
Normal file
424
web/services/home.go
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"platform/web/core"
|
||||||
|
g "platform/web/globals"
|
||||||
|
m "platform/web/models"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
const homeMaxTrendDays = 366
|
||||||
|
|
||||||
|
var Home = &homeService{}
|
||||||
|
|
||||||
|
type homeService struct{}
|
||||||
|
|
||||||
|
type HomeOverviewResp struct {
|
||||||
|
PendingCustomerCount int64 `json:"pending_customer_count"`
|
||||||
|
ActiveChannelCount int64 `json:"active_channel_count"`
|
||||||
|
TodayTradeAmount string `json:"today_trade_amount"`
|
||||||
|
TodayExtractCount int64 `json:"today_extract_count"`
|
||||||
|
TodayNewCustomerCount int64 `json:"today_new_customer_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HomeTrendReq struct {
|
||||||
|
DateStart string `json:"date_start" validate:"required"`
|
||||||
|
DateEnd string `json:"date_end" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HomeTrendResp []HomeTrendItem
|
||||||
|
|
||||||
|
type HomeTrendItem struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
ActiveChannelCount int64 `json:"active_channel_count"`
|
||||||
|
TradeAmount string `json:"trade_amount"`
|
||||||
|
NewCustomerCount int64 `json:"new_customer_count"`
|
||||||
|
ExtractCount int64 `json:"extract_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HomeExtractableResp struct {
|
||||||
|
Short int64 `json:"short"`
|
||||||
|
Long int64 `json:"long"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type homeRange struct {
|
||||||
|
StartLocal time.Time
|
||||||
|
EndLocal time.Time
|
||||||
|
StartUTC time.Time
|
||||||
|
EndUTC time.Time
|
||||||
|
Dates []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type homeCountRow struct {
|
||||||
|
Date string
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type homeAmountRow struct {
|
||||||
|
Date string
|
||||||
|
Amount string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) Overview() (*HomeOverviewResp, error) {
|
||||||
|
loc := homeLocation()
|
||||||
|
startLocal := startOfLocalDay(time.Now().In(loc), loc)
|
||||||
|
rng := homeRange{
|
||||||
|
StartLocal: startLocal,
|
||||||
|
EndLocal: startLocal,
|
||||||
|
StartUTC: startLocal.UTC(),
|
||||||
|
EndUTC: startLocal.AddDate(0, 0, 1).UTC(),
|
||||||
|
Dates: []string{homeDateKey(startLocal, loc)},
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingCustomerCount, err := s.pendingCustomerCount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
activeChannelCount, err := s.activeChannelCount(time.Now().UTC())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeAmount, err := s.tradeAmount(rng.StartUTC, rng.EndUTC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
extractCount, err := s.extractCount(rng.StartUTC, rng.EndUTC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newCustomerCount, err := s.newCustomerCount(rng.StartUTC, rng.EndUTC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HomeOverviewResp{
|
||||||
|
PendingCustomerCount: pendingCustomerCount,
|
||||||
|
ActiveChannelCount: activeChannelCount,
|
||||||
|
TodayTradeAmount: tradeAmount,
|
||||||
|
TodayExtractCount: extractCount,
|
||||||
|
TodayNewCustomerCount: newCustomerCount,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) Trend(req HomeTrendReq) (HomeTrendResp, error) {
|
||||||
|
rng, err := newHomeRange(req.DateStart, req.DateEnd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make(HomeTrendResp, 0, len(rng.Dates))
|
||||||
|
byDate := make(map[string]int, len(rng.Dates))
|
||||||
|
for _, date := range rng.Dates {
|
||||||
|
byDate[date] = len(items)
|
||||||
|
items = append(items, HomeTrendItem{
|
||||||
|
Date: date,
|
||||||
|
TradeAmount: "0.00",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
activeChannels, err := s.dailyActiveChannelCounts(rng)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range activeChannels {
|
||||||
|
if index, ok := byDate[row.Date]; ok {
|
||||||
|
items[index].ActiveChannelCount = row.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trades, err := s.dailyTradeAmounts(rng)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range trades {
|
||||||
|
if index, ok := byDate[row.Date]; ok {
|
||||||
|
items[index].TradeAmount = normalizeDecimalString(row.Amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newCustomers, err := s.dailyNewCustomerCounts(rng)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range newCustomers {
|
||||||
|
if index, ok := byDate[row.Date]; ok {
|
||||||
|
items[index].NewCustomerCount = row.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extracts, err := s.dailyExtractCounts(rng)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range extracts {
|
||||||
|
if index, ok := byDate[row.Date]; ok {
|
||||||
|
items[index].ExtractCount = row.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) Extractable() (*HomeExtractableResp, error) {
|
||||||
|
loc := homeLocation()
|
||||||
|
now := time.Now().In(loc)
|
||||||
|
|
||||||
|
var resources []*m.Resource
|
||||||
|
err := g.DB.
|
||||||
|
Preload("Short").
|
||||||
|
Preload("Long").
|
||||||
|
Where("active = ?", true).
|
||||||
|
Find(&resources).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &HomeExtractableResp{}
|
||||||
|
for _, resource := range resources {
|
||||||
|
switch resource.Type {
|
||||||
|
case m.ResourceTypeShort:
|
||||||
|
resp.Short += calcShortExtractable(resource.Short, now, loc)
|
||||||
|
case m.ResourceTypeLong:
|
||||||
|
resp.Long += calcLongExtractable(resource.Long, now, loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) pendingCustomerCount() (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := g.DB.Model(&m.User{}).Where("admin_id IS NULL").Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) activeChannelCount(now time.Time) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := g.DB.Model(&m.Channel{}).Where("expired_at > ?", now.UTC()).Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) tradeAmount(startUTC, endUTC time.Time) (string, error) {
|
||||||
|
var amount string
|
||||||
|
err := g.DB.Raw(`
|
||||||
|
SELECT COALESCE(SUM(payment), 0)::text
|
||||||
|
FROM "trade"
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND status = ?
|
||||||
|
AND completed_at >= ?
|
||||||
|
AND completed_at < ?
|
||||||
|
`, int(m.TradeStatusSuccess), startUTC, endUTC).Scan(&amount).Error
|
||||||
|
return normalizeDecimalString(amount), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) extractCount(startUTC, endUTC time.Time) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := g.DB.Raw(`
|
||||||
|
SELECT COALESCE(SUM(count), 0)
|
||||||
|
FROM logs_user_usage
|
||||||
|
WHERE time >= ?
|
||||||
|
AND time < ?
|
||||||
|
`, startUTC, endUTC).Scan(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) newCustomerCount(startUTC, endUTC time.Time) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := g.DB.Model(&m.User{}).
|
||||||
|
Where("created_at >= ? AND created_at < ?", startUTC, endUTC).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) dailyActiveChannelCounts(rng homeRange) ([]homeCountRow, error) {
|
||||||
|
var rows []homeCountRow
|
||||||
|
err := g.DB.Raw(`
|
||||||
|
WITH days AS (
|
||||||
|
SELECT generate_series(?::timestamptz, ?::timestamptz - interval '1 day', interval '1 day') AS day_start
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
to_char(days.day_start AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date,
|
||||||
|
COUNT(channel.id) AS count
|
||||||
|
FROM days
|
||||||
|
LEFT JOIN channel
|
||||||
|
ON channel.deleted_at IS NULL
|
||||||
|
AND channel.created_at < days.day_start + interval '1 day'
|
||||||
|
AND channel.expired_at >= days.day_start
|
||||||
|
GROUP BY days.day_start
|
||||||
|
ORDER BY days.day_start
|
||||||
|
`, rng.StartUTC, rng.EndUTC).Scan(&rows).Error
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) dailyTradeAmounts(rng homeRange) ([]homeAmountRow, error) {
|
||||||
|
var rows []homeAmountRow
|
||||||
|
err := g.DB.Raw(`
|
||||||
|
SELECT
|
||||||
|
to_char(completed_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date,
|
||||||
|
COALESCE(SUM(payment), 0)::text AS amount
|
||||||
|
FROM "trade"
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND status = ?
|
||||||
|
AND completed_at >= ?
|
||||||
|
AND completed_at < ?
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date
|
||||||
|
`, int(m.TradeStatusSuccess), rng.StartUTC, rng.EndUTC).Scan(&rows).Error
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) dailyNewCustomerCounts(rng homeRange) ([]homeCountRow, error) {
|
||||||
|
var rows []homeCountRow
|
||||||
|
err := g.DB.Raw(`
|
||||||
|
SELECT
|
||||||
|
to_char(created_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date,
|
||||||
|
COUNT(*) AS count
|
||||||
|
FROM "user"
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND created_at >= ?
|
||||||
|
AND created_at < ?
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date
|
||||||
|
`, rng.StartUTC, rng.EndUTC).Scan(&rows).Error
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *homeService) dailyExtractCounts(rng homeRange) ([]homeCountRow, error) {
|
||||||
|
var rows []homeCountRow
|
||||||
|
err := g.DB.Raw(`
|
||||||
|
SELECT
|
||||||
|
to_char(time AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date,
|
||||||
|
COALESCE(SUM(count), 0) AS count
|
||||||
|
FROM logs_user_usage
|
||||||
|
WHERE time >= ?
|
||||||
|
AND time < ?
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date
|
||||||
|
`, rng.StartUTC, rng.EndUTC).Scan(&rows).Error
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHomeRange(dateStart, dateEnd string) (homeRange, error) {
|
||||||
|
loc := homeLocation()
|
||||||
|
|
||||||
|
startLocal, err := time.ParseInLocation(time.DateOnly, dateStart, loc)
|
||||||
|
if err != nil {
|
||||||
|
return homeRange{}, core.NewBizErr("date_start 必须是 YYYY-MM-DD 格式", err)
|
||||||
|
}
|
||||||
|
endLocal, err := time.ParseInLocation(time.DateOnly, dateEnd, loc)
|
||||||
|
if err != nil {
|
||||||
|
return homeRange{}, core.NewBizErr("date_end 必须是 YYYY-MM-DD 格式", err)
|
||||||
|
}
|
||||||
|
if endLocal.Before(startLocal) {
|
||||||
|
return homeRange{}, core.NewBizErr("date_end 不能早于 date_start")
|
||||||
|
}
|
||||||
|
|
||||||
|
days := int(endLocal.Sub(startLocal).Hours()/24) + 1
|
||||||
|
if days > homeMaxTrendDays {
|
||||||
|
return homeRange{}, core.NewBizErr(fmt.Sprintf("日期范围不能超过 %d 天", homeMaxTrendDays))
|
||||||
|
}
|
||||||
|
|
||||||
|
dates := make([]string, 0, days)
|
||||||
|
for day := startLocal; !day.After(endLocal); day = day.AddDate(0, 0, 1) {
|
||||||
|
dates = append(dates, homeDateKey(day, loc))
|
||||||
|
}
|
||||||
|
|
||||||
|
return homeRange{
|
||||||
|
StartLocal: startLocal,
|
||||||
|
EndLocal: endLocal,
|
||||||
|
StartUTC: startLocal.UTC(),
|
||||||
|
EndUTC: endLocal.AddDate(0, 0, 1).UTC(),
|
||||||
|
Dates: dates,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcShortExtractable(resource *m.ResourceShort, now time.Time, loc *time.Location) int64 {
|
||||||
|
if resource == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return calcExtractable(resource.Type, resource.Quota, resource.Used, resource.Daily, resource.LastAt, resource.ExpireAt, now, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcLongExtractable(resource *m.ResourceLong, now time.Time, loc *time.Location) int64 {
|
||||||
|
if resource == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return calcExtractable(resource.Type, resource.Quota, resource.Used, resource.Daily, resource.LastAt, resource.ExpireAt, now, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcExtractable(mode m.ResourceMode, quota, used, daily int32, lastAt, expireAt *time.Time, now time.Time, loc *time.Location) int64 {
|
||||||
|
switch mode {
|
||||||
|
case m.ResourceModeQuota:
|
||||||
|
return maxInt64(int64(quota)-int64(used), 0)
|
||||||
|
|
||||||
|
case m.ResourceModeTime:
|
||||||
|
if expireAt == nil || !expireAt.After(now) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
todayRemaining := int64(quota)
|
||||||
|
if lastAt != nil && sameLocalDate(*lastAt, now, loc) {
|
||||||
|
todayRemaining = maxInt64(int64(quota)-int64(daily), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
todayStart := startOfLocalDay(now, loc)
|
||||||
|
expireStart := startOfLocalDay(expireAt.In(loc), loc)
|
||||||
|
futureDays := int64(expireStart.Sub(todayStart).Hours() / 24)
|
||||||
|
if futureDays < 0 {
|
||||||
|
futureDays = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return todayRemaining + futureDays*int64(quota)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func homeLocation() *time.Location {
|
||||||
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if err != nil {
|
||||||
|
return time.FixedZone("Asia/Shanghai", 8*60*60)
|
||||||
|
}
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
|
||||||
|
func startOfLocalDay(t time.Time, loc *time.Location) time.Time {
|
||||||
|
y, m, d := t.In(loc).Date()
|
||||||
|
return time.Date(y, m, d, 0, 0, 0, 0, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameLocalDate(a, b time.Time, loc *time.Location) bool {
|
||||||
|
ay, am, ad := a.In(loc).Date()
|
||||||
|
by, bm, bd := b.In(loc).Date()
|
||||||
|
return ay == by && am == bm && ad == bd
|
||||||
|
}
|
||||||
|
|
||||||
|
func homeDateKey(t time.Time, loc *time.Location) string {
|
||||||
|
return t.In(loc).Format(time.DateOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDecimalString(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return "0.00"
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := decimal.NewFromString(s)
|
||||||
|
if err != nil {
|
||||||
|
return "0.00"
|
||||||
|
}
|
||||||
|
return d.StringFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxInt64(a, b int64) int64 {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
87
web/services/home_test.go
Normal file
87
web/services/home_test.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
m "platform/web/models"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewHomeRange(t *testing.T) {
|
||||||
|
rng, err := newHomeRange("2026-07-20", "2026-07-21")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("newHomeRange returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := len(rng.Dates), 2; got != want {
|
||||||
|
t.Fatalf("len(Dates) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := rng.Dates[0], "2026-07-20"; got != want {
|
||||||
|
t.Fatalf("Dates[0] = %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
if got, want := rng.Dates[1], "2026-07-21"; got != want {
|
||||||
|
t.Fatalf("Dates[1] = %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
if got, want := rng.StartUTC.Format(time.RFC3339), "2026-07-19T16:00:00Z"; got != want {
|
||||||
|
t.Fatalf("StartUTC = %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
if got, want := rng.EndUTC.Format(time.RFC3339), "2026-07-21T16:00:00Z"; got != want {
|
||||||
|
t.Fatalf("EndUTC = %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHomeRangeRejectsInvalidRange(t *testing.T) {
|
||||||
|
_, err := newHomeRange("2026-07-21", "2026-07-20")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("newHomeRange expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcExtractableQuota(t *testing.T) {
|
||||||
|
loc := homeLocation()
|
||||||
|
now := time.Date(2026, 7, 21, 10, 0, 0, 0, loc)
|
||||||
|
|
||||||
|
got := calcExtractable(m.ResourceModeQuota, 100, 30, 0, nil, nil, now, loc)
|
||||||
|
if want := int64(70); got != want {
|
||||||
|
t.Fatalf("calcExtractable quota = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = calcExtractable(m.ResourceModeQuota, 100, 130, 0, nil, nil, now, loc)
|
||||||
|
if want := int64(0); got != want {
|
||||||
|
t.Fatalf("calcExtractable exhausted quota = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcExtractableTimeCountsTodayAndExpiryDate(t *testing.T) {
|
||||||
|
loc := homeLocation()
|
||||||
|
now := time.Date(2026, 7, 21, 10, 0, 0, 0, loc)
|
||||||
|
lastAt := time.Date(2026, 7, 21, 9, 0, 0, 0, loc)
|
||||||
|
expireAt := time.Date(2026, 7, 23, 12, 0, 0, 0, loc)
|
||||||
|
|
||||||
|
got := calcExtractable(m.ResourceModeTime, 100, 0, 30, &lastAt, &expireAt, now, loc)
|
||||||
|
if want := int64(270); got != want {
|
||||||
|
t.Fatalf("calcExtractable time = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcExtractableTimeResetsDailyWhenLastAtIsNotToday(t *testing.T) {
|
||||||
|
loc := homeLocation()
|
||||||
|
now := time.Date(2026, 7, 21, 10, 0, 0, 0, loc)
|
||||||
|
lastAt := time.Date(2026, 7, 20, 23, 59, 59, 0, loc)
|
||||||
|
expireAt := time.Date(2026, 7, 21, 23, 0, 0, 0, loc)
|
||||||
|
|
||||||
|
got := calcExtractable(m.ResourceModeTime, 100, 0, 80, &lastAt, &expireAt, now, loc)
|
||||||
|
if want := int64(100); got != want {
|
||||||
|
t.Fatalf("calcExtractable time reset = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcExtractableTimeExpired(t *testing.T) {
|
||||||
|
loc := homeLocation()
|
||||||
|
now := time.Date(2026, 7, 21, 10, 0, 0, 0, loc)
|
||||||
|
expireAt := time.Date(2026, 7, 21, 9, 0, 0, 0, loc)
|
||||||
|
|
||||||
|
got := calcExtractable(m.ResourceModeTime, 100, 0, 0, nil, &expireAt, now, loc)
|
||||||
|
if want := int64(0); got != want {
|
||||||
|
t.Fatalf("calcExtractable expired time = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user