feat: 添加代理充值功能到代理管理页面, 移除授权管理页面的无效操作

This commit is contained in:
2026-05-07 01:13:09 +08:00
parent d9f4574f33
commit a2eccb9818
6 changed files with 154 additions and 16 deletions
+41
View File
@@ -21,6 +21,7 @@ func SetupAgentsRoutes(r *gin.RouterGroup) {
agents.GET("/:id", handleGetAgentDetail)
agents.PUT("/:id", handleUpdateAgent)
agents.PUT("/:id/status", handleUpdateAgentStatus)
agents.POST("/:id/recharge", handleRechargeAgent)
agents.DELETE("/:id", handleDeleteAgent)
agents.GET("/:id/cards", handleGetAgentCards)
agents.PUT("/:id/cards", handleUpdateAgentCards)
@@ -77,6 +78,7 @@ func handleGetAgents(c *gin.Context) {
CreatedAt string `json:"created_at"`
LastLoginAt string `json:"last_login_at"`
Balance float64 `json:"balance"`
TotalConsumption float64 `json:"total_consumption"`
CanCreateAgent bool `json:"can_create_agent"`
CardsCount int `json:"cards_count"`
ChildAgentsCount int `json:"child_agents_count"`
@@ -149,6 +151,7 @@ func handleGetAgents(c *gin.Context) {
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
LastLoginAt: lastLoginAt,
Balance: user.Balance,
TotalConsumption: user.TotalConsumption,
CanCreateAgent: user.CanCreateAgent,
CardsCount: cardsCountMap[user.ID],
ChildAgentsCount: childCountMap[user.ID],
@@ -678,3 +681,41 @@ func handleBatchDeleteAgents(c *gin.Context) {
"deleted": len(req.AgentIDs),
})
}
func handleRechargeAgent(c *gin.Context) {
agentID := c.Param("id")
var user model.User
if err := database.DB.Where("id = ? AND role = ?", agentID, "agent").First(&user).Error; err != nil {
response.Error(c, 404, "代理不存在")
return
}
var req struct {
Amount float64 `json:"amount" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "参数错误")
return
}
if req.Amount <= 0 {
response.Error(c, 400, "充值金额必须大于0")
return
}
newBalance := user.Balance + req.Amount
if err := database.DB.Model(&user).Update("balance", newBalance).Error; err != nil {
response.Error(c, 500, "充值失败")
return
}
service.LogOperation(c, "recharge", "agent", &user.ID, fmt.Sprintf("为代理充值: %.2f, 余额: %.2f -> %.2f", req.Amount, user.Balance, newBalance), nil)
response.Success(c, gin.H{
"balance": newBalance,
"recharge": req.Amount,
"agent_id": user.ID,
"agent_name": user.Username,
})
}
+1 -14
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { CheckCircle, Edit, Key, MoreHorizontal, Plus, Share2, Trash2, Wallet, XCircle } from 'lucide-vue-next'
import { CheckCircle, Key, MoreHorizontal, Plus, Share2, Trash2, XCircle } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -256,19 +256,6 @@ onMounted(() => {
</UiButton>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent align="end">
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/edit`)">
<Edit class="mr-2 h-4 w-4" />
编辑授权
</UiDropdownMenuItem>
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/card-types`)">
<Key class="mr-2 h-4 w-4" />
卡密权限
</UiDropdownMenuItem>
<UiDropdownMenuItem @click="router.push(`/admin/agent-apps/${item.id}/recharge`)">
<Wallet class="mr-2 h-4 w-4" />
充值余额
</UiDropdownMenuItem>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem class="text-destructive" @click="confirmRemove(item)">
<Trash2 class="mr-2 h-4 w-4" />
移除授权
@@ -1,7 +1,7 @@
import type { ColumnDef, Row } from '@tanstack/vue-table'
import type { Composer } from 'vue-i18n'
import { Ban, Check, MoreHorizontal, Pencil, Trash2, X } from 'lucide-vue-next'
import { Ban, Check, MoreHorizontal, Pencil, Trash2, Wallet, X } from 'lucide-vue-next'
import { h } from 'vue'
import type { Agent } from '../data/schema'
@@ -27,6 +27,7 @@ export function getColumns(actions: {
onToggleStatus: (row: Agent) => void
onEdit: (row: Agent) => void
onDelete: (row: Agent) => void
onRecharge: (row: Agent) => void
}, t: Composer['t']): ColumnDef<Agent>[] {
return [
{
@@ -172,6 +173,10 @@ export function getColumns(actions: {
h(Pencil, { class: 'mr-2 h-4 w-4' }),
t('common.edit'),
]),
h(DropdownMenuItem, { onClick: () => actions.onRecharge(agent) }, () => [
h(Wallet, { class: 'mr-2 h-4 w-4' }),
t('admin.agents.recharge'),
]),
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(agent) }, () => [
h(Ban, { class: 'mr-2 h-4 w-4' }),
isBanned ? t('admin.agents.enable') : t('admin.agents.disable'),
@@ -20,6 +20,7 @@ const props = defineProps<Omit<DataTableProps<Agent>, 'columns'> & {
onToggleStatus: (row: Agent) => void
onEdit: (row: Agent) => void
onDelete: (row: Agent) => void
onRecharge: (row: Agent) => void
}>()
const emit = defineEmits<{
@@ -39,6 +40,7 @@ const columns = computed(() => [
onToggleStatus: props.onToggleStatus,
onEdit: props.onEdit,
onDelete: props.onDelete,
onRecharge: props.onRecharge,
}, t),
])
+103 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Ban, CheckCircle, Clock, Plus, Users } from 'lucide-vue-next'
import { Ban, CheckCircle, Clock, Plus, Users, Wallet } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -28,6 +28,11 @@ const deleteTarget = ref<Agent | null>(null)
const batchDeleteDialogOpen = ref(false)
const batchDeleteIds = ref<number[]>([])
const rechargeDialogOpen = ref(false)
const rechargeTarget = ref<Agent | null>(null)
const rechargeAmount = ref(0)
const rechargeSaving = ref(false)
const filteredAgents = computed(() => {
let result = agents.value
@@ -216,6 +221,38 @@ async function batchToggleStatus(ids: number[], status: string) {
}
}
function handleRecharge(agent: Agent) {
rechargeTarget.value = agent
rechargeAmount.value = 0
rechargeDialogOpen.value = true
}
async function handleRechargeSubmit() {
if (!rechargeTarget.value || rechargeAmount.value <= 0) {
toast.error('请输入有效的充值金额')
return
}
rechargeSaving.value = true
try {
await api.post(`/dev/agents/${rechargeTarget.value.id}/recharge`, { amount: rechargeAmount.value })
toast.success('充值成功')
rechargeDialogOpen.value = false
fetchAgents()
} catch (error: any) {
console.error('充值失败:', error)
toast.error(error.message || '充值失败')
} finally {
rechargeSaving.value = false
}
}
const quickAmounts = [100, 500, 1000, 5000]
function setRechargeAmount(amount: number) {
rechargeAmount.value = amount
}
onMounted(() => {
fetchAgents()
})
@@ -304,6 +341,7 @@ onMounted(() => {
:on-toggle-status="handleToggleStatus"
:on-edit="handleEdit"
:on-delete="confirmDelete"
:on-recharge="handleRecharge"
@refresh="fetchAgents"
@toggle-view="toggleViewMode"
@update:search-filter="searchFilter = $event"
@@ -352,5 +390,69 @@ onMounted(() => {
{{ t('admin.agents.batchDeleteConfirm', { count: batchDeleteIds.length }) }}
</template>
</ConfirmDialog>
<UiDialog v-model:open="rechargeDialogOpen">
<UiDialogContent class="sm:max-w-md">
<UiDialogHeader>
<UiDialogTitle>充值余额</UiDialogTitle>
<UiDialogDescription>
为代理 {{ rechargeTarget?.username }} 充值余额
</UiDialogDescription>
</UiDialogHeader>
<div class="space-y-4">
<div class="space-y-2">
<UiLabel>当前余额</UiLabel>
<div class="text-2xl font-bold text-primary">
¥{{ (rechargeTarget?.balance || 0).toFixed(2) }}
</div>
</div>
<div class="space-y-2">
<UiLabel>充值金额</UiLabel>
<UiInput
v-model.number="rechargeAmount"
type="number"
min="0"
step="0.01"
placeholder="请输入充值金额"
/>
</div>
<div class="flex flex-wrap gap-2">
<UiButton
v-for="amount in quickAmounts"
:key="amount"
variant="outline"
size="sm"
@click="setRechargeAmount(amount)"
>
¥{{ amount }}
</UiButton>
</div>
<div v-if="rechargeAmount > 0" class="p-4 bg-muted rounded-lg">
<div class="flex justify-between text-sm">
<span>当前余额</span>
<span>¥{{ (rechargeTarget?.balance || 0).toFixed(2) }}</span>
</div>
<div class="flex justify-between text-sm mt-1">
<span>充值金额</span>
<span class="text-green-600">+¥{{ rechargeAmount.toFixed(2) }}</span>
</div>
<UiSeparator class="my-2" />
<div class="flex justify-between font-medium">
<span>充值后余额</span>
<span>¥{{ ((rechargeTarget?.balance || 0) + rechargeAmount).toFixed(2) }}</span>
</div>
</div>
</div>
<UiDialogFooter>
<UiButton variant="outline" @click="rechargeDialogOpen = false">
取消
</UiButton>
<UiButton :disabled="rechargeSaving || rechargeAmount <= 0" @click="handleRechargeSubmit">
<span v-if="rechargeSaving" class="i-lucide-loader-2 mr-2 h-4 w-4 animate-spin" />
确认充值
</UiButton>
</UiDialogFooter>
</UiDialogContent>
</UiDialog>
</BasicPage>
</template>
+1
View File
@@ -1205,6 +1205,7 @@
"createdAt": "注册时间",
"lastLoginAt": "最后登录"
},
"recharge": "充值",
"form": {
"username": "用户名",
"usernamePlaceholder": "请输入用户名",