- dsada
+
+
+
+
+
+
+
用户总数
+
{{ totalUsers }}
+
+
+
管理员
+
+ {{ userList.filter((u) => u.role === "admin").length }}
+
+
+
+
已禁用
+
+ {{ userList.filter((u) => u.status === "disabled").length }}
+
+
+
+
+
+
+
+
+
+ | 用户 |
+ 邮箱 |
+ 角色 |
+ 状态 |
+ 注册时间 |
+ 操作 |
+
+
+
+
+ | 加载中... |
+
+
+ | 暂无用户 |
+
+
+
+
+
+ ![]()
+
+ {{ u.username.charAt(0).toUpperCase() }}
+
+
+
+ {{ u.nickname || u.username }}
+ @{{ u.username }}
+
+
+ |
+ {{ u.email || "-" }} |
+
+
+ {{ roleBadge(u.role).label }}
+
+ |
+
+
+ {{ statusBadge(u.status).label }}
+
+ |
+ {{ formatDate(u.createdAt) }} |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
+
+
diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite
index 6d26200..1df6328 100644
Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ
diff --git a/server/api/users/[id].delete.ts b/server/api/users/[id].delete.ts
new file mode 100644
index 0000000..e195dc6
--- /dev/null
+++ b/server/api/users/[id].delete.ts
@@ -0,0 +1,36 @@
+import { dbGlobal } from "drizzle-pkg/lib/db";
+import { users, sessions } 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);
+
+ if (!id || isNaN(id)) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: "无效的用户ID",
+ });
+ }
+
+ const [user] = await dbGlobal
+ .select({ id: users.id, username: users.username })
+ .from(users)
+ .where(eq(users.id, id));
+
+ if (!user) {
+ throw createError({
+ statusCode: 404,
+ statusMessage: "用户不存在",
+ });
+ }
+
+ await dbGlobal.delete(sessions).where(eq(sessions.userId, id));
+ await dbGlobal.delete(users).where(eq(users.id, id));
+
+ logger.info("user deleted by admin: %s (id: %d)", user.username, id);
+
+ return R.success({ message: "用户已删除" });
+});
diff --git a/server/api/users/index.get.ts b/server/api/users/index.get.ts
new file mode 100644
index 0000000..063af6c
--- /dev/null
+++ b/server/api/users/index.get.ts
@@ -0,0 +1,50 @@
+import { dbGlobal } from "drizzle-pkg/lib/db";
+import { users } from "drizzle-pkg/lib/schema/auth";
+import { count, desc, like, or } from "drizzle-orm";
+
+export default defineWrappedResponseHandler(async (event) => {
+ const query = getQuery(event);
+ const page = query.page ? Number(query.page) : 1;
+ const pageSize = query.pageSize ? Number(query.pageSize) : 10;
+ const search = query.search as string | undefined;
+
+ const offset = (page - 1) * pageSize;
+ const searchPattern = search ? `%${search}%` : undefined;
+
+ const [totalResult] = await dbGlobal
+ .select({ total: count() })
+ .from(users);
+
+ const list = await dbGlobal
+ .select({
+ id: users.id,
+ username: users.username,
+ email: users.email,
+ nickname: users.nickname,
+ avatar: users.avatar,
+ role: users.role,
+ status: users.status,
+ createdAt: users.createdAt,
+ })
+ .from(users)
+ .where(
+ searchPattern
+ ? or(
+ like(users.username, searchPattern),
+ like(users.email, searchPattern),
+ like(users.nickname, searchPattern),
+ )
+ : undefined,
+ )
+ .orderBy(desc(users.createdAt))
+ .limit(pageSize)
+ .offset(offset);
+
+ return R.success({
+ list,
+ total: totalResult?.total ?? 0,
+ page,
+ pageSize,
+ totalPages: Math.ceil((totalResult?.total ?? 0) / pageSize),
+ });
+});
diff --git a/server/api/users/index.post.ts b/server/api/users/index.post.ts
new file mode 100644
index 0000000..8a667da
--- /dev/null
+++ b/server/api/users/index.post.ts
@@ -0,0 +1,83 @@
+import { hash } from "bcryptjs";
+import { dbGlobal } from "drizzle-pkg/lib/db";
+import { users } from "drizzle-pkg/lib/schema/auth";
+import { isUniqueConflictOnField } from "#server/utils/db-unique-constraint";
+import log4js from "logger";
+
+const logger = log4js.getLogger("USERS");
+
+export default defineWrappedResponseHandler(async (event) => {
+ const body = await readBody(event);
+
+ if (!body?.username || !body?.password) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: "用户名和密码不能为空",
+ });
+ }
+
+ const username = body.username.trim();
+ const password = body.password;
+ const email = body.email?.trim() || undefined;
+ const role = body.role === "admin" ? "admin" : "user";
+
+ if (username.length < 3 || username.length > 20) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: "用户名长度需在 3-20 个字符之间",
+ });
+ }
+
+ if (!/^[a-zA-Z0-9_]+$/.test(username)) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: "用户名只能包含字母、数字和下划线",
+ });
+ }
+
+ if (password.length < 6) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: "密码长度至少 6 位",
+ });
+ }
+
+ if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: "邮箱格式不正确",
+ });
+ }
+
+ const passwordHash = await hash(password, 12);
+
+ try {
+ const [newUser] = await dbGlobal
+ .insert(users)
+ .values({
+ username,
+ password: passwordHash,
+ email: email || null,
+ role,
+ })
+ .returning({
+ id: users.id,
+ username: users.username,
+ email: users.email,
+ role: users.role,
+ nickname: users.nickname,
+ avatar: users.avatar,
+ });
+
+ logger.info("user created by admin: %s (role: %s)", username, role);
+ return R.success(newUser);
+ } catch (err) {
+ if (isUniqueConflictOnField(err, "username")) {
+ throw createError({
+ statusCode: 409,
+ statusMessage: "用户名已存在",
+ });
+ }
+ throw err;
+ }
+});