feat: add agent user edit, card type permission check and balance deduction
- Backend: check AgentApplicationCardType.can_generate before allowing card recharge - Backend: deduct agent balance and record consumption when creating user with card - Backend: add GET /agent/users/:id and PUT /agent/users/:id for user edit - Backend: extract checkAgentUserPermission helper, refactor handleUpdateUserStatus - Frontend: add user edit page ([id].vue) with username/email/password - Frontend: add edit action in user list dropdown menu - Frontend: add route for agent user edit page - Add i18n keys for agent user edit
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"verification-platform-backend/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetupAgentRoutes(r *gin.RouterGroup) {
|
||||
@@ -25,6 +26,8 @@ func SetupAgentRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/cards/generate", handleGenerateCards)
|
||||
r.GET("/users", handleGetUsers)
|
||||
r.POST("/users", handleCreateUser)
|
||||
r.PUT("/users/:id", handleUpdateUser)
|
||||
r.GET("/users/:id", handleGetUser)
|
||||
r.PUT("/users/:id/status", handleUpdateUserStatus)
|
||||
r.GET("/finance", handleGetFinance)
|
||||
r.GET("/profile", handleGetProfile)
|
||||
@@ -1005,13 +1008,33 @@ func handleCreateUser(c *gin.Context) {
|
||||
}
|
||||
|
||||
var cardType *model.CardType
|
||||
var agentCardType *model.AgentApplicationCardType
|
||||
if req.CardTypeID != nil && *req.CardTypeID > 0 {
|
||||
var ct model.CardType
|
||||
if err := database.DB.Where("id = ? AND application_id = ?", *req.CardTypeID, req.ApplicationID).First(&ct).Error; err != nil {
|
||||
response.Error(c, 400, "卡密类型不存在或不属于该应用")
|
||||
return
|
||||
}
|
||||
|
||||
var act model.AgentApplicationCardType
|
||||
if err := database.DB.Where("agent_application_id = ? AND card_type_id = ? AND can_generate = ?", agentApp.ID, *req.CardTypeID, true).First(&act).Error; err != nil {
|
||||
response.Error(c, 403, "无权使用该卡类充值")
|
||||
return
|
||||
}
|
||||
|
||||
totalPrice := act.Price * float64(req.CardQuantity)
|
||||
var agent model.User
|
||||
if err := database.DB.First(&agent, userID).Error; err != nil {
|
||||
response.Error(c, 404, "代理不存在")
|
||||
return
|
||||
}
|
||||
if agent.Balance < totalPrice {
|
||||
response.Error(c, 400, fmt.Sprintf("余额不足,当前余额: %.2f,需要: %.2f", agent.Balance, totalPrice))
|
||||
return
|
||||
}
|
||||
|
||||
cardType = &ct
|
||||
agentCardType = &act
|
||||
}
|
||||
|
||||
tx := database.DB.Begin()
|
||||
@@ -1121,6 +1144,27 @@ func handleCreateUser(c *gin.Context) {
|
||||
response.Error(c, 500, "充值失败")
|
||||
return
|
||||
}
|
||||
|
||||
totalPrice := agentCardType.Price * float64(req.CardQuantity)
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", userID).Update("balance", gorm.Expr("balance - ?", totalPrice)).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "扣除余额失败")
|
||||
return
|
||||
}
|
||||
|
||||
consumeRecord := model.RechargeRecord{
|
||||
UserID: userID,
|
||||
OrderNo: generateAgentOrderNo("C"),
|
||||
Amount: -totalPrice,
|
||||
Status: "success",
|
||||
PaymentType: "balance",
|
||||
Remark: fmt.Sprintf("创建用户充值卡密: %s x%d", cardType.Name, req.CardQuantity),
|
||||
}
|
||||
if err := tx.Create(&consumeRecord).Error; err != nil {
|
||||
tx.Rollback()
|
||||
response.Error(c, 500, "记录消费失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
@@ -1171,27 +1215,7 @@ func handleUpdateUserStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
agentIDs := []uint{userID}
|
||||
var childAgents []model.User
|
||||
database.DB.Where("parent_agent_id = ? AND role = ?", userID, "agent").Find(&childAgents)
|
||||
for _, child := range childAgents {
|
||||
agentIDs = append(agentIDs, child.ID)
|
||||
}
|
||||
|
||||
var cardIDs []uint
|
||||
database.DB.Model(&model.Card{}).Where("agent_id IN ?", agentIDs).Pluck("id", &cardIDs)
|
||||
|
||||
if len(cardIDs) == 0 {
|
||||
response.Error(c, 403, "无权限操作该用户")
|
||||
return
|
||||
}
|
||||
|
||||
var count int64
|
||||
database.DB.Model(&model.RechargeRecord{}).
|
||||
Where("user_id = ? AND card_id IN ? AND status = ?", user.ID, cardIDs, "success").
|
||||
Count(&count)
|
||||
|
||||
if count == 0 {
|
||||
if !checkAgentUserPermission(userID, user.ID) {
|
||||
response.Error(c, 403, "无权限操作该用户")
|
||||
return
|
||||
}
|
||||
@@ -1207,3 +1231,92 @@ func handleUpdateUserStatus(c *gin.Context) {
|
||||
|
||||
response.Success(c, user)
|
||||
}
|
||||
|
||||
func checkAgentUserPermission(userID uint, appUserID uint) bool {
|
||||
agentIDs := []uint{userID}
|
||||
var childAgents []model.User
|
||||
database.DB.Where("parent_agent_id = ? AND role = ?", userID, "agent").Find(&childAgents)
|
||||
for _, child := range childAgents {
|
||||
agentIDs = append(agentIDs, child.ID)
|
||||
}
|
||||
|
||||
var cardIDs []uint
|
||||
database.DB.Model(&model.Card{}).Where("agent_id IN ?", agentIDs).Pluck("id", &cardIDs)
|
||||
|
||||
if len(cardIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var count int64
|
||||
database.DB.Model(&model.RechargeRecord{}).
|
||||
Where("user_id = ? AND card_id IN ? AND status = ?", appUserID, cardIDs, "success").
|
||||
Count(&count)
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func handleGetUser(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.Preload("Application").First(&user, id).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if !checkAgentUserPermission(userID, user.ID) {
|
||||
response.Error(c, 403, "无权限查看该用户")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func handleUpdateUser(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
id := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AppUser
|
||||
if err := database.DB.First(&user, id).Error; err != nil {
|
||||
response.Error(c, 404, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if !checkAgentUserPermission(userID, user.ID) {
|
||||
response.Error(c, 403, "无权限修改该用户")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username != "" {
|
||||
user.Username = req.Username
|
||||
}
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.Password != "" {
|
||||
user.Password = req.Password
|
||||
}
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
response.Error(c, 500, "更新用户失败")
|
||||
return
|
||||
}
|
||||
|
||||
logDesc := fmt.Sprintf("代理更新用户: %s", user.Username)
|
||||
service.LogOperation(c, "update", "app_user", &user.ID, logDesc, nil)
|
||||
|
||||
response.Success(c, user)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, Eye, Loader2, UserPlus } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const userId = computed(() => route.params.id as string)
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
async function fetchUser() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<any>(`/agent/users/${userId.value}`)
|
||||
const user = data?.user || data
|
||||
if (user) {
|
||||
form.value.username = user.username || ''
|
||||
form.value.email = user.email || ''
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
toast.error(t('agent.users.edit.failed'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.username) {
|
||||
toast.error(t('agent.users.edit.usernameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: any = {
|
||||
username: form.value.username,
|
||||
email: form.value.email,
|
||||
}
|
||||
|
||||
if (form.value.password) {
|
||||
payload.password = form.value.password
|
||||
}
|
||||
|
||||
await api.put(`/agent/users/${userId.value}`, payload)
|
||||
toast.success(t('agent.users.edit.success'))
|
||||
router.push('/agent/users')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('更新用户失败:', error)
|
||||
toast.error(error.message || t('agent.users.edit.failed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('agent.users.edit.title')"
|
||||
:description="t('agent.users.edit.basicInfoDesc')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('agent.users.title'), href: '/agent/users' },
|
||||
{ title: t('agent.users.edit.title') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<UserPlus class="size-5" />
|
||||
{{ t('agent.users.edit.basicInfo') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('agent.users.edit.basicInfoDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="username">
|
||||
{{ t('agent.users.edit.username') }}
|
||||
</UiLabel>
|
||||
<UiInput id="username" v-model="form.username" :placeholder="t('agent.users.edit.usernamePlaceholder')" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="email">
|
||||
{{ t('agent.users.edit.email') }}
|
||||
</UiLabel>
|
||||
<UiInput id="email" v-model="form.email" type="email" :placeholder="t('agent.users.edit.emailPlaceholder')" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="password">
|
||||
{{ t('agent.users.edit.password') }}
|
||||
</UiLabel>
|
||||
<UiInput id="password" v-model="form.password" type="password" :placeholder="t('agent.users.edit.passwordPlaceholder')" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('agent.users.edit.passwordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Eye class="size-5" />
|
||||
{{ t('agent.users.edit.preview') }}
|
||||
</UiCardTitle>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('agent.users.edit.usernameLabel') }}</span>
|
||||
<span>{{ form.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('agent.users.edit.emailLabel') }}</span>
|
||||
<span>{{ form.email || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !form.username"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<Check v-else class="mr-2 h-4 w-4" />
|
||||
{{ t('agent.users.edit.saveBtn') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('agent.users.edit.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import type { Composer } from 'vue-i18n'
|
||||
|
||||
import { Ban, MoreHorizontal } from 'lucide-vue-next'
|
||||
import { Ban, MoreHorizontal, Pencil } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
export function getColumns(actions: {
|
||||
onEdit: (row: User) => void
|
||||
onToggleStatus: (row: User) => void
|
||||
}, t: Composer['t']): ColumnDef<User>[] {
|
||||
return [
|
||||
@@ -122,6 +123,10 @@ export function getColumns(actions: {
|
||||
DropdownMenuContent,
|
||||
{ align: 'end' },
|
||||
() => [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onEdit(user) }, () => [
|
||||
h(Pencil, { class: 'mr-2 h-4 w-4' }),
|
||||
t('common.edit'),
|
||||
]),
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(user) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isBanned ? t('common.unban') : t('common.ban'),
|
||||
|
||||
@@ -17,6 +17,7 @@ import DataTableToolbar from '@/pages/agent/users/components/data-table-toolbar.
|
||||
const props = defineProps<Omit<DataTableProps<User>, 'columns'> & {
|
||||
searchFilter?: string
|
||||
serverPagination?: DataTableProps<User>['serverPagination']
|
||||
onEdit: (row: User) => void
|
||||
onToggleStatus: (row: User) => void
|
||||
}>()
|
||||
|
||||
@@ -30,6 +31,7 @@ const { t } = useI18n()
|
||||
const columns = computed(() => [
|
||||
SelectColumn as ColumnDef<User>,
|
||||
...getColumns({
|
||||
onEdit: props.onEdit,
|
||||
onToggleStatus: props.onToggleStatus,
|
||||
}, t),
|
||||
])
|
||||
|
||||
@@ -75,6 +75,10 @@ function goToCreate() {
|
||||
router.push('/agent/users/create')
|
||||
}
|
||||
|
||||
function goToEdit(user: User) {
|
||||
router.push(`/agent/users/${user.id}`)
|
||||
}
|
||||
|
||||
function toggleUserStatus(user: User) {
|
||||
statusTarget.value = user
|
||||
statusAction.value = user.status === 'banned' ? 'unban' : 'ban'
|
||||
@@ -193,6 +197,7 @@ onMounted(() => {
|
||||
:data="users"
|
||||
:server-pagination="serverPagination"
|
||||
:search-filter="searchFilter"
|
||||
:on-edit="goToEdit"
|
||||
:on-toggle-status="toggleUserStatus"
|
||||
@refresh="fetchUsers"
|
||||
@update:search-filter="searchFilter = $event; currentPage = 1; fetchUsers()"
|
||||
|
||||
@@ -2823,6 +2823,26 @@
|
||||
"cancel": "Cancel",
|
||||
"success": "User created successfully",
|
||||
"failed": "Failed to create user"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit User",
|
||||
"basicInfo": "Basic Information",
|
||||
"basicInfoDesc": "Modify user basic information",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"usernameRequired": "Username is required",
|
||||
"email": "Email",
|
||||
"emailPlaceholder": "Enter email (optional)",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Leave empty to keep unchanged",
|
||||
"passwordHint": "Leave empty to keep the current password",
|
||||
"preview": "Preview",
|
||||
"usernameLabel": "Username",
|
||||
"emailLabel": "Email",
|
||||
"saveBtn": "Save Changes",
|
||||
"cancel": "Cancel",
|
||||
"success": "User updated successfully",
|
||||
"failed": "Failed to load user info"
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
|
||||
@@ -2825,6 +2825,26 @@
|
||||
"cancel": "取消",
|
||||
"success": "用户创建成功",
|
||||
"failed": "用户创建失败"
|
||||
},
|
||||
"edit": {
|
||||
"title": "编辑用户",
|
||||
"basicInfo": "基本信息",
|
||||
"basicInfoDesc": "修改用户基本信息",
|
||||
"username": "用户名",
|
||||
"usernamePlaceholder": "请输入用户名",
|
||||
"usernameRequired": "用户名不能为空",
|
||||
"email": "邮箱",
|
||||
"emailPlaceholder": "请输入邮箱(选填)",
|
||||
"password": "密码",
|
||||
"passwordPlaceholder": "留空则不修改",
|
||||
"passwordHint": "留空则不修改密码",
|
||||
"preview": "预览",
|
||||
"usernameLabel": "用户名",
|
||||
"emailLabel": "邮箱",
|
||||
"saveBtn": "保存修改",
|
||||
"cancel": "取消",
|
||||
"success": "用户信息更新成功",
|
||||
"failed": "获取用户信息失败"
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
|
||||
@@ -450,6 +450,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/pages/agent/users/create.vue'),
|
||||
meta: { title: '创建用户 - 代理商后台' },
|
||||
},
|
||||
{
|
||||
path: 'users/:id',
|
||||
name: 'AgentUserEdit',
|
||||
component: () => import('@/pages/agent/users/[id].vue'),
|
||||
meta: { title: '编辑用户 - 代理商后台' },
|
||||
},
|
||||
{
|
||||
path: 'finance',
|
||||
name: 'AgentFinance',
|
||||
|
||||
Reference in New Issue
Block a user