From cfdc12cf94b52348fd64c02c792711f4572ccf14 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 13 Aug 2026 13:31:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E8=B4=A6=E5=8F=B7=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/user.go | 81 +++----- backend/internal/router/router.go | 12 +- backend/internal/service/merchant_test.go | 87 +++----- backend/internal/service/user.go | 195 ++++++++---------- frontend/src/api/index.ts | 19 +- frontend/src/pages/PlatformUsers.tsx | 229 ++++++++++------------ frontend/src/types/index.ts | 6 - 7 files changed, 250 insertions(+), 379 deletions(-) diff --git a/backend/internal/handler/user.go b/backend/internal/handler/user.go index f5c444a..f9174b6 100644 --- a/backend/internal/handler/user.go +++ b/backend/internal/handler/user.go @@ -4,7 +4,6 @@ import ( "strconv" "affiliate_dash/internal/middleware" - "affiliate_dash/internal/model" "affiliate_dash/internal/pkg/response" "affiliate_dash/internal/service" @@ -19,23 +18,10 @@ func NewUserHandler(svc *service.UserService) *UserHandler { return &UserHandler{svc: svc} } -func (h *UserHandler) List(c *gin.Context) { +func (h *UserHandler) ListPlatformAdmins(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) - q := service.UserListQuery{ - Page: page, - Size: size, - Keyword: c.Query("keyword"), - Role: c.Query("role"), - } - if middleware.GetRole(c) != model.RoleAdmin { - q.MerchantID = middleware.GetMerchantID(c) - } - if s := c.Query("status"); s != "" { - v, _ := strconv.Atoi(s) - q.Status = &v - } - list, total, err := h.svc.List(q) + list, total, err := h.svc.ListPlatformAdmins(page, size) if err != nil { response.ServerError(c, err.Error()) return @@ -43,31 +29,19 @@ func (h *UserHandler) List(c *gin.Context) { response.Page(c, list, total, page, size) } -func (h *UserHandler) ListMerchantMembers(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - size, _ := strconv.Atoi(c.DefaultQuery("size", "20")) - list, total, err := h.svc.ListMerchantMembers(page, size) - if err != nil { - response.ServerError(c, err.Error()) - return - } - response.Page(c, list, total, page, size) -} - -type createUserReq struct { +type createPlatformAdminReq struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required,min=6"` Nickname string `json:"nickname"` - Role string `json:"role"` } -func (h *UserHandler) Create(c *gin.Context) { - var req createUserReq +func (h *UserHandler) CreatePlatformAdmin(c *gin.Context) { + var req createPlatformAdminReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } - user, err := h.svc.Create(req.Username, req.Password, req.Nickname, req.Role, middleware.GetMerchantID(c)) + user, err := h.svc.CreatePlatformAdmin(req.Username, req.Password, req.Nickname) if err != nil { response.BadRequest(c, err.Error()) return @@ -75,45 +49,36 @@ func (h *UserHandler) Create(c *gin.Context) { response.OK(c, user) } -type userStatusReq struct { - Status int `json:"status"` +type updatePlatformAdminReq struct { + Username string `json:"username" binding:"required"` + Nickname string `json:"nickname"` + Password string `json:"password" binding:"omitempty,min=6"` + Status int `json:"status"` } -func (h *UserHandler) UpdateStatus(c *gin.Context) { +func (h *UserHandler) UpdatePlatformAdmin(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - var req userStatusReq + var req updatePlatformAdminReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } - if err := h.svc.UpdateStatus(uint(id), req.Status); err != nil { + user, err := h.svc.UpdatePlatformAdmin(uint(id), service.UpdatePlatformAdminInput{ + Username: req.Username, + Nickname: req.Nickname, + Password: req.Password, + Status: req.Status, + }, middleware.GetUserID(c)) + if err != nil { response.BadRequest(c, err.Error()) return } - response.OK(c, nil) + response.OK(c, user) } -type userRoleReq struct { - Role string `json:"role" binding:"required"` -} - -func (h *UserHandler) UpdateRole(c *gin.Context) { +func (h *UserHandler) DeletePlatformAdmin(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - var req userRoleReq - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误:角色必填") - return - } - if err := h.svc.UpdateRole(uint(id), req.Role, middleware.GetUserID(c)); err != nil { - response.BadRequest(c, err.Error()) - return - } - response.OK(c, nil) -} - -func (h *UserHandler) Delete(c *gin.Context) { - id, _ := strconv.ParseUint(c.Param("id"), 10, 64) - if err := h.svc.Delete(uint(id), middleware.GetUserID(c)); err != nil { + if err := h.svc.DeletePlatformAdmin(uint(id), middleware.GetUserID(c)); err != nil { response.BadRequest(c, err.Error()) return } diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 508eab0..fbae9e8 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -188,16 +188,14 @@ func Setup(h *Handlers) *gin.Engine { merchant.PATCH("/roles/:id", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.UpdateRole) } - // 用户 / 发货记录 / 平台商户(仅管理员) + // 平台管理员 / 发货记录 / 平台商户(仅管理员) admin := auth.Group("") admin.Use(middleware.RequireRole(model.RoleAdmin)) { - admin.GET("/users", h.User.List) - admin.GET("/platform/members", h.User.ListMerchantMembers) - admin.POST("/users", h.User.Create) - admin.PATCH("/users/:id/status", h.User.UpdateStatus) - admin.PATCH("/users/:id/role", h.User.UpdateRole) - admin.DELETE("/users/:id", h.User.Delete) + admin.GET("/platform/admins", h.User.ListPlatformAdmins) + admin.POST("/platform/admins", h.User.CreatePlatformAdmin) + admin.PATCH("/platform/admins/:id", h.User.UpdatePlatformAdmin) + admin.DELETE("/platform/admins/:id", h.User.DeletePlatformAdmin) admin.GET("/platform/merchants", h.Merchant.ListPlatformMerchants) admin.POST("/platform/merchants", h.Merchant.CreateMerchant) admin.PATCH("/platform/merchants/:id", h.Merchant.UpdateMerchantSettings) diff --git a/backend/internal/service/merchant_test.go b/backend/internal/service/merchant_test.go index 278ba19..c235dc8 100644 --- a/backend/internal/service/merchant_test.go +++ b/backend/internal/service/merchant_test.go @@ -206,90 +206,63 @@ func TestAuthServiceChangePasswordVerifiesCurrentPassword(t *testing.T) { func TestUserServiceCannotDeleteOrDisableLastAdmin(t *testing.T) { db := newServiceTestDB(t) svc := NewUserService(db, nil) - admin, err := svc.Create("only-admin", "password123", "唯一管理员", model.RoleAdmin, 0) + admin, err := svc.CreatePlatformAdmin("only-admin", "password123", "唯一管理员") if err != nil { t.Fatalf("create admin: %v", err) } - if err := svc.UpdateStatus(admin.ID, 0); err == nil { + if _, err := svc.UpdatePlatformAdmin(admin.ID, UpdatePlatformAdminInput{ + Username: admin.Username, Nickname: admin.Nickname, Status: 0, + }, 0); err == nil { t.Fatal("should not disable the last enabled admin") } - other, err := svc.Create("other-admin", "password123", "另一管理员", model.RoleAdmin, 0) + other, err := svc.CreatePlatformAdmin("other-admin", "password123", "另一管理员") if err != nil { t.Fatalf("create second admin: %v", err) } - if err := svc.Delete(other.ID, admin.ID); err != nil { + if err := svc.DeletePlatformAdmin(other.ID, admin.ID); err != nil { t.Fatalf("delete non-current admin: %v", err) } - if err := svc.Delete(admin.ID, other.ID); err == nil { + if err := svc.DeletePlatformAdmin(admin.ID, other.ID); err == nil { t.Fatal("should not delete the last enabled admin") } } -func TestUserServicePromotesMerchantAccountToPlatformAdmin(t *testing.T) { +func TestPlatformAdminCRUDDoesNotCreateMerchantMembership(t *testing.T) { db := newServiceTestDB(t) - var merchant model.Merchant - if err := db.Where("code = ?", model.MerchantCodeSelfOperated).First(&merchant).Error; err != nil { - t.Fatalf("load self-operated merchant: %v", err) - } svc := NewUserService(db, NewTenantService(db)) - actor, err := svc.Create("existing-admin", "password123", "现有管理员", model.RoleAdmin, 0) + actor, err := svc.CreatePlatformAdmin("existing-admin", "password123", "现有管理员") if err != nil { t.Fatalf("create actor: %v", err) } - member, err := svc.Create("skin_nh_admin", "password123", "新管理员", model.RoleMerchant, 0) + admin, err := svc.CreatePlatformAdmin("platform-admin", "password123", "平台管理员") if err != nil { - t.Fatalf("create merchant user: %v", err) + t.Fatalf("create platform admin: %v", err) } - if err := svc.UpdateRole(member.ID, model.RoleAdmin, actor.ID); err != nil { - t.Fatalf("promote account: %v", err) + var membershipCount int64 + if err := db.Model(&model.MerchantMember{}).Where("user_id = ?", admin.ID).Count(&membershipCount).Error; err != nil { + t.Fatalf("count merchant memberships: %v", err) } - var promoted model.User - if err := db.First(&promoted, member.ID).Error; err != nil || promoted.Role != model.RoleAdmin { - t.Fatalf("account should be admin, user=%+v err=%v", promoted, err) + if membershipCount != 0 { + t.Fatalf("platform admin must not have merchant memberships, count=%d", membershipCount) } - var membership model.MerchantMember - if err := db.Where("merchant_id = ? AND user_id = ?", merchant.ID, member.ID).First(&membership).Error; err != nil { - t.Fatalf("promoted account should join self-operated merchant: %v", err) - } - if membership.Role != model.MemberRoleOwner || !membership.IsDefault { - t.Fatalf("unexpected promoted membership: %+v", membership) - } -} - -func TestListMerchantMembersGroupsEmployeesByMerchant(t *testing.T) { - db := newServiceTestDB(t) - merchantA := model.Merchant{Code: "grouped-members-a", Name: "分组商户 A", Status: model.MerchantStatusActive} - merchantB := model.Merchant{Code: "grouped-members-b", Name: "分组商户 B", Status: model.MerchantStatusActive} - employeeA := model.User{Username: "grouped-employee-a", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1} - employeeB := model.User{Username: "grouped-employee-b", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1} - admin := model.User{Username: "grouped-admin", PasswordHash: "hash", Role: model.RoleAdmin, Status: 1} - for _, entity := range []interface{}{&merchantA, &merchantB, &employeeA, &employeeB, &admin} { - if err := db.Create(entity).Error; err != nil { - t.Fatalf("create fixture: %v", err) - } - } - for _, member := range []model.MerchantMember{ - {MerchantID: merchantA.ID, UserID: employeeA.ID, Role: model.MemberRoleOperator, Status: 1}, - {MerchantID: merchantA.ID, UserID: employeeB.ID, Role: model.MemberRoleFinance, Status: 1}, - {MerchantID: merchantB.ID, UserID: admin.ID, Role: model.MemberRoleOwner, Status: 1}, - } { - if err := db.Create(&member).Error; err != nil { - t.Fatalf("create membership: %v", err) - } - } - - groups, total, err := NewUserService(db, nil).ListMerchantMembers(1, 20) + updated, err := svc.UpdatePlatformAdmin(admin.ID, UpdatePlatformAdminInput{ + Username: "platform-admin-updated", Nickname: "更新后管理员", Password: "new-password", Status: 1, + }, actor.ID) if err != nil { - t.Fatalf("list grouped members: %v", err) + t.Fatalf("update platform admin: %v", err) } - if total != 1 || len(groups) != 1 { - t.Fatalf("expected one merchant group, total=%d groups=%+v", total, groups) + if updated.Username != "platform-admin-updated" || updated.Nickname != "更新后管理员" { + t.Fatalf("unexpected updated admin: %+v", updated) } - if groups[0].MerchantID != merchantA.ID || groups[0].Merchant == nil || groups[0].Merchant.Name != merchantA.Name { - t.Fatalf("unexpected merchant group: %+v", groups[0]) + var saved model.User + if err := db.First(&saved, admin.ID).Error; err != nil { + t.Fatalf("load updated admin: %v", err) } - if len(groups[0].Members) != 2 { - t.Fatalf("expected two employees in the merchant group, got %+v", groups[0].Members) + if err := bcrypt.CompareHashAndPassword([]byte(saved.PasswordHash), []byte("new-password")); err != nil { + t.Fatalf("updated password should match: %v", err) + } + if err := svc.DeletePlatformAdmin(admin.ID, actor.ID); err != nil { + t.Fatalf("delete platform admin: %v", err) } } diff --git a/backend/internal/service/user.go b/backend/internal/service/user.go index e94e349..6d1c039 100644 --- a/backend/internal/service/user.go +++ b/backend/internal/service/user.go @@ -7,7 +7,6 @@ import ( "golang.org/x/crypto/bcrypt" "gorm.io/gorm" - "gorm.io/gorm/clause" ) type UserService struct { @@ -28,15 +27,6 @@ type UserListQuery struct { Status *int } -// MerchantMemberGroup keeps employee assignments grouped by merchant for -// platform account management. Pagination is intentionally based on merchants -// so one merchant's employees are never split across pages. -type MerchantMemberGroup struct { - MerchantID uint `json:"merchant_id"` - Merchant *model.Merchant `json:"merchant,omitempty"` - Members []model.MerchantMember `json:"members"` -} - func (s *UserService) List(q UserListQuery) ([]model.User, int64, error) { if q.Page < 1 { q.Page = 1 @@ -68,53 +58,6 @@ func (s *UserService) List(q UserListQuery) ([]model.User, int64, error) { return list, total, err } -// ListMerchantMembers returns employee assignments grouped by merchant for platform operations. -func (s *UserService) ListMerchantMembers(page, size int) ([]MerchantMemberGroup, int64, error) { - page, size = normalizePage(page, size) - tx := s.db.Model(&model.MerchantMember{}). - Joins("JOIN users ON users.id = merchant_members.user_id AND users.deleted_at IS NULL"). - Where("users.role = ?", model.RoleMerchant) - var total int64 - if err := tx.Distinct("merchant_members.merchant_id").Count(&total).Error; err != nil { - return nil, 0, err - } - var merchantIDs []uint - if err := tx.Distinct("merchant_members.merchant_id"). - Order("merchant_members.merchant_id ASC"). - Offset((page-1)*size).Limit(size).Pluck("merchant_members.merchant_id", &merchantIDs).Error; err != nil { - return nil, 0, err - } - if len(merchantIDs) == 0 { - return []MerchantMemberGroup{}, total, nil - } - var members []model.MerchantMember - err := s.db.Model(&model.MerchantMember{}). - Joins("JOIN users ON users.id = merchant_members.user_id AND users.deleted_at IS NULL"). - Where("users.role = ? AND merchant_members.merchant_id IN ?", model.RoleMerchant, merchantIDs). - Preload("Merchant").Preload("User"). - Order("merchant_members.merchant_id ASC, merchant_members.id ASC"). - Find(&members).Error - if err != nil { - return nil, 0, err - } - - groupsByMerchant := make(map[uint]*MerchantMemberGroup, len(merchantIDs)) - groups := make([]MerchantMemberGroup, 0, len(merchantIDs)) - for _, merchantID := range merchantIDs { - group := MerchantMemberGroup{MerchantID: merchantID, Members: []model.MerchantMember{}} - groups = append(groups, group) - groupsByMerchant[merchantID] = &groups[len(groups)-1] - } - for _, member := range members { - group := groupsByMerchant[member.MerchantID] - if group.Merchant == nil { - group.Merchant = member.Merchant - } - group.Members = append(group.Members, member) - } - return groups, total, nil -} - func (s *UserService) Create(username, password, nickname, role string, merchantID uint) (*model.User, error) { var count int64 s.db.Model(&model.User{}).Where("username = ?", username).Count(&count) @@ -169,6 +112,82 @@ func (s *UserService) Create(username, password, nickname, role string, merchant return user, nil } +type UpdatePlatformAdminInput struct { + Username string + Nickname string + Password string + Status int +} + +func (s *UserService) ListPlatformAdmins(page, size int) ([]model.User, int64, error) { + return s.List(UserListQuery{Page: page, Size: size, Role: model.RoleAdmin}) +} + +// CreatePlatformAdmin creates an account with platform-only privileges. It +// deliberately does not assign the account to any merchant. +func (s *UserService) CreatePlatformAdmin(username, password, nickname string) (*model.User, error) { + return s.Create(username, password, nickname, model.RoleAdmin, 0) +} + +func (s *UserService) UpdatePlatformAdmin(id uint, in UpdatePlatformAdminInput, actorUserID uint) (*model.User, error) { + if id == 0 { + return nil, errors.New("用户不存在") + } + if id == actorUserID { + return nil, errors.New("当前账号请通过右上角账户菜单修改密码") + } + if in.Status != 0 && in.Status != 1 { + return nil, errors.New("用户状态无效") + } + returnUser := &model.User{} + err := s.db.Transaction(func(tx *gorm.DB) error { + var user model.User + if err := tx.First(&user, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("用户不存在") + } + return err + } + if user.Role != model.RoleAdmin { + return errors.New("只能管理平台管理员账号") + } + if user.Status == 1 && in.Status == 0 { + var admins int64 + if err := tx.Model(&model.User{}).Where("role = ? AND status = ?", model.RoleAdmin, 1).Count(&admins).Error; err != nil { + return err + } + if admins <= 1 { + return errors.New("至少保留一个启用的平台管理员") + } + } + if in.Username != user.Username { + var count int64 + if err := tx.Model(&model.User{}).Where("username = ? AND id <> ?", in.Username, user.ID).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return errors.New("用户名已存在") + } + } + updates := map[string]interface{}{"username": in.Username, "nickname": in.Nickname, "status": in.Status} + if in.Password != "" { + hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + updates["password_hash"] = string(hash) + } + if err := tx.Model(&user).Updates(updates).Error; err != nil { + return err + } + return tx.First(returnUser, user.ID).Error + }) + if err != nil { + return nil, err + } + return returnUser, nil +} + func (s *UserService) UpdateStatus(id uint, status int) error { if status != 0 && status != 1 { return errors.New("用户状态无效") @@ -194,61 +213,9 @@ func (s *UserService) UpdateStatus(id uint, status int) error { }) } -// UpdateRole changes the platform-level account role. Promoted administrators are -// also made owners of the self-operated merchant for a consistent merchant context. -func (s *UserService) UpdateRole(id uint, role string, actorUserID uint) error { - if id == 0 { - return errors.New("用户不存在") - } - if id == actorUserID { - return errors.New("不能修改当前登录账号的系统角色") - } - if role != model.RoleAdmin && role != model.RoleMerchant { - return errors.New("无效的账号角色") - } - return s.db.Transaction(func(tx *gorm.DB) error { - var user model.User - if err := tx.First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("用户不存在") - } - return err - } - if user.Role == role { - return nil - } - if user.Role == model.RoleAdmin && user.Status == 1 && role != model.RoleAdmin { - var admins int64 - if err := tx.Model(&model.User{}).Where("role = ? AND status = ?", model.RoleAdmin, 1).Count(&admins).Error; err != nil { - return err - } - if admins <= 1 { - return errors.New("至少保留一个启用的平台管理员") - } - } - if err := tx.Model(&user).Update("role", role).Error; err != nil { - return err - } - if role != model.RoleAdmin { - return nil - } - var merchant model.Merchant - if err := tx.Where("code = ?", model.MerchantCodeSelfOperated).First(&merchant).Error; err != nil { - return err - } - member := model.MerchantMember{MerchantID: merchant.ID, UserID: user.ID, Role: model.MemberRoleOwner, Status: user.Status, IsDefault: true} - return tx.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "merchant_id"}, {Name: "user_id"}}, - DoUpdates: clause.Assignments(map[string]interface{}{ - "role": model.MemberRoleOwner, "status": user.Status, "is_default": true, - }), - }).Create(&member).Error - }) -} - -// Delete removes a platform account and its merchant memberships. It protects the current +// DeletePlatformAdmin removes a platform-only account. It protects the current // account and the final enabled platform administrator so the platform cannot be locked out. -func (s *UserService) Delete(id, actorUserID uint) error { +func (s *UserService) DeletePlatformAdmin(id, actorUserID uint) error { if id == 0 { return errors.New("用户不存在") } @@ -263,6 +230,9 @@ func (s *UserService) Delete(id, actorUserID uint) error { } return err } + if user.Role != model.RoleAdmin { + return errors.New("只能删除平台管理员账号") + } if user.Role == model.RoleAdmin && user.Status == 1 { var admins int64 if err := tx.Model(&model.User{}).Where("role = ? AND status = ?", model.RoleAdmin, 1).Count(&admins).Error; err != nil { @@ -272,9 +242,6 @@ func (s *UserService) Delete(id, actorUserID uint) error { return errors.New("至少保留一个启用的平台管理员") } } - if err := tx.Where("user_id = ?", id).Delete(&model.MerchantMember{}).Error; err != nil { - return err - } return tx.Delete(&user).Error }) } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index cfefef5..d7080cd 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -17,8 +17,7 @@ import type { LoginResult, LowBalanceAlertConfig, Merchant, - MerchantMember, - MerchantMemberGroup, + MerchantMember, MerchantRole, MerchantProduct, PageResult, @@ -49,15 +48,13 @@ export const dashboardApi = { export const userApi = { list: (params?: Record) => - request.get('/users', { params }).then((r) => r.data.data as PageResult), - create: (data: { username: string; password: string; nickname?: string; role?: string }) => - request.post('/users', data).then((r) => r.data.data as User), - updateStatus: (id: number, status: number) => - request.patch(`/users/${id}/status`, { status }).then((r) => r.data.data), - updateRole: (id: number, role: User['role']) => - request.patch(`/users/${id}/role`, { role }).then((r) => r.data.data), + request.get('/platform/admins', { params }).then((r) => r.data.data as PageResult), + create: (data: { username: string; password: string; nickname?: string }) => + request.post('/platform/admins', data).then((r) => r.data.data as User), + update: (id: number, data: { username: string; nickname?: string; password?: string; status: number }) => + request.patch(`/platform/admins/${id}`, data).then((r) => r.data.data as User), remove: (id: number) => - request.delete(`/users/${id}`).then((r) => r.data.data), + request.delete(`/platform/admins/${id}`).then((r) => r.data.data), } export const merchantApi = { @@ -179,8 +176,6 @@ export const deliveryApi = { } export const platformApi = { - members: (params?: Record) => - request.get('/platform/members', { params }).then((r) => r.data.data as PageResult), merchants: (params?: Record) => request.get('/platform/merchants', { params }).then((r) => r.data.data as PageResult), createMerchant: (data: { diff --git a/frontend/src/pages/PlatformUsers.tsx b/frontend/src/pages/PlatformUsers.tsx index 528f157..ea61f0a 100644 --- a/frontend/src/pages/PlatformUsers.tsx +++ b/frontend/src/pages/PlatformUsers.tsx @@ -1,27 +1,33 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { Button, Popconfirm, Space, Table, Tabs, Tag, Typography, message } from 'antd' -import { DeleteOutlined, ReloadOutlined, SafetyCertificateOutlined } from '@ant-design/icons' +import { useCallback, useEffect, useState } from 'react' +import { Button, Form, Input, Modal, Popconfirm, Space, Switch, Table, Tag, Typography, message } from 'antd' +import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' -import { platformApi, userApi } from '../api' +import { userApi } from '../api' import { PageHeader } from '../components/PageHeader' -import type { MerchantMember, MerchantMemberGroup, PageResult, User } from '../types' +import type { PageResult, User } from '../types' import { formatDateTime } from '../utils/time' import { useAuth } from '../store/auth' -import { roleText } from './merchantCenterUtils' -import { useNavigate } from 'react-router-dom' + +type AdminFormValues = { + username: string + nickname?: string + password?: string + status: boolean +} export default function PlatformUsers() { const { user: currentUser } = useAuth() - const navigate = useNavigate() - const [tab, setTab] = useState('admins') const [admins, setAdmins] = useState>({ list: [], total: 0, page: 1, size: 20 }) - const [members, setMembers] = useState>({ list: [], total: 0, page: 1, size: 20 }) const [loading, setLoading] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [editingAdmin, setEditingAdmin] = useState(null) + const [modalOpen, setModalOpen] = useState(false) + const [form] = Form.useForm() const loadAdmins = useCallback(async (page = admins.page, size = admins.size) => { setLoading(true) try { - setAdmins(await userApi.list({ page, size, role: 'admin' })) + setAdmins(await userApi.list({ page, size })) } catch (e) { message.error(e instanceof Error ? e.message : '加载失败') } finally { @@ -29,141 +35,114 @@ export default function PlatformUsers() { } }, [admins.page, admins.size]) - const loadMembers = useCallback(async (page = members.page, size = members.size) => { - setLoading(true) - try { - setMembers(await platformApi.members({ page, size })) - } catch (e) { - message.error(e instanceof Error ? e.message : '加载失败') - } finally { - setLoading(false) - } - }, [members.page, members.size]) - useEffect(() => { - if (tab === 'admins') void loadAdmins() - else void loadMembers() - }, [loadAdmins, loadMembers, tab]) + void loadAdmins() + }, [loadAdmins]) + + const openCreate = () => { + setEditingAdmin(null) + form.setFieldsValue({ username: '', nickname: '', password: '', status: true }) + setModalOpen(true) + } + + const openEdit = (record: User) => { + setEditingAdmin(record) + form.setFieldsValue({ username: record.username, nickname: record.nickname, password: '', status: record.status === 1 }) + setModalOpen(true) + } + + const submit = async () => { + const values = await form.validateFields() + setSubmitting(true) + try { + if (editingAdmin) { + await userApi.update(editingAdmin.id, { + username: values.username, + nickname: values.nickname || '', + password: values.password || undefined, + status: values.status ? 1 : 0, + }) + message.success('平台管理员已更新') + } else { + await userApi.create({ username: values.username, password: values.password || '', nickname: values.nickname || '' }) + message.success('平台管理员已创建') + } + setModalOpen(false) + form.resetFields() + void loadAdmins(editingAdmin ? admins.page : 1) + } catch (e) { + message.error(e instanceof Error ? e.message : editingAdmin ? '更新失败' : '创建失败') + } finally { + setSubmitting(false) + } + } const remove = async (record: User) => { try { await userApi.remove(record.id) - message.success('账号已删除') - loadAdmins() + message.success('平台管理员已删除') + void loadAdmins() } catch (e) { message.error(e instanceof Error ? e.message : '删除失败') } } - const promote = async (record: MerchantMember) => { - try { - await userApi.updateRole(record.user_id, 'admin') - message.success('已设为平台超级管理员;请使用该账号重新登录') - loadMembers() - } catch (e) { - message.error(e instanceof Error ? e.message : '设置失败') - } - } - - const adminColumns: ColumnsType = [ - { title: '用户名', dataIndex: 'username', width: 240, render: (value) => {value} }, - { title: '昵称', dataIndex: 'nickname', width: 180, render: (value) => value || '-' }, - { title: '状态', dataIndex: 'status', width: 100, render: (value) => value === 1 ? 启用 : 停用 }, - { title: '创建时间', dataIndex: 'created_at', width: 180, render: formatDateTime }, + const columns: ColumnsType = [ + { title: '用户名', dataIndex: 'username', width: 260, render: (value) => {value} }, + { title: '昵称', dataIndex: 'nickname', width: 220, render: (value) => value || '-' }, + { title: '状态', dataIndex: 'status', width: 120, render: (value) => value === 1 ? 启用 : 停用 }, + { title: '创建时间', dataIndex: 'created_at', width: 190, render: formatDateTime }, { title: '操作', - width: 130, + width: 190, render: (_, record) => record.id === currentUser?.id ? 当前账号 : ( - remove(record)} okButtonProps={{ danger: true }}> - - - ), - }, - ] - - const memberColumns: ColumnsType = [ - { title: '员工账号', dataIndex: ['user', 'username'], width: 220, render: (_, record) => {record.user?.username || `#${record.user_id}`} }, - { title: '昵称', dataIndex: ['user', 'nickname'], width: 180, render: (_, record) => record.user?.nickname || '-' }, - { title: '商户角色', dataIndex: 'role', width: 140, render: (role) => {roleText(role)} }, - { title: '成员状态', dataIndex: 'status', width: 110, render: (status) => status === 1 ? 启用 : 停用 }, - { - title: '操作', - width: 230, - render: (_, record) => ( - {record.user?.role !== 'admin' && record.user_id !== currentUser?.id && ( - promote(record)}> - - - )} + + remove(record)} okButtonProps={{ danger: true }}> + + ), }, ] - const merchantColumns: ColumnsType = [ - { - title: '商户', - width: 310, - render: (_, record) => {record.merchant?.name || `商户#${record.merchant_id}`}{record.merchant?.code || '-'}, - }, - { - title: '员工', - width: 120, - render: (_, record) => {record.members.length} 人, - }, - { - title: '角色分布', - render: (_, record) => { - const roles = [...new Set(record.members.map((member) => member.role))] - return {roles.map((role) => {roleText(role)})} - }, - }, - { - title: '状态', - width: 150, - render: (_, record) => { - const enabled = record.members.filter((member) => member.status === 1).length - return <>启用 {enabled}{enabled < record.members.length && 停用 {record.members.length - enabled}} - }, - }, - { - title: '操作', - width: 150, - render: (_, record) => , - }, - ] - - const items = useMemo(() => [ - { - key: 'admins', - label: `平台管理员 (${admins.total})`, - children: loadAdmins(page, size) }} />, - }, - { - key: 'members', - label: `商户员工 (${members.total})`, - children:
, - rowExpandable: (record) => record.members.length > 0, - }} - pagination={{ current: members.page, pageSize: members.size, total: members.total, showSizeChanger: true, onChange: (page, size) => loadMembers(page, size) }} - />, - }, - ], [adminColumns, admins, loadAdmins, loadMembers, loading, memberColumns, members]) - return (
- } loading={loading} onClick={() => tab === 'admins' ? loadAdmins() : loadMembers()}>刷新} /> - + } + /> +
void loadAdmins(page, size) }} + /> + setModalOpen(false)} + onOk={() => void submit()} + confirmLoading={submitting} + okText="保存" + destroyOnHidden + > +
+ + + + + + + + + + {editingAdmin && } + +
) } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 17a0402..6b1aea7 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -33,12 +33,6 @@ export interface MerchantMember { created_at: string } -export interface MerchantMemberGroup { - merchant_id: number - merchant?: Merchant - members: MerchantMember[] -} - export interface MerchantRole { id: number merchant_id: number