diff --git a/backend/app/http/super/dto/user.go b/backend/app/http/super/dto/user.go index cef980d..71fed30 100644 --- a/backend/app/http/super/dto/user.go +++ b/backend/app/http/super/dto/user.go @@ -3,6 +3,7 @@ package dto import ( "quyun/v2/app/requests" "quyun/v2/database/models" + "quyun/v2/pkg/consts" ) type UserPageFilter struct { @@ -18,3 +19,7 @@ type UserItem struct { StatusDescription string `json:"status_description,omitempty"` } + +type UserStatusUpdateForm struct { + Status consts.UserStatus `json:"status" validate:"required,oneof=normal disabled"` +} diff --git a/backend/app/http/super/routes.gen.go b/backend/app/http/super/routes.gen.go index 99dabb6..3325792 100644 --- a/backend/app/http/super/routes.gen.go +++ b/backend/app/http/super/routes.gen.go @@ -52,6 +52,10 @@ func (r *Routes) Register(router fiber.Router) { r.tenant.list, Query[dto.TenantFilter]("filter"), )) + r.log.Debugf("Registering route: Get /super/v1/tenants/statuses -> tenant.statusList") + router.Get("/super/v1/tenants/statuses", DataFunc0( + r.tenant.statusList, + )) r.log.Debugf("Registering route: Patch /super/v1/tenants/:tenantID -> tenant.updateExpire") router.Patch("/super/v1/tenants/:tenantID", Func2( r.tenant.updateExpire, @@ -70,6 +74,16 @@ func (r *Routes) Register(router fiber.Router) { r.user.list, Query[dto.UserPageFilter]("filter"), )) + r.log.Debugf("Registering route: Get /super/v1/users/statuses -> user.statusList") + router.Get("/super/v1/users/statuses", DataFunc0( + r.user.statusList, + )) + r.log.Debugf("Registering route: Patch /super/v1/users/:userID/status -> user.updateStatus") + router.Patch("/super/v1/users/:userID/status", Func2( + r.user.updateStatus, + PathParam[int64]("userID"), + Body[dto.UserStatusUpdateForm]("form"), + )) r.log.Info("Successfully registered all routes") } diff --git a/backend/app/http/super/tenant.go b/backend/app/http/super/tenant.go index 528b467..3504788 100644 --- a/backend/app/http/super/tenant.go +++ b/backend/app/http/super/tenant.go @@ -5,8 +5,10 @@ import ( "quyun/v2/app/http/super/dto" "quyun/v2/app/requests" "quyun/v2/app/services" + "quyun/v2/pkg/consts" "github.com/gofiber/fiber/v3" + "github.com/samber/lo" ) // @provider @@ -63,3 +65,20 @@ func (*tenant) updateExpire(ctx fiber.Ctx, tenantID int64, form *dto.TenantExpir func (*tenant) updateStatus(ctx fiber.Ctx, tenantID int64, form *dto.TenantStatusUpdateForm) error { return services.Tenant.UpdateStatus(ctx, tenantID, form.Status) } + +// statusList +// +// @Summary 租户状态列表 +// @Tags Super +// @Accept json +// @Produce json +// @Success 200 {array} requests.KV +// +// @Router /super/v1/tenants/statuses [get] +// @Bind userID path +// @Bind form body +func (*tenant) statusList(ctx fiber.Ctx) ([]requests.KV, error) { + return lo.Map(consts.TenantStatusValues(), func(item consts.TenantStatus, _ int) requests.KV { + return requests.NewKV(item.String(), item.Description()) + }), nil +} diff --git a/backend/app/http/super/user.go b/backend/app/http/super/user.go index d351655..004d6eb 100644 --- a/backend/app/http/super/user.go +++ b/backend/app/http/super/user.go @@ -5,8 +5,10 @@ import ( "quyun/v2/app/requests" "quyun/v2/app/services" _ "quyun/v2/database/models" + "quyun/v2/pkg/consts" "github.com/gofiber/fiber/v3" + "github.com/samber/lo" ) // @provider @@ -26,3 +28,36 @@ type user struct{} func (*user) list(ctx fiber.Ctx, filter *dto.UserPageFilter) (*requests.Pager, error) { return services.User.Page(ctx, filter) } + +// updateStatus +// +// @Summary 更新用户状态 +// @Tags Super +// @Accept json +// @Produce json +// @Param userID path int64 true "UserID" +// @Param form body dto.UserStatusUpdateForm true "Form" +// +// @Router /super/v1/users/:userID/status [patch] +// @Bind userID path +// @Bind form body +func (*user) updateStatus(ctx fiber.Ctx, userID int64, form *dto.UserStatusUpdateForm) error { + return services.User.UpdateStatus(ctx, userID, form.Status) +} + +// statusList +// +// @Summary 用户状态列表 +// @Tags Super +// @Accept json +// @Produce json +// @Success 200 {array} requests.KV +// +// @Router /super/v1/users/statuses [get] +// @Bind userID path +// @Bind form body +func (*user) statusList(ctx fiber.Ctx) ([]requests.KV, error) { + return lo.Map(consts.UserStatusValues(), func(item consts.UserStatus, _ int) requests.KV { + return requests.NewKV(item.String(), item.Description()) + }), nil +} diff --git a/backend/app/requests/label.go b/backend/app/requests/label.go new file mode 100644 index 0000000..0390afa --- /dev/null +++ b/backend/app/requests/label.go @@ -0,0 +1,10 @@ +package requests + +type KV struct { + Key string `json:"key,omitempty"` + Value any `json:"value,omitempty"` +} + +func NewKV(k string, v any) KV { + return KV{Key: k, Value: v} +} diff --git a/backend/app/services/user.go b/backend/app/services/user.go index dba4e54..e436005 100644 --- a/backend/app/services/user.go +++ b/backend/app/services/user.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/samber/lo" + "github.com/sirupsen/logrus" "go.ipao.vip/gen" ) @@ -88,3 +89,21 @@ func (t *user) Page(ctx context.Context, filter *dto.UserPageFilter) (*requests. Items: items, }, nil } + +// UpdateStatus +func (t *user) UpdateStatus(ctx context.Context, userID int64, status consts.UserStatus) error { + logrus.WithField("user_id", userID).WithField("status", status).Info("update user status") + + m, err := t.FindByID(ctx, userID) + if err != nil { + return err + } + + m.Status = status + _, err = m.Update(ctx) + if err != nil { + return err + } + + return nil +} diff --git a/backend/docs/docs.go b/backend/docs/docs.go index c3facea..d2b47ff 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -117,6 +117,31 @@ const docTemplate = `{ } } }, + "/super/v1/tenants/statuses": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Super" + ], + "summary": "租户状态列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/requests.KV" + } + } + } + } + } + }, "/super/v1/tenants/{tenantID}": { "patch": { "consumes": [ @@ -250,6 +275,65 @@ const docTemplate = `{ } } } + }, + "/super/v1/users/statuses": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Super" + ], + "summary": "用户状态列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/requests.KV" + } + } + } + } + } + }, + "/super/v1/users/{userID}/status": { + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Super" + ], + "summary": "更新用户状态", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "UserID", + "name": "userID", + "in": "path", + "required": true + }, + { + "description": "Form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UserStatusUpdateForm" + } + } + ], + "responses": {} + } } }, "definitions": { @@ -452,6 +536,25 @@ const docTemplate = `{ } } }, + "dto.UserStatusUpdateForm": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "enum": [ + "normal", + "disabled" + ], + "allOf": [ + { + "$ref": "#/definitions/consts.UserStatus" + } + ] + } + } + }, "gorm.DeletedAt": { "type": "object", "properties": { @@ -558,6 +661,15 @@ const docTemplate = `{ } } }, + "requests.KV": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": {} + } + }, "requests.Pager": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 54c6e46..01f4ab9 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -111,6 +111,31 @@ } } }, + "/super/v1/tenants/statuses": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Super" + ], + "summary": "租户状态列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/requests.KV" + } + } + } + } + } + }, "/super/v1/tenants/{tenantID}": { "patch": { "consumes": [ @@ -244,6 +269,65 @@ } } } + }, + "/super/v1/users/statuses": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Super" + ], + "summary": "用户状态列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/requests.KV" + } + } + } + } + } + }, + "/super/v1/users/{userID}/status": { + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Super" + ], + "summary": "更新用户状态", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "UserID", + "name": "userID", + "in": "path", + "required": true + }, + { + "description": "Form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UserStatusUpdateForm" + } + } + ], + "responses": {} + } } }, "definitions": { @@ -446,6 +530,25 @@ } } }, + "dto.UserStatusUpdateForm": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "enum": [ + "normal", + "disabled" + ], + "allOf": [ + { + "$ref": "#/definitions/consts.UserStatus" + } + ] + } + } + }, "gorm.DeletedAt": { "type": "object", "properties": { @@ -552,6 +655,15 @@ } } }, + "requests.KV": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": {} + } + }, "requests.Pager": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index b1cea63..d0995be 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -134,6 +134,17 @@ definitions: verified_at: type: string type: object + dto.UserStatusUpdateForm: + properties: + status: + allOf: + - $ref: '#/definitions/consts.UserStatus' + enum: + - normal + - disabled + required: + - status + type: object gorm.DeletedAt: properties: time: @@ -204,6 +215,12 @@ definitions: verified_at: type: string type: object + requests.KV: + properties: + key: + type: string + value: {} + type: object requests.Pager: properties: items: {} @@ -332,6 +349,22 @@ paths: summary: 更新租户状态 tags: - Super + /super/v1/tenants/statuses: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/requests.KV' + type: array + summary: 租户状态列表 + tags: + - Super /super/v1/users: get: consumes: @@ -370,6 +403,45 @@ paths: summary: 租户列表 tags: - Super + /super/v1/users/{userID}/status: + patch: + consumes: + - application/json + parameters: + - description: UserID + format: int64 + in: path + name: userID + required: true + type: integer + - description: Form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.UserStatusUpdateForm' + produces: + - application/json + responses: {} + summary: 更新用户状态 + tags: + - Super + /super/v1/users/statuses: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/requests.KV' + type: array + summary: 用户状态列表 + tags: + - Super securityDefinitions: BasicAuth: type: basic diff --git a/frontend/superadmin/dist/index.html b/frontend/superadmin/dist/index.html index 6459065..25fa291 100644 --- a/frontend/superadmin/dist/index.html +++ b/frontend/superadmin/dist/index.html @@ -7,8 +7,8 @@ Sakai Vue - - + + diff --git a/frontend/superadmin/src/service/TenantService.js b/frontend/superadmin/src/service/TenantService.js index 95e59d9..c2fb3e0 100644 --- a/frontend/superadmin/src/service/TenantService.js +++ b/frontend/superadmin/src/service/TenantService.js @@ -27,5 +27,15 @@ export const TenantService = { method: 'PATCH', body: { duration } }); + }, + async getTenantStatuses() { + const data = await requestJson('/super/v1/tenants/statuses'); + return Array.isArray(data) ? data : []; + }, + async updateTenantStatus({ tenantID, status }) { + return requestJson(`/super/v1/tenants/${tenantID}/status`, { + method: 'PATCH', + body: { status } + }); } }; diff --git a/frontend/superadmin/src/service/UserService.js b/frontend/superadmin/src/service/UserService.js index 67c73a1..192d765 100644 --- a/frontend/superadmin/src/service/UserService.js +++ b/frontend/superadmin/src/service/UserService.js @@ -21,5 +21,15 @@ export const UserService = { total: data?.total ?? 0, items: normalizeItems(data?.items) }; + }, + async getUserStatuses() { + const data = await requestJson('/super/v1/users/statuses'); + return Array.isArray(data) ? data : []; + }, + async updateUserStatus({ userID, status }) { + return requestJson(`/super/v1/users/${userID}/status`, { + method: 'PATCH', + body: { status } + }); } }; diff --git a/frontend/superadmin/src/views/superadmin/Tenants.vue b/frontend/superadmin/src/views/superadmin/Tenants.vue index 5d0b2f5..672916c 100644 --- a/frontend/superadmin/src/views/superadmin/Tenants.vue +++ b/frontend/superadmin/src/views/superadmin/Tenants.vue @@ -124,6 +124,55 @@ function openRenewDialog(item) { renewDialogVisible.value = true; } +const tenantStatusDialogVisible = ref(false); +const tenantStatusLoading = ref(false); +const tenantStatusOptions = ref([]); +const tenantStatusTenant = ref(null); +const tenantStatusValue = ref(null); + +async function ensureTenantStatusOptionsLoaded() { + if (tenantStatusOptions.value.length > 0) return; + const list = await TenantService.getTenantStatuses(); + tenantStatusOptions.value = (list || []) + .map((kv) => ({ + label: kv?.value ?? kv?.key ?? '-', + value: kv?.key ?? '' + })) + .filter((item) => item.value); +} + +async function openTenantStatusDialog(tenant) { + tenantStatusTenant.value = tenant; + tenantStatusValue.value = tenant?.status ?? null; + tenantStatusDialogVisible.value = true; + + tenantStatusLoading.value = true; + try { + await ensureTenantStatusOptionsLoaded(); + } catch (error) { + toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载租户状态列表', life: 4000 }); + } finally { + tenantStatusLoading.value = false; + } +} + +async function confirmUpdateTenantStatus() { + const tenantID = tenantStatusTenant.value?.id; + if (!tenantID || !tenantStatusValue.value) return; + + tenantStatusLoading.value = true; + try { + await TenantService.updateTenantStatus({ tenantID, status: tenantStatusValue.value }); + toast.add({ severity: 'success', summary: '更新成功', detail: `TenantID: ${tenantID}`, life: 3000 }); + tenantStatusDialogVisible.value = false; + await loadTenants(); + } catch (error) { + toast.add({ severity: 'error', summary: '更新失败', detail: error?.message || '无法更新租户状态', life: 4000 }); + } finally { + tenantStatusLoading.value = false; + } +} + async function confirmRenew() { const tenantID = renewTenant.value?.id; if (!tenantID) return; @@ -192,7 +241,7 @@ onMounted(() => { @@ -240,5 +289,24 @@ onMounted(() => {