fix: batch 2 audit fixes — diffSettings notify fields, slog migration, frontend constants
H5: diffSettings now tracks 5 balance/quota notify fields in audit log
M15: log.Printf audit log migrated to slog.Info, removed "log" import
M14: New frontend/src/constants/account.ts with shared constants
QuotaNotifyToggle.vue uses QUOTA_THRESHOLD_TYPE_FIXED/PERCENTAGE
L2: UsageTable.vue uses BILLING_MODE_TOKEN/IMAGE from billingMode.ts
This commit is contained in:
@@ -1 +1 @@
|
|||||||
0.1.110.47
|
0.1.110.48
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||||
@@ -1120,11 +1119,11 @@ func (h *SettingHandler) auditSettingsUpdate(c *gin.Context, before *service.Sys
|
|||||||
|
|
||||||
subject, _ := middleware.GetAuthSubjectFromContext(c)
|
subject, _ := middleware.GetAuthSubjectFromContext(c)
|
||||||
role, _ := middleware.GetUserRoleFromContext(c)
|
role, _ := middleware.GetUserRoleFromContext(c)
|
||||||
log.Printf("AUDIT: settings updated at=%s user_id=%d role=%s changed=%v",
|
slog.Info("settings updated",
|
||||||
time.Now().UTC().Format(time.RFC3339),
|
"audit", true,
|
||||||
subject.UserID,
|
"user_id", subject.UserID,
|
||||||
role,
|
"role", role,
|
||||||
changed,
|
"changed", changed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1358,6 +1357,22 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
|||||||
if before.EnableCCHSigning != after.EnableCCHSigning {
|
if before.EnableCCHSigning != after.EnableCCHSigning {
|
||||||
changed = append(changed, "enable_cch_signing")
|
changed = append(changed, "enable_cch_signing")
|
||||||
}
|
}
|
||||||
|
// Balance & quota notification
|
||||||
|
if before.BalanceLowNotifyEnabled != after.BalanceLowNotifyEnabled {
|
||||||
|
changed = append(changed, "balance_low_notify_enabled")
|
||||||
|
}
|
||||||
|
if before.BalanceLowNotifyThreshold != after.BalanceLowNotifyThreshold {
|
||||||
|
changed = append(changed, "balance_low_notify_threshold")
|
||||||
|
}
|
||||||
|
if before.BalanceLowNotifyRechargeURL != after.BalanceLowNotifyRechargeURL {
|
||||||
|
changed = append(changed, "balance_low_notify_recharge_url")
|
||||||
|
}
|
||||||
|
if before.AccountQuotaNotifyEnabled != after.AccountQuotaNotifyEnabled {
|
||||||
|
changed = append(changed, "account_quota_notify_enabled")
|
||||||
|
}
|
||||||
|
if !equalNotifyEmailEntries(before.AccountQuotaNotifyEmails, after.AccountQuotaNotifyEmails) {
|
||||||
|
changed = append(changed, "account_quota_notify_emails")
|
||||||
|
}
|
||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1414,6 +1429,18 @@ func equalIntSlice(a, b []int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func equalNotifyEmailEntries(a, b []service.NotifyEmailEntry) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i].Email != b[i].Email || a[i].Verified != b[i].Verified || a[i].Disabled != b[i].Disabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// TestSMTPRequest 测试SMTP连接请求
|
// TestSMTPRequest 测试SMTP连接请求
|
||||||
type TestSMTPRequest struct {
|
type TestSMTPRequest struct {
|
||||||
SMTPHost string `json:"smtp_host"`
|
SMTPHost string `json:"smtp_host"`
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { QUOTA_THRESHOLD_TYPE_FIXED, QUOTA_THRESHOLD_TYPE_PERCENTAGE } from '@/constants/account'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
enabled: boolean | null
|
enabled: boolean | null
|
||||||
threshold: number | null
|
threshold: number | null
|
||||||
@@ -35,17 +37,17 @@ const emit = defineEmits<{
|
|||||||
@input="emit('update:threshold', parseFloat(($event.target as HTMLInputElement).value) || null)"
|
@input="emit('update:threshold', parseFloat(($event.target as HTMLInputElement).value) || null)"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
:max="thresholdType === 'percentage' ? 100 : undefined"
|
:max="thresholdType === QUOTA_THRESHOLD_TYPE_PERCENTAGE ? 100 : undefined"
|
||||||
:step="thresholdType === 'percentage' ? 1 : 0.01"
|
:step="thresholdType === QUOTA_THRESHOLD_TYPE_PERCENTAGE ? 1 : 0.01"
|
||||||
class="input py-1 text-sm flex-1 min-w-0"
|
class="input py-1 text-sm flex-1 min-w-0"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
:value="thresholdType || 'fixed'"
|
:value="thresholdType || QUOTA_THRESHOLD_TYPE_FIXED"
|
||||||
@change="emit('update:thresholdType', ($event.target as HTMLSelectElement).value)"
|
@change="emit('update:thresholdType', ($event.target as HTMLSelectElement).value)"
|
||||||
class="input py-1 text-xs w-[4.5rem] flex-shrink-0 text-center"
|
class="input py-1 text-xs w-[4.5rem] flex-shrink-0 text-center"
|
||||||
>
|
>
|
||||||
<option value="fixed">$</option>
|
<option :value="QUOTA_THRESHOLD_TYPE_FIXED">$</option>
|
||||||
<option value="percentage">%</option>
|
<option :value="QUOTA_THRESHOLD_TYPE_PERCENTAGE">%</option>
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
|
|
||||||
<template #cell-tokens="{ row }">
|
<template #cell-tokens="{ row }">
|
||||||
<!-- 图片生成请求(仅按次计费时显示图片格式) -->
|
<!-- 图片生成请求(仅按次计费时显示图片格式) -->
|
||||||
<div v-if="row.image_count > 0 && row.billing_mode === 'image'" class="flex items-center gap-1.5">
|
<div v-if="row.image_count > 0 && row.billing_mode === BILLING_MODE_IMAGE" class="flex items-center gap-1.5">
|
||||||
<svg class="h-4 w-4 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
<span class="font-medium text-white">${{ tooltipData.output_cost.toFixed(6) }}</span>
|
<span class="font-medium text-white">${{ tooltipData.output_cost.toFixed(6) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Token billing: show unit prices per 1M tokens -->
|
<!-- Token billing: show unit prices per 1M tokens -->
|
||||||
<template v-if="!tooltipData?.billing_mode || tooltipData.billing_mode === 'token'">
|
<template v-if="!tooltipData?.billing_mode || tooltipData.billing_mode === BILLING_MODE_TOKEN">
|
||||||
<div v-if="tooltipData && tooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
<div v-if="tooltipData && tooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
||||||
<span class="text-gray-400">{{ t('usage.inputTokenPrice') }}</span>
|
<span class="text-gray-400">{{ t('usage.inputTokenPrice') }}</span>
|
||||||
<span class="font-medium text-sky-300">{{ formatTokenPricePerMillion(tooltipData.input_cost, tooltipData.input_tokens) }} {{ t('usage.perMillionTokens') }}</span>
|
<span class="font-medium text-sky-300">{{ formatTokenPricePerMillion(tooltipData.input_cost, tooltipData.input_tokens) }} {{ t('usage.perMillionTokens') }}</span>
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<!-- Per-request / image billing: show unit price -->
|
<!-- Per-request / image billing: show unit price -->
|
||||||
<div v-else class="flex items-center justify-between gap-4">
|
<div v-else class="flex items-center justify-between gap-4">
|
||||||
<span class="text-gray-400">{{ tooltipData.billing_mode === 'image' ? t('usage.imageUnitPrice') : t('usage.unitPrice') }}</span>
|
<span class="text-gray-400">{{ tooltipData.billing_mode === BILLING_MODE_IMAGE ? t('usage.imageUnitPrice') : t('usage.unitPrice') }}</span>
|
||||||
<span class="font-medium text-sky-300">${{ tooltipData.total_cost?.toFixed(6) || '0.000000' }}</span>
|
<span class="font-medium text-sky-300">${{ tooltipData.total_cost?.toFixed(6) || '0.000000' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tooltipData && tooltipData.cache_creation_cost > 0" class="flex items-center justify-between gap-4">
|
<div v-if="tooltipData && tooltipData.cache_creation_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
@@ -350,7 +350,7 @@ import { formatCacheTokens, formatMultiplier } from '@/utils/formatters'
|
|||||||
import { formatTokenPricePerMillion } from '@/utils/usagePricing'
|
import { formatTokenPricePerMillion } from '@/utils/usagePricing'
|
||||||
import { getUsageServiceTierLabel } from '@/utils/usageServiceTier'
|
import { getUsageServiceTierLabel } from '@/utils/usageServiceTier'
|
||||||
import { resolveUsageRequestType } from '@/utils/usageRequestType'
|
import { resolveUsageRequestType } from '@/utils/usageRequestType'
|
||||||
import { getBillingModeLabel, getBillingModeBadgeClass } from '@/utils/billingMode'
|
import { getBillingModeLabel, getBillingModeBadgeClass, BILLING_MODE_TOKEN, BILLING_MODE_IMAGE } from '@/utils/billingMode'
|
||||||
|
|
||||||
/** Compute the account-billed cost for display: (account_stats_cost ?? total_cost) * rate_multiplier */
|
/** Compute the account-billed cost for display: (account_stats_cost ?? total_cost) * rate_multiplier */
|
||||||
function accountBilled(row: { total_cost?: number | null; account_stats_cost?: number | null; account_rate_multiplier?: number | null }): number {
|
function accountBilled(row: { total_cost?: number | null; account_stats_cost?: number | null; account_rate_multiplier?: number | null }): number {
|
||||||
|
|||||||
10
frontend/src/constants/account.ts
Normal file
10
frontend/src/constants/account.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** WebSearch emulation mode values (must match backend WebSearchMode* constants in account.go) */
|
||||||
|
export const WEB_SEARCH_MODE_DEFAULT = 'default' as const
|
||||||
|
export const WEB_SEARCH_MODE_ENABLED = 'enabled' as const
|
||||||
|
export const WEB_SEARCH_MODE_DISABLED = 'disabled' as const
|
||||||
|
export type WebSearchMode = typeof WEB_SEARCH_MODE_DEFAULT | typeof WEB_SEARCH_MODE_ENABLED | typeof WEB_SEARCH_MODE_DISABLED
|
||||||
|
|
||||||
|
/** Quota notification threshold type values (must match thresholdType* constants in balance_notify_service.go) */
|
||||||
|
export const QUOTA_THRESHOLD_TYPE_FIXED = 'fixed' as const
|
||||||
|
export const QUOTA_THRESHOLD_TYPE_PERCENTAGE = 'percentage' as const
|
||||||
|
export type QuotaThresholdType = typeof QUOTA_THRESHOLD_TYPE_FIXED | typeof QUOTA_THRESHOLD_TYPE_PERCENTAGE
|
||||||
Reference in New Issue
Block a user