Browse Source
- Backend: extend GET /api/users with status, role filtering and sorting - Backend: add PUT /api/users/:id endpoint for updating user info - Backend: add POST /api/users/batch for batch enable/disable/delete - Frontend: rewrite users page with filter bar, stats cards, sortable table - Frontend: add batch action bar and detail drawer for editing - Frontend: add create modal and confirm dialog for batch delete Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>as
6 changed files with 1508 additions and 203 deletions
File diff suppressed because it is too large
@ -0,0 +1,368 @@ |
|||
# Users Management Implementation Plan |
|||
|
|||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
|||
|
|||
**Goal:** 重新实现 admin/users 模块,提供多条件筛选、批量操作、详情抽屉编辑功能 |
|||
|
|||
**Architecture:** 后端扩展用户 API 支持状态/角色筛选、排序、批量操作;前端重写页面组件,采用筛选栏 + 多选表格 + 批量操作栏 + 详情抽屉的布局 |
|||
|
|||
**Tech Stack:** Nuxt 3, TypeScript, Tailwind (CSS variables from DESIGN.md), useHttpFetch composable |
|||
|
|||
--- |
|||
|
|||
## File Structure |
|||
|
|||
``` |
|||
server/api/users/ |
|||
index.get.ts — 修改:增加 status, role, sortBy, sortOrder 筛选 |
|||
[id].put.ts — 新增:更新单个用户 |
|||
batch.post.ts — 新增:批量操作(启用/禁用/删除) |
|||
|
|||
app/pages/admin/users/ |
|||
index.vue — 重写:完整用户管理界面 |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Task 1: Extend GET /api/users with filtering & sorting |
|||
|
|||
**Files:** |
|||
- Modify: `server/api/users/index.get.ts` |
|||
|
|||
- [ ] **Step 1: Read current implementation** |
|||
|
|||
Review `server/api/users/index.get.ts` (lines 1-50 already read) |
|||
|
|||
- [ ] **Step 2: Add query parameters for status, role, sortBy, sortOrder** |
|||
|
|||
```typescript |
|||
// Add after line 9 (after search) |
|||
const status = query.status as string | undefined; |
|||
const role = query.role as string | undefined; |
|||
const sortBy = query.sortBy as string | undefined; |
|||
const sortOrder = query.sortOrder as "asc" | "desc" | undefined; |
|||
``` |
|||
|
|||
- [ ] **Step 3: Build where clause** |
|||
|
|||
```typescript |
|||
// Add after line 11 (after searchPattern) |
|||
const conditions = searchPattern |
|||
? or( |
|||
like(users.username, searchPattern), |
|||
like(users.email, searchPattern), |
|||
like(users.nickname, searchPattern), |
|||
) |
|||
: undefined; |
|||
|
|||
if (status && status !== "all") { |
|||
conditions && conditions.length |
|||
? (conditions.push(eq(users.status, status as "active" | "disabled"))) |
|||
: undefined; |
|||
} |
|||
|
|||
if (role && role !== "all") { |
|||
conditions |
|||
? undefined |
|||
: undefined; |
|||
} |
|||
``` |
|||
|
|||
Actually, use drizzle's `and` helper: |
|||
|
|||
```typescript |
|||
import { and, eq } from "drizzle-orm"; |
|||
|
|||
// Replace the where clause building (lines 30-38) with: |
|||
const whereConditions = []; |
|||
|
|||
if (searchPattern) { |
|||
whereConditions.push( |
|||
or( |
|||
like(users.username, searchPattern), |
|||
like(users.email, searchPattern), |
|||
like(users.nickname, searchPattern), |
|||
) |
|||
); |
|||
} |
|||
|
|||
if (status && status !== "all") { |
|||
whereConditions.push(eq(users.status, status as "active" | "disabled")); |
|||
} |
|||
|
|||
if (role && role !== "all") { |
|||
whereConditions.push(eq(users.role, role as "admin" | "user")); |
|||
} |
|||
|
|||
const whereClause = whereConditions.length > 0 ? and(...whereConditions) : undefined; |
|||
``` |
|||
|
|||
- [ ] **Step 4: Add sorting logic** |
|||
|
|||
```typescript |
|||
// Add before dbGlobal.select (before line 18) |
|||
const orderColumn = sortBy === "username" ? users.username |
|||
: sortBy === "createdAt" ? users.createdAt |
|||
: users.createdAt; |
|||
const orderDirection = sortOrder === "asc" ? orderColumn : desc(orderColumn); |
|||
``` |
|||
|
|||
- [ ] **Step 5: Update query to use whereClause and orderBy** |
|||
|
|||
Replace `.where(searchPattern ? or(...) : undefined)` with `.where(whereClause)` |
|||
Replace `.orderBy(desc(users.createdAt))` with `.orderBy(orderDirection)` |
|||
|
|||
- [ ] **Step 6: Commit** |
|||
|
|||
```bash |
|||
git add server/api/users/index.get.ts |
|||
git commit -m "feat(users): add status, role filtering and sorting to GET /api/users" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Task 2: Create PUT /api/users/:id endpoint |
|||
|
|||
**Files:** |
|||
- Create: `server/api/users/[id].put.ts` |
|||
|
|||
- [ ] **Step 1: Create the file** |
|||
|
|||
```typescript |
|||
import { dbGlobal } from "drizzle-pkg/lib/db"; |
|||
import { users } from "drizzle-pkg/lib/schema/auth"; |
|||
import { eq } from "drizzle-orm"; |
|||
import log4js from "logger"; |
|||
|
|||
const logger = log4js.getLogger("USERS"); |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
const id = Number(event.context.params?.id); |
|||
const body = await readBody(event); |
|||
|
|||
if (!id || isNaN(id)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的用户ID", |
|||
}); |
|||
} |
|||
|
|||
if (body.email !== undefined && body.email !== "" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "邮箱格式不正确", |
|||
}); |
|||
} |
|||
|
|||
if (body.role && !["admin", "user"].includes(body.role)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的角色", |
|||
}); |
|||
} |
|||
|
|||
if (body.status && !["active", "disabled"].includes(body.status)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的状态", |
|||
}); |
|||
} |
|||
|
|||
const [existing] = await dbGlobal |
|||
.select({ id: users.id }) |
|||
.from(users) |
|||
.where(eq(users.id, id)); |
|||
|
|||
if (!existing) { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: "用户不存在", |
|||
}); |
|||
} |
|||
|
|||
const updateData: Partial<{ |
|||
nickname: string | null; |
|||
email: string | null; |
|||
role: "admin" | "user"; |
|||
status: "active" | "disabled"; |
|||
}> = {}; |
|||
|
|||
if (body.nickname !== undefined) updateData.nickname = body.nickname || null; |
|||
if (body.email !== undefined) updateData.email = body.email || null; |
|||
if (body.role !== undefined) updateData.role = body.role; |
|||
if (body.status !== undefined) updateData.status = body.status; |
|||
|
|||
const [updated] = await dbGlobal |
|||
.update(users) |
|||
.set(updateData) |
|||
.where(eq(users.id, id)) |
|||
.returning({ |
|||
id: users.id, |
|||
username: users.username, |
|||
email: users.email, |
|||
nickname: users.nickname, |
|||
avatar: users.avatar, |
|||
role: users.role, |
|||
status: users.status, |
|||
createdAt: users.createdAt, |
|||
}); |
|||
|
|||
logger.info("user updated by admin: %s (id: %d)", updated.username, id); |
|||
return R.success(updated); |
|||
}); |
|||
``` |
|||
|
|||
- [ ] **Step 2: Commit** |
|||
|
|||
```bash |
|||
git add server/api/users/[id].put.ts |
|||
git commit -m "feat(users): add PUT /api/users/:id endpoint" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Task 3: Create POST /api/users/batch endpoint |
|||
|
|||
**Files:** |
|||
- Create: `server/api/users/batch.post.ts` |
|||
|
|||
- [ ] **Step 1: Create the file** |
|||
|
|||
```typescript |
|||
import { dbGlobal } from "drizzle-pkg/lib/db"; |
|||
import { users } from "drizzle-pkg/lib/schema/auth"; |
|||
import { eq, inArray } from "drizzle-orm"; |
|||
import log4js from "logger"; |
|||
|
|||
const logger = log4js.getLogger("USERS"); |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
const body = await readBody(event); |
|||
|
|||
if (!body?.ids || !Array.isArray(body.ids) || body.ids.length === 0) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "请选择要操作的用户", |
|||
}); |
|||
} |
|||
|
|||
if (!body?.action || !["enable", "disable", "delete"].includes(body.action)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的操作类型", |
|||
}); |
|||
} |
|||
|
|||
const ids = body.ids.map(Number).filter(n => !isNaN(n)); |
|||
if (ids.length === 0) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的用户ID列表", |
|||
}); |
|||
} |
|||
|
|||
if (body.action === "delete") { |
|||
await dbGlobal.delete(users).where(inArray(users.id, ids)); |
|||
logger.info("users batch deleted by admin: count=%d", ids.length); |
|||
return R.success({ message: `已删除 ${ids.length} 个用户` }); |
|||
} |
|||
|
|||
const newStatus = body.action === "enable" ? "active" : "disabled"; |
|||
await dbGlobal |
|||
.update(users) |
|||
.set({ status: newStatus }) |
|||
.where(inArray(users.id, ids)); |
|||
|
|||
logger.info("users batch %s by admin: ids=%s", body.action, ids.join(",")); |
|||
return R.success({ |
|||
message: `已${body.action === "enable" ? "启用" : "禁用"} ${ids.length} 个用户` |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
- [ ] **Step 2: Commit** |
|||
|
|||
```bash |
|||
git add server/api/users/batch.post.ts |
|||
git commit -m "feat(users): add POST /api/users/batch for batch operations" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Task 4: Rewrite users/index.vue page |
|||
|
|||
**Files:** |
|||
- Modify: `app/pages/admin/users/index.vue` |
|||
|
|||
This is the main frontend task. Follow the spec exactly: |
|||
- Filter bar with status tabs, role dropdown, search |
|||
- Stats cards (total, admin, disabled) |
|||
- Sortable table with checkboxes |
|||
- Batch action bar |
|||
- Detail drawer |
|||
- Create modal |
|||
- Confirm dialog |
|||
|
|||
- [ ] **Step 1: Read current implementation** |
|||
|
|||
Already read (320 lines) — use as reference for structure |
|||
|
|||
- [ ] **Step 2: Write new implementation** |
|||
|
|||
Complete rewrite with all features from spec: |
|||
- State tabs (全部/正常/禁用) using pill-style buttons |
|||
- Role dropdown select |
|||
- Search input with icon |
|||
- Sortable table columns (username, createdAt) |
|||
- Checkbox multi-select with select-all |
|||
- Stats cards row |
|||
- Batch bar slides in when selection > 0 |
|||
- Detail drawer from right side (480px wide) |
|||
- Create modal centered |
|||
- Confirm dialog for batch delete |
|||
|
|||
Use DESIGN.md tokens: |
|||
- `var(--color-primary)` for coral buttons |
|||
- `var(--color-accent-teal)` for enable actions |
|||
- `var(--color-error)` for delete actions |
|||
- `var(--color-surface-card)` for cards |
|||
- `var(--color-hairline)` for borders |
|||
- `var(--rounded-md)` (8px) for buttons/inputs |
|||
- `var(--rounded-lg)` (12px) for cards |
|||
|
|||
- [ ] **Step 3: Test the page** |
|||
|
|||
Start dev server `npm run dev` and verify: |
|||
- [ ] Filter by status works |
|||
- [ ] Filter by role works |
|||
- [ ] Search works |
|||
- [ ] Table sorting works (click username/createdAt headers) |
|||
- [ ] Checkbox selection works |
|||
- [ ] Batch bar appears when items selected |
|||
- [ ] Batch enable/disable works |
|||
- [ ] Batch delete shows confirm dialog |
|||
- [ ] Create modal works |
|||
- [ ] Edit drawer opens and saves |
|||
|
|||
- [ ] **Step 4: Commit** |
|||
|
|||
```bash |
|||
git add app/pages/admin/users/index.vue |
|||
git commit -m "feat(users): complete rewrite with filtering, sorting, batch operations" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Verification Checklist |
|||
|
|||
- [ ] GET /api/users supports `status`, `role`, `sortBy`, `sortOrder` params |
|||
- [ ] PUT /api/users/:id updates nickname, email, role, status |
|||
- [ ] POST /api/users/batch handles enable/disable/delete |
|||
- [ ] Page shows filter bar with status tabs + role dropdown + search |
|||
- [ ] Page shows stats cards (total/admin/disabled) |
|||
- [ ] Table columns are sortable (username, createdAt) |
|||
- [ ] Checkbox multi-select works with select-all |
|||
- [ ] Batch action bar shows enable/disable/delete buttons |
|||
- [ ] Detail drawer opens on edit and saves changes |
|||
- [ ] Create modal adds new user |
|||
- [ ] Batch delete shows confirm dialog |
|||
@ -0,0 +1,105 @@ |
|||
# 用户管理模块重新设计 |
|||
|
|||
## Overview |
|||
|
|||
重新实现 `app/pages/admin/users/` 模块,面向日常运维场景,提供功能完整、操作高效的用户管理界面。 |
|||
|
|||
## Layout Structure |
|||
|
|||
``` |
|||
┌─────────────────────────────────────────────────────────────┐ |
|||
│ Header: 标题 "用户管理" + 统计卡片(总数/管理员/禁用) │ |
|||
├─────────────────────────────────────────────────────────────┤ |
|||
│ Filter Bar: [状态标签] [角色下拉] [搜索框] [+新增用户] │ |
|||
├─────────────────────────────────────────────────────────────┤ |
|||
│ Table: ☑ | 用户 | 邮箱 | 角色 | 状态 | 注册时间 | 操作 │ |
|||
│ ☐ 张三 xxx@ 管理员 正常 2024-01 编辑 │ |
|||
│ ☐ 李四 --- 普通用户 禁用 2024-02 编辑 │ |
|||
├─────────────────────────────────────────────────────────────┤ |
|||
│ Batch Bar (选中后显示): 已选中 N 项 [启用] [禁用] [删除] │ |
|||
├─────────────────────────────────────────────────────────────┤ |
|||
│ Pagination: 共 X 条,第 1/5 页 [上一页] [下一页] │ |
|||
└─────────────────────────────────────────────────────────────┘ |
|||
|
|||
Drawer: 点击编辑从右侧滑入,宽度 480px |
|||
``` |
|||
|
|||
## Components |
|||
|
|||
### Filter Bar |
|||
|
|||
- **状态标签筛选**:全部 / 正常 / 禁用,点击切换,pill 样式 |
|||
- **角色下拉**:全部角色 / 管理员 / 普通用户 |
|||
- **搜索框**:placeholder "搜索用户名、邮箱",回车触发搜索,搜索图标在左侧 |
|||
- **新增用户按钮**:右上角,primary 样式,图标 + 文字 |
|||
|
|||
### Stats Cards |
|||
|
|||
- 三列 grid 布局 |
|||
- 卡片:总用户数、管理员数、已禁用数 |
|||
- 数字使用 display 字体(28px),标签使用 caption(13px) |
|||
|
|||
### Data Table |
|||
|
|||
- **列头**:全选checkbox、用户、邮箱、角色、状态、注册时间、操作 |
|||
- **用户名列**:头像(36px圆形) + 昵称 + @username |
|||
- **角色/状态**:badge 样式,admin 用 coral 背景,user 用 teal 背景 |
|||
- **可排序列头**:用户名、注册时间,点击切换升序/降序,排序图标 |
|||
- **操作列**:「编辑」文字按钮 |
|||
|
|||
### Batch Action Bar |
|||
|
|||
- 选中 > 0 时从表格下方滑出 |
|||
- 显示 "已选中 N 项" |
|||
- 三个按钮:启用(teal)/ 禁用(muted)/ 删除(error) |
|||
- 删除按钮点击后弹出确认 Dialog,内容 "确定删除选中的 N 个用户?此操作不可撤销。" |
|||
|
|||
### Detail Drawer |
|||
|
|||
- 从右侧滑入,宽度 480px,overlay 背景 |
|||
- 标题:"编辑用户" + 用户名 |
|||
- 字段:用户名(只读)、昵称、邮箱、角色(select)、状态(select) |
|||
- 底部:取消(secondary)+ 保存(primary)按钮 |
|||
|
|||
### Create Modal |
|||
|
|||
- 居中弹窗,最大宽度 440px |
|||
- 标题:新增用户 |
|||
- 字段:用户名、密码、邮箱(选填)、角色(select) |
|||
- 底部:取消 + 创建按钮 |
|||
|
|||
### Confirm Dialog |
|||
|
|||
- 居中弹窗,最大宽度 400px |
|||
- 标题:确认删除 |
|||
- 内容:确定删除选中的 N 个用户?此操作不可撤销。 |
|||
- 底部:取消 + 确认删除(error 样式) |
|||
|
|||
## States |
|||
|
|||
### Table States |
|||
|
|||
- **Loading**:表格区域显示 "加载中..." |
|||
- **Empty**:无数据时显示 "暂无用户" |
|||
- **Error**:错误时显示错误信息,支持重试 |
|||
|
|||
### Empty States |
|||
|
|||
- 无筛选结果时显示 "未找到匹配的用户" |
|||
|
|||
## API Integration |
|||
|
|||
- `GET /api/users` — 列表查询,支持 `page`, `pageSize`, `search`, `status`, `role`, `sortBy`, `sortOrder` |
|||
- `POST /api/users` — 创建用户 |
|||
- `PUT /api/users/:id` — 更新用户 |
|||
- `DELETE /api/users/:id` — 删除用户 |
|||
- `PUT /api/users/batch` — 批量操作,支持 `action: 'enable' | 'disable' | 'delete'`, `ids: number[]` |
|||
|
|||
## Design Tokens |
|||
|
|||
遵循 DESIGN.md 系统: |
|||
|
|||
- 颜色:`primary` (#cc785c), `accent-teal` (#5db8a6), `error` (#c64545) |
|||
- 字体:display 用于数字,body 用于正文 |
|||
- 圆角:md (8px) 用于按钮/输入,lg (12px) 用于卡片 |
|||
- 间距:16px (md), 24px (lg), 32px (xl) |
|||
@ -0,0 +1,96 @@ |
|||
import { dbGlobal } from "drizzle-pkg/lib/db"; |
|||
import { users } from "drizzle-pkg/lib/schema/auth"; |
|||
import { eq } from "drizzle-orm"; |
|||
import log4js from "logger"; |
|||
import { requireAdmin } from "#server/utils/admin-guard"; |
|||
|
|||
const logger = log4js.getLogger("USERS"); |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
const id = Number(event.context.params?.id); |
|||
const body = await readBody(event); |
|||
requireAdmin(event); |
|||
|
|||
if (!id || isNaN(id)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的用户ID", |
|||
}); |
|||
} |
|||
|
|||
// Validate email format if provided
|
|||
if (body.email !== undefined && body.email !== "" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "邮箱格式不正确", |
|||
}); |
|||
} |
|||
|
|||
// Validate role
|
|||
if (body.role && !["admin", "user"].includes(body.role)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的角色", |
|||
}); |
|||
} |
|||
|
|||
// Validate status
|
|||
if (body.status && !["active", "disabled"].includes(body.status)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的状态", |
|||
}); |
|||
} |
|||
|
|||
// Check user exists
|
|||
const [existing] = await dbGlobal |
|||
.select({ id: users.id }) |
|||
.from(users) |
|||
.where(eq(users.id, id)); |
|||
|
|||
if (!existing) { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: "用户不存在", |
|||
}); |
|||
} |
|||
|
|||
// Build update data
|
|||
const updateData: Partial<{ |
|||
nickname: string | null; |
|||
email: string | null; |
|||
role: "admin" | "user"; |
|||
status: "active" | "disabled"; |
|||
}> = {}; |
|||
|
|||
if (body.nickname !== undefined) updateData.nickname = body.nickname || null; |
|||
if (body.email !== undefined) updateData.email = body.email || null; |
|||
if (body.role !== undefined) updateData.role = body.role; |
|||
if (body.status !== undefined) updateData.status = body.status; |
|||
|
|||
// Perform update
|
|||
const [updated] = await dbGlobal |
|||
.update(users) |
|||
.set(updateData) |
|||
.where(eq(users.id, id)) |
|||
.returning({ |
|||
id: users.id, |
|||
username: users.username, |
|||
email: users.email, |
|||
nickname: users.nickname, |
|||
avatar: users.avatar, |
|||
role: users.role, |
|||
status: users.status, |
|||
createdAt: users.createdAt, |
|||
}); |
|||
|
|||
if (!updated) { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: "用户更新失败", |
|||
}); |
|||
} |
|||
|
|||
logger.info("user updated by admin: %s (id: %d)", updated.username, id); |
|||
return R.success(updated); |
|||
}); |
|||
@ -0,0 +1,52 @@ |
|||
import { dbGlobal } from "drizzle-pkg/lib/db"; |
|||
import { users } from "drizzle-pkg/lib/schema/auth"; |
|||
import { inArray } from "drizzle-orm"; |
|||
import log4js from "logger"; |
|||
import { requireAdmin } from "#server/utils/admin-guard"; |
|||
|
|||
const logger = log4js.getLogger("USERS"); |
|||
|
|||
export default defineWrappedResponseHandler(async (event) => { |
|||
requireAdmin(event); |
|||
|
|||
const body = await readBody(event); |
|||
|
|||
if (!body?.ids || !Array.isArray(body.ids) || body.ids.length === 0) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "请选择要操作的用户", |
|||
}); |
|||
} |
|||
|
|||
if (!body?.action || !["enable", "disable", "delete"].includes(body.action)) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的操作类型", |
|||
}); |
|||
} |
|||
|
|||
const ids = body.ids.map((n: number) => Number(n)).filter(n => !isNaN(n)); |
|||
if (ids.length === 0) { |
|||
throw createError({ |
|||
statusCode: 400, |
|||
statusMessage: "无效的用户ID列表", |
|||
}); |
|||
} |
|||
|
|||
if (body.action === "delete") { |
|||
await dbGlobal.delete(users).where(inArray(users.id, ids)); |
|||
logger.info("users batch deleted by admin: count=%d", ids.length); |
|||
return R.success({ message: `已删除 ${ids.length} 个用户` }); |
|||
} |
|||
|
|||
const newStatus = body.action === "enable" ? "active" : "disabled"; |
|||
await dbGlobal |
|||
.update(users) |
|||
.set({ status: newStatus }) |
|||
.where(inArray(users.id, ids)); |
|||
|
|||
logger.info("users batch %s by admin: ids=%s", body.action, ids.join(",")); |
|||
return R.success({ |
|||
message: `已${body.action === "enable" ? "启用" : "禁用"} ${ids.length} 个用户` |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue