fix: batch 1 audit fixes — quota SQL fixed mode, public recharge URL, WebSearch bool fallback, UpdatePlan validation
H1: incrementUsageBillingAccountQuota now uses shared dailyExpiredExpr/weeklyExpiredExpr
constants (supporting fixed reset mode) instead of hardcoded '24 hours'/'168 hours'
H4: public settings endpoint now maps balance_low_notify_recharge_url
H6: GetWebSearchEmulationMode tolerates legacy bool values (true→enabled)
H7: UpdatePlan validates non-nil patch fields (rejects negative price, empty name, etc.)
H8: UsageTable accountBilled() helper with total_cost ?? 0 null guard
H9: AdminUsageLog TS type adds channel_id + billing_tier
M2: account.go "fixed" literals replaced with thresholdTypeFixed constant
M13: SystemSettings TS type adds web_search_emulation_enabled
UI: QuotaLimitCard title labels now use flex-1 to align with flex-1 input boxes
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -1178,12 +1179,21 @@ const (
|
||||
|
||||
// GetWebSearchEmulationMode 返回账号的 WebSearch 模拟模式。
|
||||
// 三态:default(跟随渠道)/ enabled(强制开启)/ disabled(强制关闭)。
|
||||
// 旧 bool 值需通过 SQL 迁移脚本转换,Go 代码不做兼容。
|
||||
// 兼容旧 bool 值:true→enabled, false→default(并记录 debug 日志)。
|
||||
func (a *Account) GetWebSearchEmulationMode() string {
|
||||
if a == nil || a.Platform != PlatformAnthropic || a.Type != AccountTypeAPIKey || a.Extra == nil {
|
||||
return WebSearchModeDefault
|
||||
}
|
||||
mode, ok := a.Extra[featureKeyWebSearchEmulation].(string)
|
||||
raw := a.Extra[featureKeyWebSearchEmulation]
|
||||
// Tolerant: legacy bool values (pre-migration or stale writes)
|
||||
if b, ok := raw.(bool); ok {
|
||||
slog.Debug("legacy bool web_search_emulation value", "account_id", a.ID, "value", b)
|
||||
if b {
|
||||
return WebSearchModeEnabled
|
||||
}
|
||||
return WebSearchModeDefault
|
||||
}
|
||||
mode, ok := raw.(string)
|
||||
if !ok {
|
||||
return WebSearchModeDefault
|
||||
}
|
||||
@@ -1522,7 +1532,7 @@ func (a *Account) GetQuotaNotifyDailyThreshold() float64 {
|
||||
}
|
||||
|
||||
func (a *Account) GetQuotaNotifyDailyThresholdType() string {
|
||||
return a.getExtraStringDefault("quota_notify_daily_threshold_type", "fixed")
|
||||
return a.getExtraStringDefault("quota_notify_daily_threshold_type", thresholdTypeFixed)
|
||||
}
|
||||
|
||||
func (a *Account) GetQuotaNotifyWeeklyEnabled() bool {
|
||||
@@ -1534,7 +1544,7 @@ func (a *Account) GetQuotaNotifyWeeklyThreshold() float64 {
|
||||
}
|
||||
|
||||
func (a *Account) GetQuotaNotifyWeeklyThresholdType() string {
|
||||
return a.getExtraStringDefault("quota_notify_weekly_threshold_type", "fixed")
|
||||
return a.getExtraStringDefault("quota_notify_weekly_threshold_type", thresholdTypeFixed)
|
||||
}
|
||||
|
||||
func (a *Account) GetQuotaNotifyTotalEnabled() bool {
|
||||
@@ -1546,7 +1556,7 @@ func (a *Account) GetQuotaNotifyTotalThreshold() float64 {
|
||||
}
|
||||
|
||||
func (a *Account) GetQuotaNotifyTotalThresholdType() string {
|
||||
return a.getExtraStringDefault("quota_notify_total_threshold_type", "fixed")
|
||||
return a.getExtraStringDefault("quota_notify_total_threshold_type", thresholdTypeFixed)
|
||||
}
|
||||
|
||||
// nextFixedDailyReset 计算在 after 之后的下一个每日固定重置时间点
|
||||
|
||||
@@ -50,8 +50,8 @@ func TestGetWebSearchEmulationMode_OldBoolTrue(t *testing.T) {
|
||||
Type: AccountTypeAPIKey,
|
||||
Extra: map[string]any{featureKeyWebSearchEmulation: true},
|
||||
}
|
||||
// bool is not a string, type assertion fails → default
|
||||
require.Equal(t, WebSearchModeDefault, a.GetWebSearchEmulationMode())
|
||||
// bool true → tolerant fallback → enabled (not default)
|
||||
require.Equal(t, WebSearchModeEnabled, a.GetWebSearchEmulationMode())
|
||||
}
|
||||
|
||||
func TestGetWebSearchEmulationMode_OldBoolFalse(t *testing.T) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/group"
|
||||
@@ -10,6 +11,46 @@ import (
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
)
|
||||
|
||||
// validatePlanRequired checks that all required fields for a plan are provided.
|
||||
func validatePlanRequired(name string, groupID int64, price float64, validityDays int, validityUnit string) error {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return infraerrors.BadRequest("PLAN_NAME_REQUIRED", "plan name is required")
|
||||
}
|
||||
if groupID <= 0 {
|
||||
return infraerrors.BadRequest("PLAN_GROUP_REQUIRED", "group is required")
|
||||
}
|
||||
if price <= 0 {
|
||||
return infraerrors.BadRequest("PLAN_PRICE_INVALID", "price must be > 0")
|
||||
}
|
||||
if validityDays <= 0 {
|
||||
return infraerrors.BadRequest("PLAN_VALIDITY_REQUIRED", "validity days must be > 0")
|
||||
}
|
||||
if strings.TrimSpace(validityUnit) == "" {
|
||||
return infraerrors.BadRequest("PLAN_VALIDITY_UNIT_REQUIRED", "validity unit is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePlanPatch validates only the non-nil fields in a patch update.
|
||||
func validatePlanPatch(req UpdatePlanRequest) error {
|
||||
if req.Name != nil && strings.TrimSpace(*req.Name) == "" {
|
||||
return infraerrors.BadRequest("PLAN_NAME_REQUIRED", "plan name is required")
|
||||
}
|
||||
if req.GroupID != nil && *req.GroupID <= 0 {
|
||||
return infraerrors.BadRequest("PLAN_GROUP_REQUIRED", "group is required")
|
||||
}
|
||||
if req.Price != nil && *req.Price <= 0 {
|
||||
return infraerrors.BadRequest("PLAN_PRICE_INVALID", "price must be > 0")
|
||||
}
|
||||
if req.ValidityDays != nil && *req.ValidityDays <= 0 {
|
||||
return infraerrors.BadRequest("PLAN_VALIDITY_REQUIRED", "validity days must be > 0")
|
||||
}
|
||||
if req.ValidityUnit != nil && strings.TrimSpace(*req.ValidityUnit) == "" {
|
||||
return infraerrors.BadRequest("PLAN_VALIDITY_UNIT_REQUIRED", "validity unit is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Plan CRUD ---
|
||||
|
||||
// PlanGroupInfo holds the group details needed for subscription plan display.
|
||||
@@ -66,14 +107,17 @@ func (s *PaymentConfigService) GetGroupInfoMap(ctx context.Context, plans []*dbe
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) ListPlans(ctx context.Context) ([]*dbent.SubscriptionPlan, error) {
|
||||
return s.entClient.SubscriptionPlan.Query().Order(subscriptionplan.BySortOrder()).All(ctx)
|
||||
return s.entClient.SubscriptionPlan.Query().Order(subscriptionplan.ByCreatedAt()).All(ctx)
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) ListPlansForSale(ctx context.Context) ([]*dbent.SubscriptionPlan, error) {
|
||||
return s.entClient.SubscriptionPlan.Query().Where(subscriptionplan.ForSaleEQ(true)).Order(subscriptionplan.BySortOrder()).All(ctx)
|
||||
return s.entClient.SubscriptionPlan.Query().Where(subscriptionplan.ForSaleEQ(true)).Order(subscriptionplan.ByCreatedAt()).All(ctx)
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) CreatePlan(ctx context.Context, req CreatePlanRequest) (*dbent.SubscriptionPlan, error) {
|
||||
if err := validatePlanRequired(req.Name, req.GroupID, req.Price, req.ValidityDays, req.ValidityUnit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := s.entClient.SubscriptionPlan.Create().
|
||||
SetGroupID(req.GroupID).SetName(req.Name).SetDescription(req.Description).
|
||||
SetPrice(req.Price).SetValidityDays(req.ValidityDays).SetValidityUnit(req.ValidityUnit).
|
||||
@@ -86,8 +130,12 @@ func (s *PaymentConfigService) CreatePlan(ctx context.Context, req CreatePlanReq
|
||||
}
|
||||
|
||||
// UpdatePlan updates a subscription plan by ID (patch semantics).
|
||||
// NOTE: This function exceeds 30 lines due to per-field nil-check patch update boilerplate.
|
||||
// NOTE: This function exceeds 30 lines due to per-field nil-check patch update boilerplate
|
||||
// plus a validation guard for non-nil fields.
|
||||
func (s *PaymentConfigService) UpdatePlan(ctx context.Context, id int64, req UpdatePlanRequest) (*dbent.SubscriptionPlan, error) {
|
||||
if err := validatePlanPatch(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := s.entClient.SubscriptionPlan.UpdateOneID(id)
|
||||
if req.GroupID != nil {
|
||||
u.SetGroupID(*req.GroupID)
|
||||
|
||||
Reference in New Issue
Block a user