diff --git a/backend/internal/router/agent/agent.go b/backend/internal/router/agent/agent.go index 504d1f7..63ee69a 100644 --- a/backend/internal/router/agent/agent.go +++ b/backend/internal/router/agent/agent.go @@ -10,7 +10,9 @@ import ( "time" "verification-platform-backend/internal/database" "verification-platform-backend/internal/model" + "verification-platform-backend/internal/service" "verification-platform-backend/pkg/response" + "verification-platform-backend/pkg/utils" "github.com/gin-gonic/gin" ) @@ -22,6 +24,8 @@ func SetupAgentRoutes(r *gin.RouterGroup) { r.GET("/cards", handleGetCards) r.POST("/cards/generate", handleGenerateCards) r.GET("/users", handleGetUsers) + r.POST("/users", handleCreateUser) + r.PUT("/users/:id/status", handleUpdateUserStatus) r.GET("/finance", handleGetFinance) r.GET("/profile", handleGetProfile) r.PUT("/profile", handleUpdateProfile) @@ -945,3 +949,261 @@ func handleGetCloudVariableRecords(c *gin.Context) { "total_pages": (total + int64(pageSize) - 1) / int64(pageSize), }) } + +func handleCreateUser(c *gin.Context) { + userID := c.GetUint("user_id") + + var req struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + ApplicationID uint `json:"application_id"` + CardTypeID *uint `json:"card_type_id"` + CardQuantity int `json:"card_quantity"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, 400, "参数错误") + return + } + + if req.Username == "" { + response.Error(c, 400, "用户名不能为空") + return + } + if req.Password == "" { + response.Error(c, 400, "密码不能为空") + return + } + if req.ApplicationID == 0 { + response.Error(c, 400, "所属应用不能为空") + return + } + if req.CardQuantity < 1 { + req.CardQuantity = 1 + } + if req.CardQuantity > 100 { + response.Error(c, 400, "卡密数量不能超过100") + return + } + + var agentApp model.AgentApplication + if err := database.DB.Where("application_id = ? AND agent_id = ? AND is_received = ?", req.ApplicationID, userID, true).First(&agentApp).Error; err != nil { + response.Error(c, 403, "无权限在该应用下创建用户") + return + } + + var app model.Application + if err := database.DB.First(&app, req.ApplicationID).Error; err != nil { + response.Error(c, 404, "应用不存在") + return + } + + var existingUser model.AppUser + if err := database.DB.Where("username = ? AND application_id = ?", req.Username, app.ID).First(&existingUser).Error; err == nil { + response.Error(c, 400, "用户已存在") + return + } + + var cardType *model.CardType + 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 + } + cardType = &ct + } + + tx := database.DB.Begin() + + user := model.AppUser{ + Username: req.Username, + Email: req.Email, + Password: req.Password, + Avatar: "", + Status: "active", + ApplicationID: app.ID, + } + + if err := tx.Create(&user).Error; err != nil { + tx.Rollback() + response.Error(c, 500, "创建用户失败") + return + } + + var cards []model.Card + if cardType != nil { + now := time.Now() + for i := 0; i < req.CardQuantity; i++ { + cardKey := "CK" + utils.GenerateRandomString(16) + card := model.Card{ + ApplicationID: req.ApplicationID, + CardTypeID: cardType.ID, + CardKey: cardKey, + CreatorID: userID, + AgentID: &userID, + AppUserID: &user.ID, + Status: "used", + } + card.UsedAt = &now + + if err := tx.Create(&card).Error; err != nil { + tx.Rollback() + response.Error(c, 500, "生成卡密失败") + return + } + + user.IsTrialUser = false + + if cardType.Value == -1 { + if cardType.RechargeType == "subscription" { + permanentExpiry := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) + user.ExpiryAt = &permanentExpiry + user.Balance = -1 + } else { + user.Balance = -1 + user.ExpiryAt = nil + } + } else { + switch cardType.RechargeType { + case "subscription": + var baseTime time.Time + if user.ExpiryAt != nil && user.ExpiryAt.After(now) { + baseTime = *user.ExpiryAt + } else { + baseTime = now + } + var duration time.Duration + switch cardType.ValueUnit { + case "minute": + duration = time.Duration(cardType.Value) * time.Minute + case "hour": + duration = time.Duration(cardType.Value) * time.Hour + case "day": + duration = time.Duration(cardType.Value) * 24 * time.Hour + case "month": + duration = time.Duration(cardType.Value) * 30 * 24 * time.Hour + case "year": + duration = time.Duration(cardType.Value) * 365 * 24 * time.Hour + default: + duration = time.Duration(cardType.Value) * time.Second + } + newExpiry := baseTime.Add(duration) + user.ExpiryAt = &newExpiry + case "balance": + fallthrough + default: + user.Balance += cardType.Value + } + } + + rechargeRecord := model.RechargeRecord{ + UserID: user.ID, + OrderNo: generateAgentOrderNo("R"), + CardID: &card.ID, + CardCode: card.CardKey, + Amount: cardType.Price, + Status: "success", + PaymentType: "card", + Remark: "代理创建用户充值 - " + cardType.Name, + } + if err := tx.Create(&rechargeRecord).Error; err != nil { + tx.Rollback() + response.Error(c, 500, "创建充值记录失败") + return + } + + cards = append(cards, card) + } + + if err := tx.Save(&user).Error; err != nil { + tx.Rollback() + response.Error(c, 500, "充值失败") + return + } + } + + if err := tx.Commit().Error; err != nil { + response.Error(c, 500, "创建用户失败") + return + } + + logDesc := fmt.Sprintf("代理创建用户: %s (应用: %s)", user.Username, app.Name) + if cardType != nil { + logDesc += fmt.Sprintf(",充值卡密: %s x%d", cardType.Name, req.CardQuantity) + } + service.LogOperation(c, "create", "app_user", &user.ID, logDesc, nil) + + result := gin.H{ + "user": user, + } + if len(cards) > 0 { + result["cards"] = cards + } + + response.Success(c, result) +} + +func generateAgentOrderNo(prefix string) string { + return prefix + time.Now().Format("20060102150405") + utils.GenerateRandomString(6) +} + +func handleUpdateUserStatus(c *gin.Context) { + userID := c.GetUint("user_id") + id := c.Param("id") + + var req struct { + Status string `json:"status"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, 400, "参数错误") + return + } + + if req.Status != "active" && req.Status != "banned" { + response.Error(c, 400, "状态值无效,仅支持 active 或 banned") + return + } + + var user model.AppUser + if err := database.DB.First(&user, id).Error; err != nil { + response.Error(c, 404, "用户不存在") + 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 { + response.Error(c, 403, "无权限操作该用户") + return + } + + user.Status = req.Status + if err := database.DB.Save(&user).Error; err != nil { + response.Error(c, 500, "更新用户状态失败") + return + } + + logDesc := fmt.Sprintf("代理更新用户状态: %s -> %s", user.Username, req.Status) + service.LogOperation(c, "update", "app_user", &user.ID, logDesc, nil) + + response.Success(c, user) +} diff --git a/frontend/src/pages/agent/cloud-variables/[id]/records.vue b/frontend/src/pages/agent/cloud-variables/[id]/records.vue index 4d062e3..378c47f 100644 --- a/frontend/src/pages/agent/cloud-variables/[id]/records.vue +++ b/frontend/src/pages/agent/cloud-variables/[id]/records.vue @@ -368,7 +368,7 @@ onMounted(async () => { :placeholder="t('agent.cloudVariables.records.startDate')" class="w-[180px]" /> - {{ t('agent.finance.to') }} + {{ t('agent.cloudVariables.records.to') }} [] { +export function getColumns(actions: { + onToggleStatus: (row: User) => void +}, t: Composer['t']): ColumnDef[] { return [ { accessorKey: 'username', @@ -90,5 +100,40 @@ export function getColumns(t: Composer['t']): ColumnDef[] { } }, }, + { + id: 'actions', + header: () => h('span', { class: 'sr-only' }, t('common.actions')), + cell: ({ row }) => { + const user = row.original + const isBanned = user.status === 'banned' + + return h( + DropdownMenu, + {}, + { + default: () => [ + h(DropdownMenuTrigger, { asChild: true }, () => + h(Button, { variant: 'ghost', class: 'h-8 w-8 p-0' }, () => [ + h(MoreHorizontal, { class: 'h-4 w-4' }), + h('span', { class: 'sr-only' }, t('common.openMenu')), + ]), + ), + h( + DropdownMenuContent, + { align: 'end' }, + () => [ + h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(user) }, () => [ + h(Ban, { class: 'mr-2 h-4 w-4' }), + isBanned ? t('common.unban') : t('common.ban'), + ]), + ], + ), + ], + }, + ) + }, + enableSorting: false, + enableHiding: false, + }, ] } diff --git a/frontend/src/pages/agent/users/components/data-table.vue b/frontend/src/pages/agent/users/components/data-table.vue index 6a388d0..93e3c10 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'] + onToggleStatus: (row: User) => void }>() const emit = defineEmits<{ @@ -28,7 +29,9 @@ const { t } = useI18n() const columns = computed(() => [ SelectColumn as ColumnDef, - ...getColumns(t), + ...getColumns({ + onToggleStatus: props.onToggleStatus, + }, t), ]) const table = generateVueTable({ diff --git a/frontend/src/pages/agent/users/create.vue b/frontend/src/pages/agent/users/create.vue new file mode 100644 index 0000000..7586837 --- /dev/null +++ b/frontend/src/pages/agent/users/create.vue @@ -0,0 +1,319 @@ + + + diff --git a/frontend/src/pages/agent/users/index.vue b/frontend/src/pages/agent/users/index.vue index b75a844..5a70e47 100644 --- a/frontend/src/pages/agent/users/index.vue +++ b/frontend/src/pages/agent/users/index.vue @@ -1,15 +1,19 @@