fix finance balance delta recharges

This commit is contained in:
SmartUp Developer
2026-07-03 20:05:28 +08:00
parent bb7ed5a7e0
commit 1670503720
12 changed files with 745 additions and 24 deletions
+183 -1
View File
@@ -247,6 +247,15 @@
<el-input-number v-model="form.balance_alert_threshold" :min="0" :precision="2" :value-on-clear="null" style="width: 100%" />
<div class="form-hint">余额低于此值时发送 Webhook 通知留空/0 表示不监控</div>
</el-form-item>
<el-form-item label="财务计费模式">
<el-select v-model="form.finance_cost_mode" style="width: 100%">
<el-option label="远端 usage 统计" value="usage_stats" />
<el-option label="余额差分统计" value="balance_delta" />
</el-select>
<div class="form-hint">
远端会清理使用记录的上游可选余额差分需配置余额接口首个完整采样日后才可用于对账
</div>
</el-form-item>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="检测间隔(秒)">
@@ -310,6 +319,76 @@
<span>{{ detailUpstream.last_error }}</span>
</div>
<div class="section-title">
<el-icon><Plus /></el-icon>
充值记录
<span class="section-sub">列表合计 {{ formatBalance(rechargeTotal) }}</span>
</div>
<el-alert
v-if="detailUpstream?.finance_cost_mode !== 'balance_delta'"
class="recharge-alert"
type="info"
:closable="false"
show-icon
title="该上游当前使用远端 usage 统计,通常不需要录入充值。"
/>
<el-form class="recharge-form" label-position="top">
<el-row :gutter="10">
<el-col :span="8">
<el-form-item label="日期">
<el-date-picker
v-model="rechargeForm.date"
type="date"
value-format="YYYY-MM-DD"
:disabled-date="disableFutureDate"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="金额">
<el-input-number v-model="rechargeForm.amount" :min="0.01" :precision="4" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="备注">
<el-input v-model="rechargeForm.note" maxlength="200" show-word-limit placeholder="可选" />
</el-form-item>
</el-col>
</el-row>
<div class="recharge-actions">
<el-button v-if="editingRechargeId" size="small" @click="resetRechargeForm">取消编辑</el-button>
<el-button
size="small"
type="primary"
:loading="rechargeSaving"
:disabled="!rechargeForm.date || !rechargeForm.amount || rechargeForm.amount <= 0"
@click="saveRecharge"
>
{{ editingRechargeId ? '保存充值' : '添加充值' }}
</el-button>
</div>
</el-form>
<el-table :data="recharges" v-loading="rechargeLoading" size="small" class="recharge-table">
<el-table-column prop="date" label="日期" width="120" />
<el-table-column label="金额" width="120">
<template #default="{ row }"><span class="mono">{{ formatBalance(row.amount) }}</span></template>
</el-table-column>
<el-table-column prop="note" label="备注" min-width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.note || '—' }}</template>
</el-table-column>
<el-table-column label="操作" width="120" align="right">
<template #default="{ row }">
<el-button size="small" text title="编辑充值记录" @click="editRecharge(row)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button size="small" text type="danger" title="删除充值记录" @click="confirmDeleteRecharge(row)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
<div class="section-title">
<el-icon><Key /></el-icon>
已创建 Key
@@ -463,7 +542,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance } from 'element-plus'
import dayjs from 'dayjs'
import { Refresh, Plus, Edit, List, Delete, Warning, Clock, ArrowRight, Pointer, Key } from '@element-plus/icons-vue'
import { upstreamsApi, type AuthCaptureCandidate, type GeneratedUpstreamKey, type UpstreamData, type UpstreamBatchActionResponse } from '@/api'
import { upstreamsApi, type AuthCaptureCandidate, type FinanceCostMode, type GeneratedUpstreamKey, type UpstreamData, type UpstreamBatchActionResponse, type UpstreamRecharge } from '@/api'
import AuthCaptureDialog from '@/components/AuthCaptureDialog.vue'
const list = ref<(UpstreamData & { _testing?: boolean; _checking?: boolean })[]>([])
@@ -551,6 +630,7 @@ const defaultForm = () => ({
balance_response_path: platformDefaults.sub2api.balance_response_path,
balance_divisor: platformDefaults.sub2api.balance_divisor,
balance_alert_threshold: null as number | null,
finance_cost_mode: 'usage_stats' as FinanceCostMode,
})
const form = ref(defaultForm())
const rules = {
@@ -862,11 +942,20 @@ const detailVisible = ref(false)
const detailUpstream = ref<UpstreamData | null>(null)
const snapshots = ref<any[]>([])
const generatedKeys = ref<GeneratedUpstreamKey[]>([])
const recharges = ref<UpstreamRecharge[]>([])
const snapshotLoading = ref(false)
const keysLoading = ref(false)
const rechargeLoading = ref(false)
const rechargeSaving = ref(false)
const expandedId = ref<number | null>(null)
const snapshotOffset = ref(0)
const snapshotLimit = 20
const editingRechargeId = ref<number | null>(null)
const rechargeForm = ref({
date: dayjs().format('YYYY-MM-DD'),
amount: null as number | null,
note: '',
})
const keyDialogVisible = ref(false)
const keyTarget = ref<UpstreamData | null>(null)
@@ -900,6 +989,10 @@ const healthyRate = computed(() => {
const pendingChecks = computed(() => list.value.filter((item) => !item.last_checked_at).length)
const rechargeTotal = computed(() =>
recharges.value.reduce((sum, item) => sum + (Number(item.amount) || 0), 0),
)
function usesTokenEndpointUpstream(row: UpstreamData | null) {
if (!row) return false
return row.api_prefix === ''
@@ -1006,6 +1099,7 @@ function openEdit(row: UpstreamData) {
balance_response_path: row.balance_response_path || '',
balance_divisor: row.balance_divisor ?? 1.0,
balance_alert_threshold: row.balance_alert_threshold ?? null,
finance_cost_mode: row.finance_cost_mode || 'usage_stats',
}
drawerVisible.value = true
}
@@ -1080,8 +1174,10 @@ function openDetail(row: UpstreamData) {
detailUpstream.value = row
snapshots.value = []
generatedKeys.value = []
recharges.value = []
snapshotOffset.value = 0
expandedId.value = null
resetRechargeForm()
_groupRowsCache.clear()
detailVisible.value = true
}
@@ -1100,6 +1196,7 @@ async function loadGeneratedKeys() {
async function loadSnapshots() {
if (!detailUpstream.value) return
loadGeneratedKeys()
loadRecharges()
snapshotLoading.value = true
try {
const res = await upstreamsApi.listSnapshots(detailUpstream.value.id, snapshotLimit, snapshotOffset.value)
@@ -1115,6 +1212,79 @@ async function loadSnapshots() {
}
}
function resetRechargeForm() {
editingRechargeId.value = null
rechargeForm.value = {
date: dayjs().format('YYYY-MM-DD'),
amount: null,
note: '',
}
}
function disableFutureDate(time: Date) {
return dayjs(time).isAfter(dayjs(), 'day')
}
async function loadRecharges() {
if (!detailUpstream.value) return
rechargeLoading.value = true
try {
const res = await upstreamsApi.listRecharges(detailUpstream.value.id)
recharges.value = res.data
} catch (e: any) {
ElMessage.error(e.response?.data?.detail || '加载充值记录失败')
} finally {
rechargeLoading.value = false
}
}
function editRecharge(row: UpstreamRecharge) {
editingRechargeId.value = row.id
rechargeForm.value = {
date: row.date,
amount: row.amount,
note: row.note || '',
}
}
async function saveRecharge() {
if (!detailUpstream.value || !rechargeForm.value.date || !rechargeForm.value.amount || rechargeForm.value.amount <= 0) return
rechargeSaving.value = true
try {
const payload = {
date: rechargeForm.value.date,
amount: rechargeForm.value.amount,
note: rechargeForm.value.note,
}
if (editingRechargeId.value) {
await upstreamsApi.updateRecharge(detailUpstream.value.id, editingRechargeId.value, payload)
ElMessage.success('充值记录已更新')
} else {
await upstreamsApi.createRecharge(detailUpstream.value.id, payload)
ElMessage.success('充值记录已添加')
}
resetRechargeForm()
await loadRecharges()
} catch (e: any) {
ElMessage.error(e.response?.data?.detail || '保存充值记录失败')
} finally {
rechargeSaving.value = false
}
}
async function confirmDeleteRecharge(row: UpstreamRecharge) {
if (!detailUpstream.value) return
try {
await ElMessageBox.confirm(`确认删除 ${row.date} 的充值记录?`, '删除确认', { type: 'warning' })
await upstreamsApi.deleteRecharge(detailUpstream.value.id, row.id)
ElMessage.success('充值记录已删除')
if (editingRechargeId.value === row.id) resetRechargeForm()
await loadRecharges()
} catch {
// noop
}
}
async function openKeyGenerate(row: UpstreamData) {
keyTarget.value = row
keyResults.value = []
@@ -1520,6 +1690,18 @@ onMounted(loadList)
font-weight: 500;
}
.recharge-alert,
.recharge-form,
.recharge-table {
margin-bottom: 1rem;
}
.recharge-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.snapshot-list {
display: grid;
gap: 0.7rem;