mirror of
https://github.com/orris-inc/orris.git
synced 2026-05-06 21:44:01 +08:00
Replace direct uses of sub.CurrentPeriodStart/End with subscription.ResolveTrafficPeriod across the quota cache, forward usage endpoints, and DTOs. For calendar_month plans these windows diverge, causing the node hub real-time enforcer to query usage over the wrong window after a calendar reset and forward usage endpoints to return mis-aligned traffic figures. SubscriptionDTO and DashboardSubscriptionDTO now expose currentTrafficCycleStart/End plus the upload/download breakdown, so the frontend pairs figures with the matching window and skips a redundant traffic-stats request. Also demote context.Canceled / DeadlineExceeded inside Errorw/Error to WARN. Repository read paths previously logged client cancellations as ERRORs (e.g. dashboard 30s timeout produced an ERROR per repo on the call chain), which polluted alerts.
212 lines
7.7 KiB
Go
212 lines
7.7 KiB
Go
package usecases
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/orris-inc/orris/internal/domain/forward"
|
|
"github.com/orris-inc/orris/internal/domain/subscription"
|
|
"github.com/orris-inc/orris/internal/infrastructure/cache"
|
|
"github.com/orris-inc/orris/internal/shared/biztime"
|
|
"github.com/orris-inc/orris/internal/shared/errors"
|
|
"github.com/orris-inc/orris/internal/shared/logger"
|
|
)
|
|
|
|
// GetSubscriptionForwardUsageQuery represents the input for getting subscription forward usage.
|
|
type GetSubscriptionForwardUsageQuery struct {
|
|
SubscriptionID uint
|
|
}
|
|
|
|
// GetSubscriptionForwardUsageResult represents the subscription's forward rule usage and quota information.
|
|
type GetSubscriptionForwardUsageResult struct {
|
|
RuleCount int `json:"rule_count"`
|
|
RuleLimit int `json:"rule_limit"`
|
|
TrafficUsed uint64 `json:"traffic_used"` // in bytes
|
|
TrafficLimit uint64 `json:"traffic_limit"` // in bytes, 0 means unlimited
|
|
AllowedTypes []string `json:"allowed_types"`
|
|
}
|
|
|
|
// GetSubscriptionForwardUsageUseCase handles getting subscription forward usage.
|
|
type GetSubscriptionForwardUsageUseCase struct {
|
|
repo forward.RuleQuerier
|
|
subscriptionRepo subscription.SubscriptionRepository
|
|
planRepo subscription.PlanRepository
|
|
usageRepo subscription.SubscriptionUsageRepository // Legacy, kept for compatibility
|
|
usageStatsRepo subscription.SubscriptionUsageStatsRepository // For historical traffic data (>24h ago)
|
|
hourlyCache cache.HourlyTrafficCache // For recent traffic data (last 24h)
|
|
logger logger.Interface
|
|
}
|
|
|
|
// NewGetSubscriptionForwardUsageUseCase creates a new GetSubscriptionForwardUsageUseCase.
|
|
func NewGetSubscriptionForwardUsageUseCase(
|
|
repo forward.RuleQuerier,
|
|
subscriptionRepo subscription.SubscriptionRepository,
|
|
planRepo subscription.PlanRepository,
|
|
usageRepo subscription.SubscriptionUsageRepository,
|
|
usageStatsRepo subscription.SubscriptionUsageStatsRepository,
|
|
hourlyCache cache.HourlyTrafficCache,
|
|
logger logger.Interface,
|
|
) *GetSubscriptionForwardUsageUseCase {
|
|
return &GetSubscriptionForwardUsageUseCase{
|
|
repo: repo,
|
|
subscriptionRepo: subscriptionRepo,
|
|
planRepo: planRepo,
|
|
usageRepo: usageRepo,
|
|
usageStatsRepo: usageStatsRepo,
|
|
hourlyCache: hourlyCache,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Execute retrieves forward rule usage statistics for a specific subscription.
|
|
func (uc *GetSubscriptionForwardUsageUseCase) Execute(ctx context.Context, query GetSubscriptionForwardUsageQuery) (*GetSubscriptionForwardUsageResult, error) {
|
|
uc.logger.Debugw("executing get subscription forward usage use case", "subscription_id", query.SubscriptionID)
|
|
|
|
// Validate subscription ID
|
|
if query.SubscriptionID == 0 {
|
|
return nil, errors.NewValidationError("subscription_id is required")
|
|
}
|
|
|
|
// Get the subscription
|
|
sub, err := uc.subscriptionRepo.GetByID(ctx, query.SubscriptionID)
|
|
if err != nil {
|
|
uc.logger.Errorw("failed to get subscription", "subscription_id", query.SubscriptionID, "error", err)
|
|
return nil, fmt.Errorf("failed to get subscription: %w", err)
|
|
}
|
|
if sub == nil {
|
|
return nil, errors.NewNotFoundError("subscription", fmt.Sprintf("%d", query.SubscriptionID))
|
|
}
|
|
|
|
// Get the subscription's plan
|
|
plan, err := uc.planRepo.GetByID(ctx, sub.PlanID())
|
|
if err != nil {
|
|
uc.logger.Errorw("failed to get plan", "plan_id", sub.PlanID(), "error", err)
|
|
return nil, fmt.Errorf("failed to get plan: %w", err)
|
|
}
|
|
if plan == nil {
|
|
return nil, errors.NewNotFoundError("plan", fmt.Sprintf("%d", sub.PlanID()))
|
|
}
|
|
|
|
// Check if plan is forward type
|
|
if !plan.PlanType().IsForward() {
|
|
uc.logger.Warnw("subscription plan does not support forward rules",
|
|
"subscription_id", query.SubscriptionID,
|
|
"plan_type", plan.PlanType().String(),
|
|
)
|
|
return nil, errors.NewValidationError("subscription plan does not support forward rules")
|
|
}
|
|
|
|
planFeatures := plan.Features()
|
|
if planFeatures == nil {
|
|
return nil, errors.NewValidationError("plan features not configured")
|
|
}
|
|
|
|
// Get rule limit
|
|
ruleLimit, err := planFeatures.GetRuleLimit()
|
|
if err != nil {
|
|
uc.logger.Warnw("failed to get rule limit", "subscription_id", query.SubscriptionID, "error", err)
|
|
ruleLimit = 0 // Default to unlimited on error
|
|
}
|
|
|
|
// Get traffic limit
|
|
trafficLimit, err := planFeatures.GetTrafficLimit()
|
|
if err != nil {
|
|
uc.logger.Warnw("failed to get traffic limit", "subscription_id", query.SubscriptionID, "error", err)
|
|
trafficLimit = 0 // Default to unlimited on error
|
|
}
|
|
|
|
// Get allowed rule types
|
|
allowedTypes, err := planFeatures.GetRuleTypes()
|
|
if err != nil {
|
|
uc.logger.Warnw("failed to get rule types", "subscription_id", query.SubscriptionID, "error", err)
|
|
allowedTypes = []string{"direct", "entry", "chain", "direct_chain"} // Default to all types on error
|
|
}
|
|
// Empty means all types allowed
|
|
if len(allowedTypes) == 0 {
|
|
allowedTypes = []string{"direct", "entry", "chain", "direct_chain"}
|
|
}
|
|
|
|
// Count current rules for this subscription
|
|
ruleCount, err := uc.repo.CountBySubscriptionID(ctx, query.SubscriptionID)
|
|
if err != nil {
|
|
uc.logger.Errorw("failed to count subscription forward rules", "subscription_id", query.SubscriptionID, "error", err)
|
|
return nil, fmt.Errorf("failed to get rule count: %w", err)
|
|
}
|
|
|
|
// Query traffic usage from Redis (recent) and MySQL stats (historical).
|
|
// Use the resolved *traffic cycle* (calendar_month or billing_cycle) so
|
|
// the displayed traffic_used matches what quota enforcement counts.
|
|
var trafficUsed uint64
|
|
cycle := subscription.ResolveTrafficPeriod(plan, sub)
|
|
periodStart := cycle.Start
|
|
periodEnd := biztime.EndOfDayUTC(cycle.End)
|
|
now := biztime.NowUTC()
|
|
|
|
// Use start of yesterday's business day as batch/speed boundary (Lambda architecture)
|
|
// MySQL: complete days before yesterday; Redis: yesterday + today (within 48h TTL)
|
|
recentBoundary := biztime.StartOfDayUTC(now.AddDate(0, 0, -1))
|
|
|
|
subscriptionIDs := []uint{query.SubscriptionID}
|
|
resourceType := string(subscription.ResourceTypeForwardRule)
|
|
|
|
// Determine time boundaries for recent data (yesterday + today from Redis)
|
|
recentFrom := periodStart
|
|
if recentFrom.Before(recentBoundary) {
|
|
recentFrom = recentBoundary
|
|
}
|
|
|
|
// Get recent traffic from Redis (yesterday + today)
|
|
if recentFrom.Before(periodEnd) && recentFrom.Before(now) {
|
|
recentTo := periodEnd
|
|
if recentTo.After(now) {
|
|
recentTo = now
|
|
}
|
|
recentTraffic, err := uc.hourlyCache.GetTotalTrafficBySubscriptionIDs(
|
|
ctx, subscriptionIDs, resourceType, recentFrom, recentTo,
|
|
)
|
|
if err != nil {
|
|
uc.logger.Warnw("failed to get recent traffic from Redis", "subscription_id", query.SubscriptionID, "error", err)
|
|
} else {
|
|
for _, t := range recentTraffic {
|
|
trafficUsed += t.Total
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get historical traffic from MySQL stats (complete days before yesterday)
|
|
if periodStart.Before(recentBoundary) {
|
|
historicalTo := recentBoundary.Add(-time.Second)
|
|
if historicalTo.After(periodEnd) {
|
|
historicalTo = periodEnd
|
|
}
|
|
historicalTraffic, err := uc.usageStatsRepo.GetTotalBySubscriptionIDs(
|
|
ctx, subscriptionIDs, &resourceType, subscription.GranularityDaily, periodStart, historicalTo,
|
|
)
|
|
if err != nil {
|
|
uc.logger.Warnw("failed to get historical traffic from stats", "subscription_id", query.SubscriptionID, "error", err)
|
|
} else if historicalTraffic != nil {
|
|
trafficUsed += historicalTraffic.Total
|
|
}
|
|
}
|
|
|
|
result := &GetSubscriptionForwardUsageResult{
|
|
RuleCount: int(ruleCount),
|
|
RuleLimit: ruleLimit,
|
|
TrafficUsed: trafficUsed,
|
|
TrafficLimit: trafficLimit,
|
|
AllowedTypes: allowedTypes,
|
|
}
|
|
|
|
uc.logger.Debugw("subscription forward usage retrieved successfully",
|
|
"subscription_id", query.SubscriptionID,
|
|
"rule_count", ruleCount,
|
|
"rule_limit", ruleLimit,
|
|
"traffic_used", trafficUsed,
|
|
"traffic_limit", trafficLimit,
|
|
"allowed_types", allowedTypes,
|
|
)
|
|
|
|
return result, nil
|
|
}
|