diff --git a/backend/app/http/tenant/dto/tenant_user_admin.go b/backend/app/http/tenant/dto/tenant_user_admin.go new file mode 100644 index 0000000..7e53649 --- /dev/null +++ b/backend/app/http/tenant/dto/tenant_user_admin.go @@ -0,0 +1,26 @@ +package dto + +import ( + "quyun/v2/app/requests" + "quyun/v2/database/models" +) + +// AdminTenantUserJoinResponse 返回租户管理员添加成员后的结果。 +type AdminTenantUserJoinResponse struct { + // TenantUser 租户成员关系记录。 + TenantUser *models.TenantUser `json:"tenant_user,omitempty"` +} + +// AdminTenantUserRoleUpdateForm 租户成员角色更新表单。 +type AdminTenantUserRoleUpdateForm struct { + // Role 角色:member/tenant_admin。 + Role string `json:"role,omitempty"` +} + +// AdminTenantUserListFilter 租户管理员查询成员列表的过滤条件。 +type AdminTenantUserListFilter struct { + // Pagination 分页参数(page/limit)。 + requests.Pagination `json:",inline" query:",inline"` + // UserID 按用户ID过滤(可选)。 + UserID *int64 `json:"user_id,omitempty" query:"user_id"` +} diff --git a/backend/app/http/tenant/provider.gen.go b/backend/app/http/tenant/provider.gen.go index e4dcc2e..26f24ad 100755 --- a/backend/app/http/tenant/provider.gen.go +++ b/backend/app/http/tenant/provider.gen.go @@ -60,15 +60,17 @@ func Provide(opts ...opt.Option) error { order *order, orderAdmin *orderAdmin, orderMe *orderMe, + tenantUserAdmin *tenantUserAdmin, ) (contracts.HttpRoute, error) { obj := &Routes{ - content: content, - contentAdmin: contentAdmin, - me: me, - middlewares: middlewares, - order: order, - orderAdmin: orderAdmin, - orderMe: orderMe, + content: content, + contentAdmin: contentAdmin, + me: me, + middlewares: middlewares, + order: order, + orderAdmin: orderAdmin, + orderMe: orderMe, + tenantUserAdmin: tenantUserAdmin, } if err := obj.Prepare(); err != nil { return nil, err @@ -78,5 +80,12 @@ func Provide(opts ...opt.Option) error { }, atom.GroupRoutes); err != nil { return err } + if err := container.Container.Provide(func() (*tenantUserAdmin, error) { + obj := &tenantUserAdmin{} + + return obj, nil + }); err != nil { + return err + } return nil } diff --git a/backend/app/http/tenant/routes.gen.go b/backend/app/http/tenant/routes.gen.go index a3d6df7..17def4b 100644 --- a/backend/app/http/tenant/routes.gen.go +++ b/backend/app/http/tenant/routes.gen.go @@ -24,12 +24,13 @@ type Routes struct { log *log.Entry `inject:"false"` middlewares *middlewares.Middlewares // Controller instances - content *content - contentAdmin *contentAdmin - me *me - order *order - orderAdmin *orderAdmin - orderMe *orderMe + content *content + contentAdmin *contentAdmin + me *me + order *order + orderAdmin *orderAdmin + orderMe *orderMe + tenantUserAdmin *tenantUserAdmin } // Prepare initializes the routes provider with logging configuration. @@ -184,6 +185,29 @@ func (r *Routes) Register(router fiber.Router) { Local[*models.User]("user"), PathParam[int64]("orderID"), )) + // Register routes for controller: tenantUserAdmin + r.log.Debugf("Registering route: Get /t/:tenantCode/v1/admin/users -> tenantUserAdmin.adminTenantUsers") + router.Get("/t/:tenantCode/v1/admin/users"[len(r.Path()):], DataFunc3( + r.tenantUserAdmin.adminTenantUsers, + Local[*models.Tenant]("tenant"), + Local[*models.TenantUser]("tenant_user"), + Query[dto.AdminTenantUserListFilter]("filter"), + )) + r.log.Debugf("Registering route: Patch /t/:tenantCode/v1/admin/users/:userID/role -> tenantUserAdmin.adminSetUserRole") + router.Patch("/t/:tenantCode/v1/admin/users/:userID/role"[len(r.Path()):], DataFunc4( + r.tenantUserAdmin.adminSetUserRole, + Local[*models.Tenant]("tenant"), + Local[*models.TenantUser]("tenant_user"), + PathParam[int64]("userID"), + Body[dto.AdminTenantUserRoleUpdateForm]("form"), + )) + r.log.Debugf("Registering route: Post /t/:tenantCode/v1/admin/users/:userID/join -> tenantUserAdmin.adminJoinUser") + router.Post("/t/:tenantCode/v1/admin/users/:userID/join"[len(r.Path()):], DataFunc3( + r.tenantUserAdmin.adminJoinUser, + Local[*models.Tenant]("tenant"), + Local[*models.TenantUser]("tenant_user"), + PathParam[int64]("userID"), + )) r.log.Info("Successfully registered all routes") } diff --git a/backend/app/http/tenant/tenant_user_admin.go b/backend/app/http/tenant/tenant_user_admin.go new file mode 100644 index 0000000..c2d0edf --- /dev/null +++ b/backend/app/http/tenant/tenant_user_admin.go @@ -0,0 +1,183 @@ +package tenant + +import ( + "strings" + + "quyun/v2/app/errorx" + "quyun/v2/app/http/tenant/dto" + "quyun/v2/app/requests" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + "go.ipao.vip/gen" +) + +// tenantUserAdmin provides tenant-admin member management endpoints. +// +// @provider +type tenantUserAdmin struct{} + +// adminJoinUser +// +// @Summary 添加租户成员(租户管理) +// @Tags Tenant +// @Accept json +// @Produce json +// @Param tenantCode path string true "Tenant Code" +// @Param userID path int64 true "UserID" +// @Success 200 {object} dto.AdminTenantUserJoinResponse +// +// @Router /t/:tenantCode/v1/admin/users/:userID/join [post] +// @Bind tenant local key(tenant) +// @Bind tenantUser local key(tenant_user) +// @Bind userID path +func (*tenantUserAdmin) adminJoinUser( + ctx fiber.Ctx, + tenant *models.Tenant, + tenantUser *models.TenantUser, + userID int64, +) (*dto.AdminTenantUserJoinResponse, error) { + if err := requireTenantAdmin(tenantUser); err != nil { + return nil, err + } + if userID <= 0 { + return nil, errorx.ErrInvalidParameter.WithMsg("user_id must be > 0") + } + + log.WithFields(log.Fields{ + "tenant_id": tenant.ID, + "operator_user_id": tenantUser.UserID, + "target_user_id": userID, + "operator_is_admin": true, + }).Info("tenant.admin.users.join") + + // 关键逻辑:以 TenantUser 为准创建成员关系;服务层保证幂等(已存在则不重复创建)。 + if err := services.Tenant.AddUser(ctx.Context(), tenant.ID, userID); err != nil { + return nil, err + } + + m, err := services.Tenant.FindTenantUser(ctx.Context(), tenant.ID, userID) + if err != nil { + return nil, err + } + return &dto.AdminTenantUserJoinResponse{TenantUser: m}, nil +} + +// adminSetUserRole +// +// @Summary 设置成员角色(租户管理) +// @Tags Tenant +// @Accept json +// @Produce json +// @Param tenantCode path string true "Tenant Code" +// @Param userID path int64 true "UserID" +// @Param form body dto.AdminTenantUserRoleUpdateForm true "Form" +// @Success 200 {object} dto.AdminTenantUserJoinResponse +// +// @Router /t/:tenantCode/v1/admin/users/:userID/role [patch] +// @Bind tenant local key(tenant) +// @Bind tenantUser local key(tenant_user) +// @Bind userID path +// @Bind form body +func (*tenantUserAdmin) adminSetUserRole( + ctx fiber.Ctx, + tenant *models.Tenant, + tenantUser *models.TenantUser, + userID int64, + form *dto.AdminTenantUserRoleUpdateForm, +) (*dto.AdminTenantUserJoinResponse, error) { + if err := requireTenantAdmin(tenantUser); err != nil { + return nil, err + } + if userID <= 0 || form == nil { + return nil, errorx.ErrInvalidParameter + } + + roleStr := strings.TrimSpace(form.Role) + if roleStr == "" { + return nil, errorx.ErrInvalidParameter.WithMsg("role is required") + } + + var role consts.TenantUserRole + switch roleStr { + case string(consts.TenantUserRoleMember): + role = consts.TenantUserRoleMember + case string(consts.TenantUserRoleTenantAdmin): + role = consts.TenantUserRoleTenantAdmin + default: + return nil, errorx.ErrInvalidParameter.WithMsg("invalid role") + } + + log.WithFields(log.Fields{ + "tenant_id": tenant.ID, + "operator_user_id": tenantUser.UserID, + "target_user_id": userID, + "role": role, + }).Info("tenant.admin.users.set_role") + + if err := services.Tenant.SetUserRole(ctx.Context(), tenant.ID, userID, role); err != nil { + return nil, err + } + + m, err := services.Tenant.FindTenantUser(ctx.Context(), tenant.ID, userID) + if err != nil { + return nil, err + } + return &dto.AdminTenantUserJoinResponse{TenantUser: m}, nil +} + +// adminTenantUsers +// +// @Summary 成员列表(租户管理) +// @Tags Tenant +// @Accept json +// @Produce json +// @Param tenantCode path string true "Tenant Code" +// @Param filter query dto.AdminTenantUserListFilter true "Filter" +// @Success 200 {object} requests.Pager{items=models.TenantUser} +// +// @Router /t/:tenantCode/v1/admin/users [get] +// @Bind tenant local key(tenant) +// @Bind tenantUser local key(tenant_user) +// @Bind filter query +func (*tenantUserAdmin) adminTenantUsers( + ctx fiber.Ctx, + tenant *models.Tenant, + tenantUser *models.TenantUser, + filter *dto.AdminTenantUserListFilter, +) (*requests.Pager, error) { + if err := requireTenantAdmin(tenantUser); err != nil { + return nil, err + } + if filter == nil { + filter = &dto.AdminTenantUserListFilter{} + } + + log.WithFields(log.Fields{ + "tenant_id": tenant.ID, + "user_id": tenantUser.UserID, + "query_uid": filter.UserID, + }).Info("tenant.admin.users.list") + + filter.Pagination.Format() + + tbl, query := models.TenantUserQuery.QueryContext(ctx.Context()) + conds := []gen.Condition{tbl.TenantID.Eq(tenant.ID)} + if filter.UserID != nil && *filter.UserID > 0 { + conds = append(conds, tbl.UserID.Eq(*filter.UserID)) + } + + items, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit)) + if err != nil { + return nil, err + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index 2bfc038..b228a21 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -15,6 +15,8 @@ import ( "github.com/samber/lo" "github.com/sirupsen/logrus" "go.ipao.vip/gen" + "go.ipao.vip/gen/types" + "gorm.io/gorm" ) // tenant implements tenant-related domain operations. @@ -35,9 +37,27 @@ func (t *tenant) ContainsUserID(ctx context.Context, tenantID, userID int64) (*m // AddUser func (t *tenant) AddUser(ctx context.Context, tenantID, userID int64) error { + logrus.WithFields(logrus.Fields{ + "tenant_id": tenantID, + "user_id": userID, + }).Info("services.tenant.add_user") + + // 幂等:若成员关系已存在,则直接返回成功,避免重复插入触发唯一约束错误。 + tbl, query := models.TenantUserQuery.QueryContext(ctx) + _, err := query.Where(tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID)).First() + if err == nil { + return nil + } + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(err, "AddUser failed to query existing, tenantID: %d, userID: %d", tenantID, userID) + } + + // 关键默认值:加入租户默认成为 member,并设置为 verified(避免 DB 默认值与枚举不一致导致脏数据)。 tenantUser := &models.TenantUser{ TenantID: tenantID, UserID: userID, + Role: types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleMember}), + Status: consts.UserStatusVerified, } if err := tenantUser.Create(ctx); err != nil { @@ -69,7 +89,8 @@ func (t *tenant) SetUserRole(ctx context.Context, tenantID, userID int64, role . return errors.Wrapf(err, "SetUserRole failed to find, tenantID: %d, userID: %d", tenantID, userID) } - tenantUser.Role = role + // 角色更新:当前约定 role 数组通常只存一个主角色(member/tenant_admin)。 + tenantUser.Role = types.NewArray(role) if _, err := tenantUser.Update(ctx); err != nil { return errors.Wrapf(err, "SetUserRole failed to update, tenantID: %d, userID: %d", tenantID, userID) } diff --git a/backend/app/services/tenant_test.go b/backend/app/services/tenant_test.go index 0e0d548..e1ce13e 100644 --- a/backend/app/services/tenant_test.go +++ b/backend/app/services/tenant_test.go @@ -7,6 +7,7 @@ import ( "quyun/v2/app/commands/testx" "quyun/v2/database" "quyun/v2/database/models" + "quyun/v2/pkg/consts" "quyun/v2/pkg/utils" . "github.com/smartystreets/goconvey/convey" @@ -48,3 +49,52 @@ func (t *TenantTestSuite) Test_TenantUserCount() { t.T().Logf("%s", utils.MustJsonString(result)) }) } + +func (t *TenantTestSuite) Test_AddUser() { + Convey("Tenant.AddUser", t.T(), func() { + ctx := t.T().Context() + tenantID := int64(1) + userID := int64(2) + + database.Truncate(ctx, t.DB, models.TableNameTenantUser) + + Convey("首次添加成员成功", func() { + err := Tenant.AddUser(ctx, tenantID, userID) + So(err, ShouldBeNil) + + m, err := Tenant.FindTenantUser(ctx, tenantID, userID) + So(err, ShouldBeNil) + So(m, ShouldNotBeNil) + So(m.TenantID, ShouldEqual, tenantID) + So(m.UserID, ShouldEqual, userID) + }) + + Convey("重复添加应幂等返回成功", func() { + So(Tenant.AddUser(ctx, tenantID, userID), ShouldBeNil) + So(Tenant.AddUser(ctx, tenantID, userID), ShouldBeNil) + }) + }) +} + +func (t *TenantTestSuite) Test_SetUserRole() { + Convey("Tenant.SetUserRole", t.T(), func() { + ctx := t.T().Context() + tenantID := int64(1) + userID := int64(2) + + database.Truncate(ctx, t.DB, models.TableNameTenantUser) + + So(Tenant.AddUser(ctx, tenantID, userID), ShouldBeNil) + + Convey("设置为 tenant_admin 成功", func() { + err := Tenant.SetUserRole(ctx, tenantID, userID, consts.TenantUserRoleTenantAdmin) + So(err, ShouldBeNil) + + m, err := Tenant.FindTenantUser(ctx, tenantID, userID) + So(err, ShouldBeNil) + So(m, ShouldNotBeNil) + So(len(m.Role), ShouldEqual, 1) + So(m.Role[0], ShouldEqual, consts.TenantUserRoleTenantAdmin) + }) + }) +} diff --git a/backend/database/migrations/20251218171000_fix_tenant_users_status_default.sql b/backend/database/migrations/20251218171000_fix_tenant_users_status_default.sql new file mode 100644 index 0000000..5795fe0 --- /dev/null +++ b/backend/database/migrations/20251218171000_fix_tenant_users_status_default.sql @@ -0,0 +1,19 @@ +-- +goose Up +-- +goose StatementBegin +-- tenant_users.status:历史上默认值为 'active',但代码枚举使用 UserStatus(pending_verify/verified/banned)。 +-- 为避免新增成员落入未知状态,这里将默认值调整为 'verified',并修正存量 'active' -> 'verified'。 +ALTER TABLE tenant_users + ALTER COLUMN status SET DEFAULT 'verified'; + +UPDATE tenant_users +SET status = 'verified' +WHERE status = 'active'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- 回滚:恢复默认值为 'active'(不回滚数据修正)。 +ALTER TABLE tenant_users + ALTER COLUMN status SET DEFAULT 'active'; +-- +goose StatementEnd + diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 0e1a65a..3d5a3e5 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -803,6 +803,154 @@ const docTemplate = `{ } } }, + "/t/{tenantCode}/v1/admin/users": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "成员列表(租户管理)", + "parameters": [ + { + "type": "string", + "description": "Tenant Code", + "name": "tenantCode", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page is 1-based page index; values \u003c= 0 are normalized to 1.", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "UserID 按用户ID过滤(可选)。", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "$ref": "#/definitions/models.TenantUser" + } + } + } + ] + } + } + } + } + }, + "/t/{tenantCode}/v1/admin/users/{userID}/join": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "添加租户成员(租户管理)", + "parameters": [ + { + "type": "string", + "description": "Tenant Code", + "name": "tenantCode", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "UserID", + "name": "userID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.AdminTenantUserJoinResponse" + } + } + } + } + }, + "/t/{tenantCode}/v1/admin/users/{userID}/role": { + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "设置成员角色(租户管理)", + "parameters": [ + { + "type": "string", + "description": "Tenant Code", + "name": "tenantCode", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "UserID", + "name": "userID", + "in": "path", + "required": true + }, + { + "description": "Form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AdminTenantUserRoleUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.AdminTenantUserJoinResponse" + } + } + } + } + }, "/t/{tenantCode}/v1/admin/users/{userID}/topup": { "post": { "consumes": [ @@ -1553,6 +1701,28 @@ const docTemplate = `{ } } }, + "dto.AdminTenantUserJoinResponse": { + "type": "object", + "properties": { + "tenant_user": { + "description": "TenantUser 租户成员关系记录。", + "allOf": [ + { + "$ref": "#/definitions/models.TenantUser" + } + ] + } + } + }, + "dto.AdminTenantUserRoleUpdateForm": { + "type": "object", + "properties": { + "role": { + "description": "Role 角色:member/tenant_admin。", + "type": "string" + } + } + }, "dto.AdminTopupForm": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index d816b6d..6100227 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -797,6 +797,154 @@ } } }, + "/t/{tenantCode}/v1/admin/users": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "成员列表(租户管理)", + "parameters": [ + { + "type": "string", + "description": "Tenant Code", + "name": "tenantCode", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page is 1-based page index; values \u003c= 0 are normalized to 1.", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "UserID 按用户ID过滤(可选)。", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "$ref": "#/definitions/models.TenantUser" + } + } + } + ] + } + } + } + } + }, + "/t/{tenantCode}/v1/admin/users/{userID}/join": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "添加租户成员(租户管理)", + "parameters": [ + { + "type": "string", + "description": "Tenant Code", + "name": "tenantCode", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "UserID", + "name": "userID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.AdminTenantUserJoinResponse" + } + } + } + } + }, + "/t/{tenantCode}/v1/admin/users/{userID}/role": { + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "设置成员角色(租户管理)", + "parameters": [ + { + "type": "string", + "description": "Tenant Code", + "name": "tenantCode", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "UserID", + "name": "userID", + "in": "path", + "required": true + }, + { + "description": "Form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AdminTenantUserRoleUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.AdminTenantUserJoinResponse" + } + } + } + } + }, "/t/{tenantCode}/v1/admin/users/{userID}/topup": { "post": { "consumes": [ @@ -1547,6 +1695,28 @@ } } }, + "dto.AdminTenantUserJoinResponse": { + "type": "object", + "properties": { + "tenant_user": { + "description": "TenantUser 租户成员关系记录。", + "allOf": [ + { + "$ref": "#/definitions/models.TenantUser" + } + ] + } + } + }, + "dto.AdminTenantUserRoleUpdateForm": { + "type": "object", + "properties": { + "role": { + "description": "Role 角色:member/tenant_admin。", + "type": "string" + } + } + }, "dto.AdminTopupForm": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 6871cee..f7e4276 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -185,6 +185,19 @@ definitions: 退款原因:建议必填(由业务侧校验);用于审计与追责。 type: string type: object + dto.AdminTenantUserJoinResponse: + properties: + tenant_user: + allOf: + - $ref: '#/definitions/models.TenantUser' + description: TenantUser 租户成员关系记录。 + type: object + dto.AdminTenantUserRoleUpdateForm: + properties: + role: + description: Role 角色:member/tenant_admin。 + type: string + type: object dto.AdminTopupForm: properties: amount: @@ -1511,6 +1524,102 @@ paths: summary: 订单退款(租户管理) tags: - Tenant + /t/{tenantCode}/v1/admin/users: + get: + consumes: + - application/json + parameters: + - description: Tenant Code + in: path + name: tenantCode + required: true + type: string + - description: Limit is page size; only values in {10,20,50,100} are accepted + (otherwise defaults to 10). + in: query + name: limit + type: integer + - description: Page is 1-based page index; values <= 0 are normalized to 1. + in: query + name: page + type: integer + - description: UserID 按用户ID过滤(可选)。 + in: query + name: user_id + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + $ref: '#/definitions/models.TenantUser' + type: object + summary: 成员列表(租户管理) + tags: + - Tenant + /t/{tenantCode}/v1/admin/users/{userID}/join: + post: + consumes: + - application/json + parameters: + - description: Tenant Code + in: path + name: tenantCode + required: true + type: string + - description: UserID + format: int64 + in: path + name: userID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.AdminTenantUserJoinResponse' + summary: 添加租户成员(租户管理) + tags: + - Tenant + /t/{tenantCode}/v1/admin/users/{userID}/role: + patch: + consumes: + - application/json + parameters: + - description: Tenant Code + in: path + name: tenantCode + required: true + type: string + - description: UserID + format: int64 + in: path + name: userID + required: true + type: integer + - description: Form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.AdminTenantUserRoleUpdateForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.AdminTenantUserJoinResponse' + summary: 设置成员角色(租户管理) + tags: + - Tenant /t/{tenantCode}/v1/admin/users/{userID}/topup: post: consumes: diff --git a/backend/tests/tenant.http b/backend/tests/tenant.http index 05ab1df..b8a6b11 100644 --- a/backend/tests/tenant.http +++ b/backend/tests/tenant.http @@ -151,3 +151,23 @@ Authorization: Bearer {{ token }} "reason": "联调充值", "idempotency_key": "topup-{{ topupUserID }}-001" } + +### Tenant Admin - Join a user to tenant (add member) +@joinUserID = 3 +POST {{ host }}/t/{{ tenantCode }}/v1/admin/users/{{ joinUserID }}/join +Content-Type: application/json +Authorization: Bearer {{ token }} + +### Tenant Admin - Set member role +PATCH {{ host }}/t/{{ tenantCode }}/v1/admin/users/{{ joinUserID }}/role +Content-Type: application/json +Authorization: Bearer {{ token }} + +{ + "role": "tenant_admin" +} + +### Tenant Admin - Tenant members list +GET {{ host }}/t/{{ tenantCode }}/v1/admin/users?page=1&limit=20 +Content-Type: application/json +Authorization: Bearer {{ token }}