diff --git a/backend/internal/handler/user.go b/backend/internal/handler/user.go index 6a340a5..b6c6e3d 100644 --- a/backend/internal/handler/user.go +++ b/backend/internal/handler/user.go @@ -44,7 +44,6 @@ 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) CreatePlatformAdmin(c *gin.Context) { @@ -53,7 +52,7 @@ func (h *UserHandler) CreatePlatformAdmin(c *gin.Context) { response.BadRequest(c, "参数错误") return } - user, err := h.svc.CreatePlatformUser(req.Username, req.Password, req.Nickname, req.Role) + user, err := h.svc.CreatePlatformUser(req.Username, req.Password, req.Nickname) if err != nil { response.BadRequest(c, err.Error()) return @@ -66,7 +65,6 @@ type updatePlatformAdminReq struct { Nickname string `json:"nickname"` Password string `json:"password" binding:"omitempty,min=6"` Status int `json:"status"` - Role string `json:"role"` } func (h *UserHandler) UpdatePlatformAdmin(c *gin.Context) { @@ -81,7 +79,6 @@ func (h *UserHandler) UpdatePlatformAdmin(c *gin.Context) { Nickname: req.Nickname, Password: req.Password, Status: req.Status, - Role: req.Role, }, middleware.GetUserID(c)) if err != nil { response.BadRequest(c, err.Error()) diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index 4d41cde..faa73a4 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -16,6 +16,7 @@ const ( CtxUsername = "username" CtxRole = "role" CtxMerchantRole = "merchant_role" + CtxMerchantCode = "merchant_code" CtxMerchantPermissions = "merchant_permissions" ) @@ -88,27 +89,13 @@ func Tenant(tenantSvc *service.TenantService) gin.HandlerFunc { return } c.Set(CtxMerchantID, merchant.ID) + c.Set(CtxMerchantCode, merchant.Code) c.Set(CtxMerchantRole, model.MemberRoleOwner) permissions, _ := tenantSvc.MerchantRolePermissions(merchant.ID, model.MemberRoleOwner) c.Set(CtxMerchantPermissions, permissions) c.Next() return } - if GetRole(c) == model.RolePlatformSupport { - // 平台客服只借用自营商户的订单上下文:可完整操作发货订单, - // 但不写入 merchant_members,也不会获得商品、钱包、成员等权限。 - merchant, err := tenantSvc.ResolveMerchantForAdmin(model.MerchantCodeSelfOperated) - if err != nil { - response.Forbidden(c, err.Error()) - c.Abort() - return - } - c.Set(CtxMerchantID, merchant.ID) - c.Set(CtxMerchantRole, model.MemberRoleSupport) - c.Set(CtxMerchantPermissions, map[string]struct{}{model.PermissionOrdersManage: {}}) - c.Next() - return - } member, err := tenantSvc.ResolveMember(GetUserID(c), merchantRef) if err != nil { response.Forbidden(c, err.Error()) @@ -116,6 +103,9 @@ func Tenant(tenantSvc *service.TenantService) gin.HandlerFunc { return } c.Set(CtxMerchantID, member.MerchantID) + if member.Merchant != nil { + c.Set(CtxMerchantCode, member.Merchant.Code) + } c.Set(CtxMerchantRole, member.Role) permissions, err := tenantSvc.MerchantRolePermissions(member.MerchantID, member.Role) if err != nil { @@ -172,3 +162,25 @@ func GetMerchantRole(c *gin.Context) string { role, _ := value.(string) return role } + +func GetMerchantCode(c *gin.Context) string { + value, _ := c.Get(CtxMerchantCode) + code, _ := value.(string) + return code +} + +// RequireSelfOrderSiteOrAdmin 允许平台管理员,或「自用自动发货」商户的成员访问客服查询。 +func RequireSelfOrderSiteOrAdmin() gin.HandlerFunc { + return func(c *gin.Context) { + if GetRole(c) == model.RoleAdmin { + c.Next() + return + } + if GetMerchantCode(c) == model.MerchantCodeSelfOrderSite { + c.Next() + return + } + response.Forbidden(c, "权限不足") + c.Abort() + } +} diff --git a/backend/internal/model/merchant.go b/backend/internal/model/merchant.go index a4e66c8..3ce6bbd 100644 --- a/backend/internal/model/merchant.go +++ b/backend/internal/model/merchant.go @@ -12,6 +12,8 @@ const ( // MerchantCodeSelfOperated 是平台自营商户编码,作为默认商品目录的模板来源。 MerchantCodeSelfOperated = "self-operated" + // MerchantCodeSelfOrderSite 是「自用自动发货」商户编码,客服查询仅对该商户开放。 + MerchantCodeSelfOrderSite = "self-order-site" MerchantFeatureProducts = "products" MerchantFeatureOrders = "orders" diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index 74fac59..7e3d40f 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -8,9 +8,8 @@ import ( // 用户角色 const ( - RoleAdmin = "admin" // 平台管理员 - RoleMerchant = "merchant" // 商户账号 - RolePlatformSupport = "platform_support" // 平台客服 + RoleAdmin = "admin" // 平台管理员 + RoleMerchant = "merchant" // 商户账号 ) // User 系统用户 diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 665c5e4..f5d75e0 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -312,12 +312,14 @@ func Setup(h *Handlers) *gin.Engine { } } - // 平台客服查询:仅平台管理员与平台客服可用,跨商户只读查询绑定中 Token。 - // package-link 是只读的上游定位链接,允许平台客服复用。 + // 客服查询:平台管理员与「自用自动发货」商户可用,跨商户只读查询绑定中 Token。 + // package-link 是只读的上游定位链接,允许客服查询复用。 if h.Rock != nil { supportQuery := api.Group("") supportQuery.Use(middleware.Auth(h.JWT)) - supportQuery.Use(middleware.RequireRole(model.RoleAdmin, model.RolePlatformSupport)) + supportQuery.Use(middleware.Tenant(h.Tenant)) + supportQuery.Use(middleware.RequireSelfOrderSiteOrAdmin()) + supportQuery.Use(middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage)) { supportQuery.GET("/platform/rock/support/bound-tokens", h.Rock.ListSupportBoundTokens) supportQuery.GET("/platform/rock/support/tokens/:id/status-logs", h.Rock.ListSupportTokenStatusLogs) diff --git a/backend/internal/router/router_test.go b/backend/internal/router/router_test.go index d8599eb..c4aac75 100644 --- a/backend/internal/router/router_test.go +++ b/backend/internal/router/router_test.go @@ -16,10 +16,18 @@ import ( "affiliate_dash/internal/testdb" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) // newTestRouter 构建一个接好全部依赖的测试路由。 func newTestRouter(t *testing.T) (*gin.Engine, *jwt.Manager) { + t.Helper() + engine, jm, _ := newTestRouterWithDB(t) + return engine, jm +} + +// newTestRouterWithDB 在测试路由之外一并返回数据库,便于构造多商户权限场景。 +func newTestRouterWithDB(t *testing.T) (*gin.Engine, *jwt.Manager, *gorm.DB) { t.Helper() gin.SetMode(gin.TestMode) db := testdb.New(t) @@ -55,7 +63,7 @@ func newTestRouter(t *testing.T) (*gin.Engine, *jwt.Manager) { OpenAPISecret: "source-secret", OpenSignSkew: 300, }) - return engine, jm + return engine, jm, db } func TestSetupDoesNotPanic(t *testing.T) { @@ -243,31 +251,60 @@ func TestPlatformMerchantMembersRoutes(t *testing.T) { } } -// TestPlatformSupportContext 验证平台客服只获得自营商户订单上下文与客服查询权限。 -func TestPlatformSupportContext(t *testing.T) { - engine, jm := newTestRouter(t) - token, err := jm.Generate(99, "platform-support", model.RolePlatformSupport) - if err != nil { - t.Fatalf("generate token: %v", err) +// TestSelfOrderSiteSupportQueryAccess 验证客服查询只对平台管理员与「自用自动发货」商户开放。 +func TestSelfOrderSiteSupportQueryAccess(t *testing.T) { + engine, jm, db := newTestRouterWithDB(t) + + selfSite := model.Merchant{Code: model.MerchantCodeSelfOrderSite, Name: "自用自动发货", Status: model.MerchantStatusActive} + if err := db.Create(&selfSite).Error; err != nil { + t.Fatalf("创建自用自动发货商户失败: %v", err) } + otherSite := model.Merchant{Code: "other-site", Name: "其他商户", Status: model.MerchantStatusActive} + if err := db.Create(&otherSite).Error; err != nil { + t.Fatalf("创建其他商户失败: %v", err) + } + + createUser := func(username, role string) model.User { + user := model.User{Username: username, PasswordHash: "x", Nickname: username, Role: role, Status: 1} + if err := db.Create(&user).Error; err != nil { + t.Fatalf("创建账号 %s 失败: %v", username, err) + } + return user + } + bind := func(user model.User, merchant model.Merchant) { + if err := db.Create(&model.MerchantMember{ + MerchantID: merchant.ID, UserID: user.ID, Role: model.MemberRoleOwner, Status: 1, IsDefault: true, + }).Error; err != nil { + t.Fatalf("绑定商户成员失败: %v", err) + } + } + + var selfOperated model.Merchant + if err := db.Where("code = ?", model.MerchantCodeSelfOperated).First(&selfOperated).Error; err != nil { + t.Fatalf("读取自营商户失败: %v", err) + } + admin := createUser("support-admin", model.RoleAdmin) + bind(admin, selfOperated) + selfStaff := createUser("self-site-staff", model.RoleMerchant) + bind(selfStaff, selfSite) + otherStaff := createUser("other-site-staff", model.RoleMerchant) + bind(otherStaff, otherSite) cases := []struct { name string - path string + user model.User want int }{ - {name: "商户上下文", path: "/api/merchant", want: 200}, - {name: "发货订单", path: "/api/merchant/orders", want: 200}, - {name: "商品列表应被拒绝", path: "/api/merchant/products", want: 403}, - {name: "钱包应被拒绝", path: "/api/merchant/wallet", want: 403}, - {name: "平台管理接口应被拒绝", path: "/api/platform/merchants", want: 403}, - {name: "客服查询空条件应被拦截", path: "/api/platform/rock/support/bound-tokens", want: 400}, - {name: "客服查询", path: "/api/platform/rock/support/bound-tokens?search=SUP", want: 200}, - {name: "上游包链接路由", path: "/api/platform/rock/tokens/1/package-link", want: 400}, - {name: "商户客服旧接口应下线", path: "/api/merchant/support/rock/bound-tokens", want: 404}, + {name: "平台管理员", user: admin, want: 200}, + {name: "自用自动发货商户", user: selfStaff, want: 200}, + {name: "其他商户", user: otherStaff, want: 403}, } for _, tc := range cases { - req := httptest.NewRequest("GET", tc.path, nil) + token, err := jm.Generate(tc.user.ID, tc.user.Username, tc.user.Role) + if err != nil { + t.Fatalf("generate token: %v", err) + } + req := httptest.NewRequest("GET", "/api/platform/rock/support/bound-tokens?search=SUP", nil) req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() engine.ServeHTTP(w, req) @@ -276,12 +313,11 @@ func TestPlatformSupportContext(t *testing.T) { } } - req := httptest.NewRequest("GET", "/api/merchant", nil) - req.Header.Set("Authorization", "Bearer "+token) + // 未登录不能访问客服查询。 + req := httptest.NewRequest("GET", "/api/platform/rock/support/bound-tokens?search=SUP", nil) w := httptest.NewRecorder() engine.ServeHTTP(w, req) - body := w.Body.String() - if !strings.Contains(body, `"role":"support"`) || !strings.Contains(body, `"orders:manage"`) { - t.Fatalf("平台客服应获得自营商户客服上下文与 orders:manage,实际: %s", body) + if w.Code != 401 { + t.Fatalf("未登录应返回 401,实际 %d: %s", w.Code, w.Body.String()) } } diff --git a/backend/internal/service/platform_user_test.go b/backend/internal/service/platform_user_test.go index 25d1c4a..516cfa0 100644 --- a/backend/internal/service/platform_user_test.go +++ b/backend/internal/service/platform_user_test.go @@ -6,103 +6,74 @@ import ( "affiliate_dash/internal/model" ) -func TestPlatformSupportAccountRoleSwitch(t *testing.T) { +func TestPlatformAdminAccountLifecycle(t *testing.T) { db := newServiceTestDB(t) svc := NewUserService(db, NewTenantService(db)) - actor, err := svc.CreatePlatformAdmin("platform-role-actor", "password123", "管理员") + admin, err := svc.CreatePlatformUser("platform-admin-1", "password123", "管理员一") if err != nil { - t.Fatalf("创建管理员失败: %v", err) + t.Fatalf("创建平台管理员失败: %v", err) } - support, err := svc.CreatePlatformUser("platform-role-support", "password123", "平台客服", model.RolePlatformSupport) - if err != nil { - t.Fatalf("创建平台客服失败: %v", err) - } - if support.Role != model.RolePlatformSupport { - t.Fatalf("平台客服角色不符: %+v", support) + if admin.Role != model.RoleAdmin { + t.Fatalf("平台账号角色应为 admin,实际 %s", admin.Role) } - var membershipCount int64 - if err := db.Model(&model.MerchantMember{}).Where("user_id = ?", support.ID).Count(&membershipCount).Error; err != nil { - t.Fatalf("统计成员关系失败: %v", err) + var membership model.MerchantMember + if err := db.Where("user_id = ?", admin.ID).First(&membership).Error; err != nil { + t.Fatalf("平台管理员缺少自营商户成员关系: %v", err) } - if membershipCount != 0 { - t.Fatalf("平台客服不应写入商户成员关系,实际 %d 条", membershipCount) + if membership.Role != model.MemberRoleOwner || !membership.IsDefault { + t.Fatalf("平台管理员成员关系不符: %+v", membership) } list, total, err := svc.ListPlatformAdmins(1, 100) if err != nil { t.Fatalf("查询平台账号失败: %v", err) } - if total < 2 { - t.Fatalf("平台账号列表应包含管理员与平台客服,实际 total=%d", total) - } - foundSupport := false - for _, item := range list { - if item.ID == support.ID && item.Role == model.RolePlatformSupport { - foundSupport = true - } - } - if !foundSupport { - t.Fatalf("平台账号列表未包含平台客服: %+v", list) + if total != 1 || len(list) != 1 || list[0].ID != admin.ID { + t.Fatalf("平台账号列表应只包含管理员,实际 total=%d list=%+v", total, list) } - // 平台客服 -> 平台管理员:补自营商户负责人上下文。 - updated, err := svc.UpdatePlatformAdmin(support.ID, UpdatePlatformAdminInput{ - Username: support.Username, - Nickname: support.Nickname, + updated, err := svc.UpdatePlatformAdmin(admin.ID, UpdatePlatformAdminInput{ + Username: "platform-admin-renamed", + Nickname: "管理员一改", Status: 1, - Role: model.RoleAdmin, - }, actor.ID) + }, 999) if err != nil { - t.Fatalf("提升为管理员失败: %v", err) + t.Fatalf("更新平台管理员失败: %v", err) } - if updated.Role != model.RoleAdmin { - t.Fatalf("角色未更新为管理员: %+v", updated) - } - var membership model.MerchantMember - if err := db.Where("user_id = ?", support.ID).First(&membership).Error; err != nil { - t.Fatalf("管理员缺少自营商户成员关系: %v", err) - } - if membership.Role != model.MemberRoleOwner || !membership.IsDefault { - t.Fatalf("管理员成员关系不符: %+v", membership) + if updated.Username != "platform-admin-renamed" || updated.Nickname != "管理员一改" || updated.Role != model.RoleAdmin { + t.Fatalf("平台管理员更新结果不符: %+v", updated) } - // 平台管理员 -> 平台客服:移除商户成员关系,回到 Tenant 合成上下文。 - updated, err = svc.UpdatePlatformAdmin(support.ID, UpdatePlatformAdminInput{ - Username: support.Username, - Nickname: support.Nickname, - Status: 1, - Role: model.RolePlatformSupport, - }, actor.ID) + merchantUser, err := svc.Create("platform-merchant-1", "password123", "商户账号", model.RoleMerchant, 0) if err != nil { - t.Fatalf("降级为平台客服失败: %v", err) + t.Fatalf("创建商户账号失败: %v", err) } - if updated.Role != model.RolePlatformSupport { - t.Fatalf("角色未更新为平台客服: %+v", updated) - } - if err := db.Model(&model.MerchantMember{}).Where("user_id = ?", support.ID).Count(&membershipCount).Error; err != nil { - t.Fatalf("统计成员关系失败: %v", err) - } - if membershipCount != 0 { - t.Fatalf("降级后应移除商户成员关系,实际 %d 条", membershipCount) + if _, err := svc.UpdatePlatformAdmin(merchantUser.ID, UpdatePlatformAdminInput{ + Username: merchantUser.Username, + Status: 1, + }, 999); err == nil { + t.Fatal("平台账号接口不应允许管理商户账号") } } -func TestCannotDemoteLastPlatformAdmin(t *testing.T) { +func TestCannotDisableOrDeleteLastPlatformAdmin(t *testing.T) { db := newServiceTestDB(t) svc := NewUserService(db, nil) - admin, err := svc.CreatePlatformAdmin("only-role-admin", "password123", "唯一管理员") + admin, err := svc.CreatePlatformAdmin("only-admin", "password123", "唯一管理员") if err != nil { t.Fatalf("创建管理员失败: %v", err) } if _, err := svc.UpdatePlatformAdmin(admin.ID, UpdatePlatformAdminInput{ Username: admin.Username, Nickname: admin.Nickname, - Status: 1, - Role: model.RolePlatformSupport, + Status: 0, }, 0); err == nil { - t.Fatal("最后一个启用的平台管理员不能被降级") + t.Fatal("最后一个启用的平台管理员不能被停用") + } + if err := svc.DeletePlatformAdmin(admin.ID, 0); err == nil { + t.Fatal("最后一个启用的平台管理员不能被删除") } } diff --git a/backend/internal/service/user.go b/backend/internal/service/user.go index 2cbc1e6..3e08460 100644 --- a/backend/internal/service/user.go +++ b/backend/internal/service/user.go @@ -82,7 +82,7 @@ func (s *UserService) Create(username, password, nickname, role string, merchant if role == "" { role = model.RoleMerchant } - if role != model.RoleAdmin && role != model.RoleMerchant && role != model.RolePlatformSupport { + if role != model.RoleAdmin && role != model.RoleMerchant { return nil, errors.New("无效的账号角色") } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) @@ -145,14 +145,13 @@ type UpdatePlatformAdminInput struct { Nickname string Password string Status int - Role string } func (s *UserService) ListPlatformAdmins(page, size int) ([]model.User, int64, error) { list, total, err := s.List(UserListQuery{ Page: page, Size: size, - Roles: []string{model.RoleAdmin, model.RolePlatformSupport}, + Roles: []string{model.RoleAdmin}, }) if err != nil { return nil, 0, err @@ -221,23 +220,14 @@ func (s *UserService) ListMerchantAccountGroups(page, size int) ([]MerchantMembe return groups, total, nil } -// CreatePlatformUser creates a platform-level account. Platform admins get the -// hidden self-operated owner membership; platform support accounts use the -// synthetic tenant context in middleware.Tenant and do not become merchant members. -func (s *UserService) CreatePlatformUser(username, password, nickname, role string) (*model.User, error) { - if role == "" { - role = model.RoleAdmin - } - if role != model.RoleAdmin && role != model.RolePlatformSupport { - return nil, errors.New("无效的平台账号角色") - } - return s.Create(username, password, nickname, role, 0) +// CreatePlatformUser 创建平台管理员账号,并写入自营商户负责人成员关系。 +func (s *UserService) CreatePlatformUser(username, password, nickname string) (*model.User, error) { + return s.Create(username, password, nickname, model.RoleAdmin, 0) } -// CreatePlatformAdmin creates an account with platform privileges and the -// hidden self-operated merchant context required by merchant-side routes. +// CreatePlatformAdmin 保留旧调用名,统一创建平台管理员。 func (s *UserService) CreatePlatformAdmin(username, password, nickname string) (*model.User, error) { - return s.CreatePlatformUser(username, password, nickname, model.RoleAdmin) + return s.CreatePlatformUser(username, password, nickname) } func (s *UserService) UpdatePlatformAdmin(id uint, in UpdatePlatformAdminInput, actorUserID uint) (*model.User, error) { @@ -259,18 +249,11 @@ func (s *UserService) UpdatePlatformAdmin(id uint, in UpdatePlatformAdminInput, } return err } - if user.Role != model.RoleAdmin && user.Role != model.RolePlatformSupport { + if user.Role != model.RoleAdmin { return errors.New("只能管理平台账号") } - targetRole := user.Role - if in.Role != "" { - targetRole = in.Role - } - if targetRole != model.RoleAdmin && targetRole != model.RolePlatformSupport { - return errors.New("无效的平台账号角色") - } - // 降级或停用最后一个启用的平台管理员会导致平台无法登录。 - if user.Role == model.RoleAdmin && user.Status == 1 && (targetRole != model.RoleAdmin || in.Status == 0) { + // 停用最后一个启用的平台管理员会导致平台无法登录。 + 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 @@ -288,39 +271,10 @@ func (s *UserService) UpdatePlatformAdmin(id uint, in UpdatePlatformAdminInput, return errors.New("用户名已存在") } } - if targetRole != user.Role { - // 管理员需要自营商户负责人成员关系;平台客服使用 Tenant 合成上下文, - // 不写入 merchant_members,避免出现在商户成员列表里。 - if err := tx.Where("user_id = ?", user.ID).Delete(&model.MerchantMember{}).Error; err != nil { - return err - } - if targetRole == model.RoleAdmin { - var selfMerchant model.Merchant - if err := tx.Where("code = ?", model.MerchantCodeSelfOperated).First(&selfMerchant).Error; err != nil { - return err - } - member := model.MerchantMember{ - MerchantID: selfMerchant.ID, - UserID: user.ID, - Role: model.MemberRoleOwner, - Status: 1, - IsDefault: true, - } - if err := tx.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "merchant_id"}, {Name: "user_id"}}, - DoUpdates: clause.Assignments(map[string]interface{}{ - "role": model.MemberRoleOwner, "status": 1, "is_default": true, - }), - }).Create(&member).Error; err != nil { - return err - } - } - } updates := map[string]interface{}{ "username": in.Username, "nickname": in.Nickname, "status": in.Status, - "role": targetRole, } if in.Password != "" { hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) @@ -383,7 +337,7 @@ func (s *UserService) DeletePlatformAdmin(id, actorUserID uint) error { } return err } - if user.Role != model.RoleAdmin && user.Role != model.RolePlatformSupport { + if user.Role != model.RoleAdmin { return errors.New("只能删除平台账号") } if user.Role == model.RoleAdmin && user.Status == 1 { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eaef41f..e3fc7f8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,7 +23,6 @@ const PlatformOrders = lazy(() => import('./pages/PlatformOrders')) const PlatformProducts = lazy(() => import('./pages/PlatformProducts')) const PlatformUsers = lazy(() => import('./pages/PlatformUsers')) const PlatformRock = lazy(() => import('./pages/PlatformRock')) -const SupportRockTokens = lazy(() => import('./pages/SupportRockTokens')) // RouteFallback 是懒加载页面与权限校验的统一占位,避免切换时白屏。 function RouteFallback() { @@ -60,21 +59,8 @@ function MerchantPermissionRoute({ permission, children }: { permission?: string return merchantPermissions.includes(permission) ? <>{children}> : } -function SupportQueryRoute({ children }: { children: ReactNode }) { - const { isAdmin, isPlatformSupport } = useAuth() - return isAdmin || isPlatformSupport ? <>{children}> : -} - -function HomeRoute() { - const { isPlatformSupport } = useAuth() - if (isPlatformSupport) return - return -} - function MerchantCenterRoute() { - const { isPlatformSupport } = useAuth() const location = useLocation() - if (isPlatformSupport) return const requestedTab = new URLSearchParams(location.search).get('tab') || 'products' const tab = requestedTab in tabPermissions ? requestedTab : 'products' return ( @@ -97,7 +83,7 @@ function AppRoutes() { } > - } /> + } /> } /> } /> } /> @@ -139,14 +125,7 @@ function AppRoutes() { } /> - - - - } - /> + } /> } /> } /> request.post("/platform/admins", data).then((r) => r.data.data as User), update: ( id: number, @@ -112,7 +111,6 @@ export const userApi = { nickname?: string password?: string status: number - role?: 'admin' | 'platform_support' }, ) => request diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index ba3e88d..edadc03 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -70,7 +70,6 @@ const adminSections: SidebarSection[] = [ { key: 'merchant-products', label: '商品列表', path: '/merchant-products' }, { key: 'platform-products', label: '渠道与路由配置', path: '/platform-products' }, { key: 'platform-rock', label: 'Rock发货与库存风控', path: '/platform-rock' }, - { key: 'support-rock', label: '客服查询', path: '/support-rock-tokens' }, ], }, { @@ -123,21 +122,6 @@ const adminSections: SidebarSection[] = [ }, ] -const platformSupportSections: SidebarSection[] = [ - { - key: 'products', - label: '商品管理', - icon: , - children: [{ key: 'support-rock', label: '客服查询', path: '/support-rock-tokens' }], - }, - { - key: 'orders', - label: '订单管理', - icon: , - children: [{ key: 'fulfillment-orders', label: '发货订单', path: '/merchant-orders' }], - }, -] - const merchantSections: SidebarSection[] = [ { key: 'dashboard', @@ -190,7 +174,6 @@ function getSelectedKey(pathname: string, search: string) { if (pathname.startsWith('/merchant-products')) return 'merchant-products' if (pathname.startsWith('/platform-products')) return 'platform-products' if (pathname.startsWith('/platform-rock')) return 'platform-rock' - if (pathname.startsWith('/support-rock-tokens')) return 'support-rock' if (pathname.startsWith('/merchant-orders')) return 'fulfillment-orders' if (pathname.startsWith('/merchant-wallet')) return 'merchant-wallet' if (pathname.startsWith('/merchant-recharge')) return 'merchant-recharge' @@ -245,13 +228,13 @@ export default function MainLayout() { const [collapsed, setCollapsed] = useState(() => { return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === 'true' }) - const { user, logout, refreshProfile, isAdmin, isPlatformSupport, merchant, merchantPermissions } = useAuth() + const { user, logout, refreshProfile, isAdmin, merchant, merchantPermissions } = useAuth() useEffect(() => { - // 平台角色保持全局视角,清除任何残留的商户上下文 - if ((isAdmin || isPlatformSupport) && localStorage.getItem('merchant_id')) { + // 平台管理员保持全局视角,清除任何残留的商户上下文 + if (isAdmin && localStorage.getItem('merchant_id')) { localStorage.removeItem('merchant_id') } - }, [isAdmin, isPlatformSupport]) + }, [isAdmin]) const [passwordOpen, setPasswordOpen] = useState(false) const [passwordSaving, setPasswordSaving] = useState(false) const [passwordForm] = Form.useForm() @@ -261,7 +244,6 @@ export default function MainLayout() { const sections = useMemo(() => { if (isAdmin) return adminSections - if (isPlatformSupport) return platformSupportSections const features = new Set((merchant?.features || '').split(/[\s,]+/).filter(Boolean)) return merchantSections.flatMap((section) => { if (!section.children) return [section] @@ -271,7 +253,7 @@ export default function MainLayout() { )) return children.length > 0 ? [{ ...section, children }] : [] }) - }, [isAdmin, isPlatformSupport, merchant?.features, merchantPermissions]) + }, [isAdmin, merchant?.features, merchantPermissions]) const selectedKey = getSelectedKey(location.pathname, location.search) const [openKeys, setOpenKeys] = useState(() => loadStoredOpenKeys(sections, selectedKey)) @@ -343,7 +325,7 @@ export default function MainLayout() { {user?.nickname || user?.username} - 角色:{isAdmin ? '平台超级管理员' : isPlatformSupport ? '平台客服' : '商户成员'} + 角色:{isAdmin ? '平台超级管理员' : '商户成员'} ), @@ -479,10 +461,6 @@ export default function MainLayout() { 管理员 - ) : isPlatformSupport ? ( - - 平台客服 - ) : ( 商户 diff --git a/frontend/src/pages/MerchantCenter.tsx b/frontend/src/pages/MerchantCenter.tsx index 41aa9f5..8aaf8f6 100644 --- a/frontend/src/pages/MerchantCenter.tsx +++ b/frontend/src/pages/MerchantCenter.tsx @@ -60,7 +60,7 @@ export default function MerchantCenter({ fixedTab, title = "商户中心", }: MerchantCenterProps = {}) { - const { isAdmin, isPlatformSupport, user } = useAuth(); + const { isAdmin, user } = useAuth(); const location = useLocation(); const navigate = useNavigate(); const routeTab = useMemo( @@ -726,6 +726,8 @@ export default function MerchantCenter({ } }; + // 客服查询只对平台管理员与「自用自动发货」商户展示,其他商户保持原发货订单页。 + const canUseSupportQuery = isAdmin || merchant?.code === "self-order-site"; const tabItems = [ { key: "products", @@ -766,6 +768,7 @@ export default function MerchantCenter({ onRevokeLink={revokeDeliveryLink} onRestoreLink={restoreDeliveryLink} onCancelOrder={cancelOrder} + showSupportQuery={canUseSupportQuery} /> ), }, @@ -893,9 +896,7 @@ export default function MerchantCenter({ title={title} subtitle={ merchant - ? isPlatformSupport - ? `${merchant.name} (${merchant.code}) · 平台客服` - : `${merchant.name} (${merchant.code}) · 当前身份: ${merchantRole ? roleText(merchantRole) : "-"}` + ? `${merchant.name} (${merchant.code}) · 当前身份: ${merchantRole ? roleText(merchantRole) : "-"}` : "商户配置与管理中心" } breadcrumbs={computedBreadcrumbs} diff --git a/frontend/src/pages/PlatformUsers.tsx b/frontend/src/pages/PlatformUsers.tsx index 4b3e71f..f45e458 100644 --- a/frontend/src/pages/PlatformUsers.tsx +++ b/frontend/src/pages/PlatformUsers.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { Button, Form, Input, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography, message } from 'antd' +import { Button, Form, Input, Modal, Popconfirm, Space, Switch, Table, Tabs, 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' @@ -15,16 +15,6 @@ type AdminFormValues = { nickname?: string password?: string status: boolean - role: 'admin' | 'platform_support' -} - -const platformRoleOptions = [ - { value: 'admin', label: '平台管理员' }, - { value: 'platform_support', label: '平台客服' }, -] - -function platformRoleLabel(role: string) { - return platformRoleOptions.find((item) => item.value === role)?.label || role } function maskPhone(value?: string) { @@ -79,7 +69,7 @@ export default function PlatformUsers() { const openCreate = () => { setEditingAdmin(null) - form.setFieldsValue({ username: '', nickname: '', password: '', status: true, role: 'admin' }) + form.setFieldsValue({ username: '', nickname: '', password: '', status: true }) setModalOpen(true) } @@ -90,7 +80,6 @@ export default function PlatformUsers() { nickname: record.nickname, password: '', status: record.status === 1, - role: record.role === 'platform_support' ? 'platform_support' : 'admin', }) setModalOpen(true) }, [form]) @@ -105,15 +94,13 @@ export default function PlatformUsers() { nickname: values.nickname || '', password: values.password || undefined, status: values.status ? 1 : 0, - role: values.role, }) - message.success('平台账号已更新,角色变更需该账号重新登录生效') + message.success('平台账号已更新') } else { await userApi.create({ username: values.username, password: values.password || '', nickname: values.nickname || '', - role: values.role, }) message.success('平台账号已创建') } @@ -228,16 +215,6 @@ export default function PlatformUsers() { const columns: ColumnsType = useMemo(() => [ { title: '用户名', dataIndex: 'username', width: 220, render: (value) => {value} }, { title: '昵称', dataIndex: 'nickname', width: 180, render: (value) => value || '-' }, - { - title: '角色', - dataIndex: 'role', - width: 120, - render: (role) => ( - - {platformRoleLabel(role)} - - ), - }, { title: '手机号', width: 170, render: (_, record) => phoneCell(record) }, { title: '绑定设备', width: 200, render: (_, record) => deviceCell(record) }, { title: '状态', dataIndex: 'status', width: 100, render: (value) => value === 1 ? 启用 : 停用 }, @@ -248,7 +225,7 @@ export default function PlatformUsers() { render: (_, record) => record.id === currentUser?.id ? 当前账号 : ( } onClick={() => openEdit(record)}>编辑 - remove(record)} okButtonProps={{ danger: true }}> + remove(record)} okButtonProps={{ danger: true }}> }>删除 @@ -343,7 +320,7 @@ export default function PlatformUsers() { } loading={loading} onClick={() => tab === 'admins' ? void loadAdmins() : void loadMerchantAccounts()}>刷新{tab === 'admins' && } onClick={openCreate}>新建平台账号}} /> @@ -357,16 +334,13 @@ export default function PlatformUsers() { okText="保存" destroyOnHidden > - + - - - diff --git a/frontend/src/pages/SupportRockTokens.tsx b/frontend/src/pages/SupportRockTokens.tsx index 288e5d6..84d4213 100644 --- a/frontend/src/pages/SupportRockTokens.tsx +++ b/frontend/src/pages/SupportRockTokens.tsx @@ -9,7 +9,11 @@ import { buildTokenColumns } from './platformRock/tokenColumns' import { TokenStatusLogDrawer } from './platformRock/TokenStatusLogDrawer' import { VoucherPreviewModal } from './platformRock/VoucherPreviewModal' -export default function SupportRockTokens() { +export default function SupportRockTokens({ + embedded = false, +}: { + embedded?: boolean +}) { const [search, setSearch] = useState('') const [query, setQuery] = useState('') const [searched, setSearched] = useState(false) @@ -192,11 +196,13 @@ export default function SupportRockTokens() { return ( - 共 {total} 条 : undefined} - /> + {!embedded && ( + 共 {total} 条 : undefined} + /> + )} ; @@ -499,11 +501,13 @@ export function OrdersTab({ onRevokeLink: (orderNo: string) => void; onRestoreLink: (orderNo: string) => void; onCancelOrder: (orderNo: string, reason: string) => Promise | void; + showSupportQuery: boolean; }) { const [keyword, setKeyword] = useState(filters.keyword); const [cancelling, setCancelling] = useState(null); const [cancelReason, setCancelReason] = useState(""); const [cancelSubmitting, setCancelSubmitting] = useState(false); + const [showSupport, setShowSupport] = useState(false); useEffect(() => { setKeyword(filters.keyword); @@ -707,15 +711,25 @@ export function OrdersTab({ onOrderSourceChange(key as "api" | "manual")} + activeKey={showSupport ? "support" : orderSource} + onChange={(key) => { + if (key === "support") { + setShowSupport(true); + return; + } + setShowSupport(false); + onOrderSourceChange(key as "api" | "manual"); + }} items={[ { key: "api", label: "API 订单" }, { key: "manual", label: "手动订单" }, + ...(showSupportQuery + ? [{ key: "support", label: "客服查询" }] + : []), ]} style={{ minWidth: 220 }} /> - {canManage && ( + {canManage && !showSupport && ( )} + {showSupport ? ( + + ) : ( + <> @@ -767,6 +785,8 @@ export function OrdersTab({ scroll={{ x: 1780 }} pagination={pageConfig(orders, onLoadOrders)} /> + > + )} Promise isAdmin: boolean - isPlatformSupport: boolean } // Context 与 useAuth 单独放在本文件:auth.tsx 因此只导出 AuthProvider 组件, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 6ee3900..835b5d1 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -2,7 +2,7 @@ export interface User { id: number username: string nickname: string - role: 'admin' | 'merchant' | 'platform_support' + role: 'admin' | 'merchant' status: number created_at: string phone?: string