From fe9601baf4411aa2b9870273cd417457243c125f Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 17 Dec 2025 16:11:30 +0800 Subject: [PATCH] feat: add status filter --- backend/app/http/super/dto/tenant.go | 4 +- backend/app/services/tenant.go | 4 + backend/docs/dev/api.md | 138 ++++++++++++++++++ frontend/superadmin/dist/index.html | 2 +- .../superadmin/src/service/TenantService.js | 4 +- .../src/views/superadmin/Tenants.vue | 42 ++++-- 6 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 backend/docs/dev/api.md diff --git a/backend/app/http/super/dto/tenant.go b/backend/app/http/super/dto/tenant.go index ff03d1b..1122c70 100644 --- a/backend/app/http/super/dto/tenant.go +++ b/backend/app/http/super/dto/tenant.go @@ -13,8 +13,8 @@ type TenantFilter struct { requests.Pagination requests.SortQueryFilter - Name *string `json:"name,omitempty" query:"name"` - Status *string `json:"status,omitempty" query:"status"` + Name *string `json:"name,omitempty" query:"name"` + Status *consts.TenantStatus `json:"status,omitempty" query:"status"` } type TenantItem struct { diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index 2e4b235..59a7228 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -82,6 +82,10 @@ func (t *tenant) Pager(ctx context.Context, filter *dto.TenantFilter) (*requests conds = append(conds, tbl.Name.Like(database.WrapLike(*filter.Name))) } + if filter.Status != nil { + conds = append(conds, tbl.Status.Eq(*filter.Status)) + } + filter.Pagination.Format() mm, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit)) if err != nil { diff --git a/backend/docs/dev/api.md b/backend/docs/dev/api.md new file mode 100644 index 0000000..7574e5d --- /dev/null +++ b/backend/docs/dev/api.md @@ -0,0 +1,138 @@ +# Backend 新增 HTTP 接口流程(Go + Fiber + atomctl) + +本文档描述在本仓库 `backend/` 中新增一个 HTTP 接口(例如 `/super/v1/...`)的标准流程,包含路由生成、Swagger 文档生成、参数绑定与测试验证。 + +## 相关目录 + +- 路由与 Controller:`backend/app/http/**` + - Super 端示例:`backend/app/http/super/*.go` + - 生成路由:`backend/app/http/super/routes.gen.go`(自动生成,勿手改) +- Swagger 文档: + - `backend/docs/swagger.yaml` + - `backend/docs/swagger.json` + - `backend/docs/docs.go`(自动生成,勿手改) +- 本地接口调试示例:`backend/super.http` +- 命令:`backend/Makefile`(`make init`/`make run`/`make test` 等) + +## 增加一个新接口(推荐步骤) + +### 1) 选择模块与 BasePath + +- **Super 管理端**:一般挂在 `/super/v1/...`,代码放在 `backend/app/http/super/`。 +- **租户端**:项目 `main.go` 注解里有 `@BasePath /t/{tenant_code}/v1`,通常租户端接口会以该前缀为基础(具体以现有路由模块为准)。 + +先决定: + +- `METHOD`:GET/POST/PATCH/DELETE… +- `PATH`:例如 `/super/v1/users/{id}` 或 `/super/v1/users/:id` +- 鉴权:是否需要 token/权限(跟随模块现有中间件) +- 请求:path/query/body +- 响应:结构体 / 分页 / KV 列表等 + +### 2) 定义请求 DTO(Filter/Form)与响应 DTO + +Super 模块常见模式: + +- 列表分页:`dto.*Filter` / `dto.*PageFilter` + 返回 `requests.Pager` +- 更新类接口:`dto.*UpdateForm`(body) +- KV 枚举列表:返回 `[]requests.KV` + +可参考: + +- `backend/app/http/super/dto/*` +- `backend/app/http/super/user.go`、`backend/app/http/super/tenant.go` + +### 3) 编写 Controller 方法(带 Swagger 注解 + Bind) + +在对应模块的 `*.go` 中新增方法,保持和现有风格一致: + +- Controller struct 上保留 `// @provider` +- 方法上补齐 swagger 注解:`@Summary/@Tags/@Param/@Success/@Router` +- 使用 `@Bind` 约定参数来源(path/query/body) + +示例(参考 `backend/app/http/super/user.go`): + +```go +// @Summary 用户状态列表 +// @Tags Super +// @Accept json +// @Produce json +// @Success 200 {array} requests.KV +// @Router /super/v1/users/statuses [get] +func (*user) statusList(ctx fiber.Ctx) ([]requests.KV, error) { ... } +``` + +注意: + +- `@Router` 的路径写法通常与 Fiber 路由一致(例如 `:userID`)。 +- 参数绑定会驱动路由代码生成(见下一步)。 + +### 4) 连接业务层(Service) + +Controller 内尽量只做: + +- 参数解析/校验 +- 调用 `services.*` 完成业务 +- 返回结果或 error + +业务逻辑集中放在 `backend/app/services`(结合现有实现),涉及 DB 的部分走现有模型/仓储层(依项目既有组织)。 + +### 5) 生成路由代码(routes.gen.go) + +本项目路由是由 `atomctl` 自动生成的。 + +常用命令(在 `backend/` 下执行): + +- 仅生成路由:`atomctl gen route` +- 全量初始化/更新(含 swagger/enum/route/service 等):`make init` + +生成完成后检查: + +- `backend/app/http//routes.gen.go` 是否出现新路由 +- 路由 METHOD/PATH、参数绑定是否正确 + +### 6) 生成 Swagger 文档(swagger.yaml/json/docs.go) + +Swagger 也是由工具生成并落盘到 `backend/docs/`: + +- `atomctl swag init` +- 或直接 `make init`(会包含该步骤) + +生成完成后检查: + +- `backend/docs/swagger.yaml`、`backend/docs/swagger.json` 是否包含新接口 +- `backend/docs/docs.go` 是否同步更新 + +### 7) 本地验证 + +启动: + +- `make run`(会先 `make build`) + +验证方式: + +- 使用 `backend/super.http` 增加/执行请求 +- 或用 curl/Swagger UI(若项目已暴露 swagger 页面) + +### 8) 增加测试(建议) + +优先参考现有 e2e 测试: + +- `backend/tests/e2e/*` + +覆盖至少: + +- 正常请求返回 +- 参数缺失/非法 +- 权限不足/未登录(如该接口需要鉴权) + +运行: + +- `make test` + +## 常见注意事项 + +- 不要手改 `*.gen.go`、`backend/docs/docs.go`:它们由 `atomctl` 生成。 +- 确认查询参数命名与 swagger 一致(例如 `page/limit/asc/desc/status`),前端会按 swagger 拼 query。 +- 路由路径参数请在 `@Router` 与函数签名/`@Bind` 里保持一致(例如 `tenantID`、`userID`)。 + diff --git a/frontend/superadmin/dist/index.html b/frontend/superadmin/dist/index.html index d4242fd..5842c3d 100644 --- a/frontend/superadmin/dist/index.html +++ b/frontend/superadmin/dist/index.html @@ -7,7 +7,7 @@ Sakai Vue - + diff --git a/frontend/superadmin/src/service/TenantService.js b/frontend/superadmin/src/service/TenantService.js index c2fb3e0..2ac3ca8 100644 --- a/frontend/superadmin/src/service/TenantService.js +++ b/frontend/superadmin/src/service/TenantService.js @@ -7,8 +7,8 @@ function normalizeItems(items) { } export const TenantService = { - async listTenants({ page, limit, name, code, sortField, sortOrder } = {}) { - const query = { page, limit, name, code }; + async listTenants({ page, limit, name, code, status, sortField, sortOrder } = {}) { + const query = { page, limit, name, code, status }; if (sortField && sortOrder) { if (sortOrder === 1) query.asc = sortField; if (sortOrder === -1) query.desc = sortField; diff --git a/frontend/superadmin/src/views/superadmin/Tenants.vue b/frontend/superadmin/src/views/superadmin/Tenants.vue index ef50228..1fbb7bc 100644 --- a/frontend/superadmin/src/views/superadmin/Tenants.vue +++ b/frontend/superadmin/src/views/superadmin/Tenants.vue @@ -15,6 +15,7 @@ const page = ref(1); const rows = ref(10); const keyword = ref(''); +const status = ref(''); const sortField = ref('id'); const sortOrder = ref(-1); @@ -65,6 +66,7 @@ async function loadTenants() { limit: rows.value, name: keyword.value, code: keyword.value, + status: status.value, sortField: sortField.value, sortOrder: sortOrder.value }); @@ -89,6 +91,7 @@ function onSearch() { function onReset() { keyword.value = ''; + status.value = ''; sortField.value = 'id'; sortOrder.value = -1; page.value = 1; @@ -127,20 +130,26 @@ function openRenewDialog(item) { } const tenantStatusDialogVisible = ref(false); -const tenantStatusLoading = ref(false); +const tenantStatusOptionsLoading = ref(false); +const tenantStatusUpdating = 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); + tenantStatusOptionsLoading.value = true; + try { + const list = await TenantService.getTenantStatuses(); + tenantStatusOptions.value = (list || []) + .map((kv) => ({ + label: kv?.value ?? kv?.key ?? '-', + value: kv?.key ?? '' + })) + .filter((item) => item.value); + } finally { + tenantStatusOptionsLoading.value = false; + } } async function openTenantStatusDialog(tenant) { @@ -148,13 +157,10 @@ async function openTenantStatusDialog(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; } } @@ -162,7 +168,7 @@ async function confirmUpdateTenantStatus() { const tenantID = tenantStatusTenant.value?.id; if (!tenantID || !tenantStatusValue.value) return; - tenantStatusLoading.value = true; + tenantStatusUpdating.value = true; try { await TenantService.updateTenantStatus({ tenantID, status: tenantStatusValue.value }); toast.add({ severity: 'success', summary: '更新成功', detail: `TenantID: ${tenantID}`, life: 3000 }); @@ -171,7 +177,7 @@ async function confirmUpdateTenantStatus() { } catch (error) { toast.add({ severity: 'error', summary: '更新失败', detail: error?.message || '无法更新租户状态', life: 4000 }); } finally { - tenantStatusLoading.value = false; + tenantStatusUpdating.value = false; } } @@ -194,6 +200,7 @@ async function confirmRenew() { onMounted(() => { loadTenants(); + ensureTenantStatusOptionsLoaded().catch(() => {}); }); @@ -213,6 +220,9 @@ onMounted(() => { + + +