fix finance balance delta recharges
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user