"frontend/src/views/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "e5857161ffde222e691002cf5df1589792a7b579"
Commit c31974c9 authored by IanShaw027's avatar IanShaw027
Browse files

fix: 兼容部分限额字段为空的情况 #1021

修复在填写限额时,如果不填写完整的三个限额额度(日限额、周限额、月限额)就会报错的问题。

变更内容:
- 后端:添加 optionalLimitField 类型处理空值和空字符串,兼容部分限额字段为空的情况
- 前端:添加 normalizeOptionalLimit 函数规范化限额输入,将空值、空字符串和无效数字统一处理为 null
parent 6da5fa01
package admin package admin
import ( import (
"bytes"
"encoding/json"
"fmt"
"strconv" "strconv"
"strings" "strings"
...@@ -16,6 +19,55 @@ type GroupHandler struct { ...@@ -16,6 +19,55 @@ type GroupHandler struct {
adminService service.AdminService adminService service.AdminService
} }
type optionalLimitField struct {
set bool
value *float64
}
func (f *optionalLimitField) UnmarshalJSON(data []byte) error {
f.set = true
trimmed := bytes.TrimSpace(data)
if bytes.Equal(trimmed, []byte("null")) {
f.value = nil
return nil
}
var number float64
if err := json.Unmarshal(trimmed, &number); err == nil {
f.value = &number
return nil
}
var text string
if err := json.Unmarshal(trimmed, &text); err == nil {
text = strings.TrimSpace(text)
if text == "" {
f.value = nil
return nil
}
number, err = strconv.ParseFloat(text, 64)
if err != nil {
return fmt.Errorf("invalid numeric limit value %q: %w", text, err)
}
f.value = &number
return nil
}
return fmt.Errorf("invalid limit value: %s", string(trimmed))
}
func (f optionalLimitField) ToServiceInput() *float64 {
if !f.set {
return nil
}
if f.value != nil {
return f.value
}
zero := 0.0
return &zero
}
// NewGroupHandler creates a new admin group handler // NewGroupHandler creates a new admin group handler
func NewGroupHandler(adminService service.AdminService) *GroupHandler { func NewGroupHandler(adminService service.AdminService) *GroupHandler {
return &GroupHandler{ return &GroupHandler{
...@@ -31,9 +83,9 @@ type CreateGroupRequest struct { ...@@ -31,9 +83,9 @@ type CreateGroupRequest struct {
RateMultiplier float64 `json:"rate_multiplier"` RateMultiplier float64 `json:"rate_multiplier"`
IsExclusive bool `json:"is_exclusive"` IsExclusive bool `json:"is_exclusive"`
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
DailyLimitUSD *float64 `json:"daily_limit_usd"` DailyLimitUSD optionalLimitField `json:"daily_limit_usd"`
WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"`
MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`
// 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置) // 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置)
ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice1K *float64 `json:"image_price_1k"`
ImagePrice2K *float64 `json:"image_price_2k"` ImagePrice2K *float64 `json:"image_price_2k"`
...@@ -69,9 +121,9 @@ type UpdateGroupRequest struct { ...@@ -69,9 +121,9 @@ type UpdateGroupRequest struct {
IsExclusive *bool `json:"is_exclusive"` IsExclusive *bool `json:"is_exclusive"`
Status string `json:"status" binding:"omitempty,oneof=active inactive"` Status string `json:"status" binding:"omitempty,oneof=active inactive"`
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
DailyLimitUSD *float64 `json:"daily_limit_usd"` DailyLimitUSD optionalLimitField `json:"daily_limit_usd"`
WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` WeeklyLimitUSD optionalLimitField `json:"weekly_limit_usd"`
MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` MonthlyLimitUSD optionalLimitField `json:"monthly_limit_usd"`
// 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置) // 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置)
ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice1K *float64 `json:"image_price_1k"`
ImagePrice2K *float64 `json:"image_price_2k"` ImagePrice2K *float64 `json:"image_price_2k"`
...@@ -191,9 +243,9 @@ func (h *GroupHandler) Create(c *gin.Context) { ...@@ -191,9 +243,9 @@ func (h *GroupHandler) Create(c *gin.Context) {
RateMultiplier: req.RateMultiplier, RateMultiplier: req.RateMultiplier,
IsExclusive: req.IsExclusive, IsExclusive: req.IsExclusive,
SubscriptionType: req.SubscriptionType, SubscriptionType: req.SubscriptionType,
DailyLimitUSD: req.DailyLimitUSD, DailyLimitUSD: req.DailyLimitUSD.ToServiceInput(),
WeeklyLimitUSD: req.WeeklyLimitUSD, WeeklyLimitUSD: req.WeeklyLimitUSD.ToServiceInput(),
MonthlyLimitUSD: req.MonthlyLimitUSD, MonthlyLimitUSD: req.MonthlyLimitUSD.ToServiceInput(),
ImagePrice1K: req.ImagePrice1K, ImagePrice1K: req.ImagePrice1K,
ImagePrice2K: req.ImagePrice2K, ImagePrice2K: req.ImagePrice2K,
ImagePrice4K: req.ImagePrice4K, ImagePrice4K: req.ImagePrice4K,
...@@ -244,9 +296,9 @@ func (h *GroupHandler) Update(c *gin.Context) { ...@@ -244,9 +296,9 @@ func (h *GroupHandler) Update(c *gin.Context) {
IsExclusive: req.IsExclusive, IsExclusive: req.IsExclusive,
Status: req.Status, Status: req.Status,
SubscriptionType: req.SubscriptionType, SubscriptionType: req.SubscriptionType,
DailyLimitUSD: req.DailyLimitUSD, DailyLimitUSD: req.DailyLimitUSD.ToServiceInput(),
WeeklyLimitUSD: req.WeeklyLimitUSD, WeeklyLimitUSD: req.WeeklyLimitUSD.ToServiceInput(),
MonthlyLimitUSD: req.MonthlyLimitUSD, MonthlyLimitUSD: req.MonthlyLimitUSD.ToServiceInput(),
ImagePrice1K: req.ImagePrice1K, ImagePrice1K: req.ImagePrice1K,
ImagePrice2K: req.ImagePrice2K, ImagePrice2K: req.ImagePrice2K,
ImagePrice4K: req.ImagePrice4K, ImagePrice4K: req.ImagePrice4K,
......
...@@ -2368,6 +2368,23 @@ const closeCreateModal = () => { ...@@ -2368,6 +2368,23 @@ const closeCreateModal = () => {
createModelRoutingRules.value = [] createModelRoutingRules.value = []
} }
const normalizeOptionalLimit = (value: number | string | null | undefined): number | null => {
if (value === null || value === undefined) {
return null
}
if (typeof value === 'string') {
const trimmed = value.trim()
if (!trimmed) {
return null
}
const parsed = Number(trimmed)
return Number.isFinite(parsed) && parsed > 0 ? parsed : null
}
return Number.isFinite(value) && value > 0 ? value : null
}
const handleCreateGroup = async () => { const handleCreateGroup = async () => {
if (!createForm.name.trim()) { if (!createForm.name.trim()) {
appStore.showError(t('admin.groups.nameRequired')) appStore.showError(t('admin.groups.nameRequired'))
...@@ -2379,6 +2396,9 @@ const handleCreateGroup = async () => { ...@@ -2379,6 +2396,9 @@ const handleCreateGroup = async () => {
const { sora_storage_quota_gb: createQuotaGb, ...createRest } = createForm const { sora_storage_quota_gb: createQuotaGb, ...createRest } = createForm
const requestData = { const requestData = {
...createRest, ...createRest,
daily_limit_usd: normalizeOptionalLimit(createForm.daily_limit_usd as number | string | null),
weekly_limit_usd: normalizeOptionalLimit(createForm.weekly_limit_usd as number | string | null),
monthly_limit_usd: normalizeOptionalLimit(createForm.monthly_limit_usd as number | string | null),
sora_storage_quota_bytes: createQuotaGb ? Math.round(createQuotaGb * 1024 * 1024 * 1024) : 0, sora_storage_quota_bytes: createQuotaGb ? Math.round(createQuotaGb * 1024 * 1024 * 1024) : 0,
model_routing: convertRoutingRulesToApiFormat(createModelRoutingRules.value) model_routing: convertRoutingRulesToApiFormat(createModelRoutingRules.value)
} }
...@@ -2457,6 +2477,9 @@ const handleUpdateGroup = async () => { ...@@ -2457,6 +2477,9 @@ const handleUpdateGroup = async () => {
const { sora_storage_quota_gb: editQuotaGb, ...editRest } = editForm const { sora_storage_quota_gb: editQuotaGb, ...editRest } = editForm
const payload = { const payload = {
...editRest, ...editRest,
daily_limit_usd: normalizeOptionalLimit(editForm.daily_limit_usd as number | string | null),
weekly_limit_usd: normalizeOptionalLimit(editForm.weekly_limit_usd as number | string | null),
monthly_limit_usd: normalizeOptionalLimit(editForm.monthly_limit_usd as number | string | null),
sora_storage_quota_bytes: editQuotaGb ? Math.round(editQuotaGb * 1024 * 1024 * 1024) : 0, sora_storage_quota_bytes: editQuotaGb ? Math.round(editQuotaGb * 1024 * 1024 * 1024) : 0,
fallback_group_id: editForm.fallback_group_id === null ? 0 : editForm.fallback_group_id, fallback_group_id: editForm.fallback_group_id === null ? 0 : editForm.fallback_group_id,
fallback_group_id_on_invalid_request: fallback_group_id_on_invalid_request:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment