From a76a30ee782a2aada0abef74b2a0342cd37920d9 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 11 May 2026 08:48:02 +0800 Subject: [PATCH] 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 --- backend/internal/router/agent/agent.go | 155 +++++++++++++-- frontend/src/pages/agent/users/[id].vue | 181 ++++++++++++++++++ .../pages/agent/users/components/columns.ts | 7 +- .../agent/users/components/data-table.vue | 2 + frontend/src/pages/agent/users/index.vue | 5 + frontend/src/plugins/i18n/en.json | 20 ++ frontend/src/plugins/i18n/zh.json | 20 ++ frontend/src/router/routes.ts | 6 + 8 files changed, 374 insertions(+), 22 deletions(-) create mode 100644 frontend/src/pages/agent/users/[id].vue diff --git a/backend/internal/router/agent/agent.go b/backend/internal/router/agent/agent.go index 63ee69a..e34813f 100644 --- a/backend/internal/router/agent/agent.go +++ b/backend/internal/router/agent/agent.go @@ -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) +} diff --git a/frontend/src/pages/agent/users/[id].vue b/frontend/src/pages/agent/users/[id].vue new file mode 100644 index 0000000..7325f14 --- /dev/null +++ b/frontend/src/pages/agent/users/[id].vue @@ -0,0 +1,181 @@ + + + diff --git a/frontend/src/pages/agent/users/components/columns.ts b/frontend/src/pages/agent/users/components/columns.ts index beb4387..fbfec82 100644 --- a/frontend/src/pages/agent/users/components/columns.ts +++ b/frontend/src/pages/agent/users/components/columns.ts @@ -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[] { 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'), diff --git a/frontend/src/pages/agent/users/components/data-table.vue b/frontend/src/pages/agent/users/components/data-table.vue index 93e3c10..8b516c3 100644 --- a/frontend/src/pages/agent/users/components/data-table.vue +++ b/frontend/src/pages/agent/users/components/data-table.vue @@ -17,6 +17,7 @@ import DataTableToolbar from '@/pages/agent/users/components/data-table-toolbar. const props = defineProps, 'columns'> & { searchFilter?: string serverPagination?: DataTableProps['serverPagination'] + onEdit: (row: User) => void onToggleStatus: (row: User) => void }>() @@ -30,6 +31,7 @@ const { t } = useI18n() const columns = computed(() => [ SelectColumn as ColumnDef, ...getColumns({ + onEdit: props.onEdit, onToggleStatus: props.onToggleStatus, }, t), ]) diff --git a/frontend/src/pages/agent/users/index.vue b/frontend/src/pages/agent/users/index.vue index 5a70e47..62d96a8 100644 --- a/frontend/src/pages/agent/users/index.vue +++ b/frontend/src/pages/agent/users/index.vue @@ -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()" diff --git a/frontend/src/plugins/i18n/en.json b/frontend/src/plugins/i18n/en.json index a476ff7..344dc6c 100644 --- a/frontend/src/plugins/i18n/en.json +++ b/frontend/src/plugins/i18n/en.json @@ -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": { diff --git a/frontend/src/plugins/i18n/zh.json b/frontend/src/plugins/i18n/zh.json index 1f0d6cd..9f186b8 100644 --- a/frontend/src/plugins/i18n/zh.json +++ b/frontend/src/plugins/i18n/zh.json @@ -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": { diff --git a/frontend/src/router/routes.ts b/frontend/src/router/routes.ts index bf86da0..b14092c 100644 --- a/frontend/src/router/routes.ts +++ b/frontend/src/router/routes.ts @@ -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',