feat(billing):添加交易记录的查询条件

This commit is contained in:
martin committed 2026-06-06 23:36:51 +08:00
1 parent 06aff72726
commit 664e083cf6
3 files changed
+160 -17

No files matched your search

+19 -5
View File
@@ -22,9 +22,16 @@ type TenantWalletRes struct {
}
type TenantWalletTransactionsReq struct {
g.Meta `path:"/wallet/transactions" method:"get" mime:"json" tags:"租户控制台-钱包" summary:"交易记录"`
Page int `json:"page" in:"query" d:"1"`
PageSize int `json:"page_size" in:"query" d:"20"`
g.Meta `path:"/wallet/transactions" method:"get" mime:"json" tags:"租户控制台-钱包" summary:"交易记录"`
Page int `json:"page" in:"query" d:"1"`
PageSize int `json:"page_size" in:"query" d:"20"`
Type string `json:"type" in:"query" dc:"交易类型筛选"`
StartDate string `json:"start_date" in:"query" dc:"开始日期"`
EndDate string `json:"end_date" in:"query" dc:"结束日期"`
AmountMin float64 `json:"amount_min" in:"query" dc:"最小金额"`
AmountMax float64 `json:"amount_max" in:"query" dc:"最大金额"`
Username string `json:"username" in:"query" dc:"用户名筛选"`
ModelName string `json:"model_name" in:"query" dc:"模型名称筛选"`
}
type TenantWalletTransactionsRes struct {
@@ -36,8 +43,15 @@ type TenantWalletTransactionsRes struct {
// TenantWalletTransactionsExportReq 导出交易记录请求
type TenantWalletTransactionsExportReq struct {
g.Meta `path:"/wallet/transactions/export" method:"get" mime:"json" tags:"租户控制台-钱包" summary:"导出交易记录"`
Format string `json:"format" in:"query" d:"csv" v:"in:csv,xlsx" dc:"导出格式:csv / xlsx"`
g.Meta `path:"/wallet/transactions/export" method:"get" mime:"json" tags:"租户控制台-钱包" summary:"导出交易记录"`
Format string `json:"format" in:"query" d:"csv" v:"in:csv,xlsx" dc:"导出格式:csv / xlsx"`
Type string `json:"type" in:"query" dc:"交易类型筛选"`
StartDate string `json:"start_date" in:"query" dc:"开始日期"`
EndDate string `json:"end_date" in:"query" dc:"结束日期"`
AmountMin float64 `json:"amount_min" in:"query" dc:"最小金额"`
AmountMax float64 `json:"amount_max" in:"query" dc:"最大金额"`
Username string `json:"username" in:"query" dc:"用户名筛选"`
ModelName string `json:"model_name" in:"query" dc:"模型名称筛选"`
}
type TenantWalletTransactionsExportRes struct{}
+45 -3
View File
@@ -84,6 +84,28 @@ func (s *sTenant) WalletTransactions(ctx context.Context, req *v1.TenantWalletTr
query := dao.BilTransactions.Ctx(ctx).
Where("bil_transactions.tenant_id", tenantID)
if req.Type != "" {
query = query.Where("bil_transactions.type", req.Type)
}
if req.StartDate != "" {
query = query.Where("bil_transactions.created_at >= ?", req.StartDate+" 00:00:00")
}
if req.EndDate != "" {
query = query.Where("bil_transactions.created_at <= ?", req.EndDate+" 23:59:59")
}
if req.AmountMin != 0 {
query = query.Where("bil_transactions.amount >= ?", req.AmountMin)
}
if req.AmountMax != 0 {
query = query.Where("bil_transactions.amount <= ?", req.AmountMax)
}
if req.Username != "" {
query = query.Where("tu.username LIKE ?", "%"+req.Username+"%")
}
if req.ModelName != "" {
query = query.Where("bil_transactions.model_name LIKE ?", "%"+req.ModelName+"%")
}
type transactionRow struct {
Id int64 `json:"id"`
Type string `json:"type"`
@@ -410,10 +432,30 @@ func (s *sTenant) ExportWalletTransactions(ctx context.Context, req *v1.TenantWa
CreatedAt *gtime.Time `json:"created_at"`
}
q := dao.BilTransactions.Ctx(ctx).
Where("tenant_id", tenantID)
if req.Type != "" {
q = q.Where("type", req.Type)
}
if req.StartDate != "" {
q = q.Where("created_at >= ?", req.StartDate+" 00:00:00")
}
if req.EndDate != "" {
q = q.Where("created_at <= ?", req.EndDate+" 23:59:59")
}
if req.AmountMin != 0 {
q = q.Where("amount >= ?", req.AmountMin)
}
if req.AmountMax != 0 {
q = q.Where("amount <= ?", req.AmountMax)
}
if req.ModelName != "" {
q = q.Where("model_name LIKE ?", "%"+req.ModelName+"%")
}
var records []transactionRow
err := dao.BilTransactions.Ctx(ctx).
Where("tenant_id", tenantID).
Fields("id, type, amount, balance_after, user_id, request_id, model_name, description, created_at").
err := q.Fields("id, type, amount, balance_after, user_id, request_id, model_name, description, created_at").
OrderDesc("created_at").
Limit(1000).Offset(offset).
Scan(&records)
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Icon from '@/components/common/Icon.vue'
import BaseSelect from '@/components/common/BaseSelect.vue'
import request from '@/utils/request'
import { useExport } from '@/composables/useExport'
@@ -10,9 +11,27 @@ const page = ref(1)
const pageSize = 20
const total = ref(0)
// Filter state
const filterType = ref('')
const filterStartDate = ref('')
const filterEndDate = ref('')
const filterAmountMin = ref('')
const filterAmountMax = ref('')
const filterUsername = ref('')
const filterModel = ref('')
const showExportDropdown = ref(false)
const { exporting, exportFile } = useExport({
url: '/tenant/wallet/transactions/export',
getFilters: () => ({
type: filterType.value,
start_date: filterStartDate.value,
end_date: filterEndDate.value,
amount_min: filterAmountMin.value || undefined,
amount_max: filterAmountMax.value || undefined,
username: filterUsername.value,
model_name: filterModel.value,
}),
})
const txTypeLabel: Record<string, string> = {
@@ -37,6 +56,18 @@ const txTypeBadgeClass: Record<string, string> = {
unfreeze: 'badge-gray',
}
const typeOptions = [
{ value: '', label: '全部' },
{ value: 'recharge', label: '充值' },
{ value: 'consume', label: '消费' },
{ value: 'pre_deduct', label: '预扣' },
{ value: 'settle', label: '结算' },
{ value: 'refund', label: '退款' },
{ value: 'adjust', label: '调整' },
{ value: 'freeze', label: '冻结' },
{ value: 'unfreeze', label: '解冻' },
]
function formatAmount(amount: number): string {
if (amount >= 0) return '+$' + amount.toFixed(6)
return '-$' + Math.abs(amount).toFixed(6)
@@ -45,8 +76,17 @@ function formatAmount(amount: number): string {
async function fetchTransactions() {
loading.value = true
try {
const params: any = { page: page.value, page_size: pageSize }
if (filterType.value) params.type = filterType.value
if (filterStartDate.value) params.start_date = filterStartDate.value
if (filterEndDate.value) params.end_date = filterEndDate.value
if (filterAmountMin.value) params.amount_min = filterAmountMin.value
if (filterAmountMax.value) params.amount_max = filterAmountMax.value
if (filterUsername.value) params.username = filterUsername.value
if (filterModel.value) params.model_name = filterModel.value
const res: any = await request.get('/tenant/wallet/transactions', {
params: { page: page.value, page_size: pageSize },
params,
})
const raw = res.data?.data
transactions.value = Array.isArray(raw) ? raw : (raw?.data || raw?.list || [])
@@ -58,6 +98,23 @@ async function fetchTransactions() {
}
}
function applyFilters() {
page.value = 1
fetchTransactions()
}
function resetFilters() {
filterType.value = ''
filterStartDate.value = ''
filterEndDate.value = ''
filterAmountMin.value = ''
filterAmountMax.value = ''
filterUsername.value = ''
filterModel.value = ''
page.value = 1
fetchTransactions()
}
function prevPage() {
if (page.value > 1) {
page.value--
@@ -102,6 +159,44 @@ onMounted(fetchTransactions)
</div>
</div>
<!-- Filters -->
<div class="card">
<div class="card-body">
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500 whitespace-nowrap">开始日期</label>
<input v-model="filterStartDate" type="date" class="input" style="width:140px" />
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500 whitespace-nowrap">结束日期</label>
<input v-model="filterEndDate" type="date" class="input" style="width:140px" />
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500 whitespace-nowrap">类型</label>
<BaseSelect v-model="filterType" :options="typeOptions" container-class="w-[100px]" />
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500 whitespace-nowrap">金额范围</label>
<input v-model="filterAmountMin" type="number" step="0.01" placeholder="最小" class="input" style="width:100px" @keyup.enter="applyFilters" />
<span class="text-gray-400">~</span>
<input v-model="filterAmountMax" type="number" step="0.01" placeholder="最大" class="input" style="width:100px" @keyup.enter="applyFilters" />
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500 whitespace-nowrap">用户名</label>
<input v-model="filterUsername" type="text" placeholder="搜索用户" class="input" style="width:120px" @keyup.enter="applyFilters" />
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500 whitespace-nowrap">模型</label>
<input v-model="filterModel" type="text" placeholder="例如:gpt-4o" class="input" style="width:150px" @keyup.enter="applyFilters" />
</div>
<div class="ml-auto flex items-center gap-2">
<button class="btn btn-primary btn-sm" @click="applyFilters">搜索</button>
<button class="btn btn-secondary btn-sm" @click="resetFilters">重置</button>
</div>
</div>
</div>
</div>
<!-- Transactions -->
<div class="card">
<div v-if="loading" class="p-8 flex justify-center">
@@ -114,7 +209,6 @@ onMounted(fetchTransactions)
<tr>
<th class="min-w-20">类型</th>
<th class="min-w-30">金额</th>
<th class="min-w-30">扣费来源</th>
<th class="min-w-30">余额</th>
<th class="min-w-35">用户</th>
<th class="min-w-50">请求ID</th>
@@ -133,13 +227,6 @@ onMounted(fetchTransactions)
<td :class="tx.amount >= 0 ? 'text-emerald-600 font-semibold' : 'text-red-600 font-semibold'">
{{ formatAmount(tx.amount) }}
</td>
<td class="text-gray-700">
<template v-if="tx.type === 'consume' || tx.type === 'settle'">
<span v-if="tx.plan_deduction > 0" class="badge badge-primary text-xs mr-1">套餐抵扣</span>
<span v-if="tx.wallet_deduction > 0" class="badge badge-gray text-xs">钱包</span>
</template>
<template v-else>--</template>
</td>
<td class="text-gray-700">${{ tx.balance_after?.toFixed(6) ?? '--' }}</td>
<td class="text-gray-700 text-sm">{{ tx.username || '--' }}</td>
<td class="text-gray-500 text-xs font-mono">{{ tx.request_id || '--' }}</td>