From aeaf2a1ad9c853a72b80b92f159caba0da6c0482 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Wed, 27 May 2026 14:16:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=88=9B=E5=BB=BA=E3=80=81=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=92=8C=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 3 +- app/pages/admin/users/index.vue | 807 +++++++++++++++++++++++++++++++++++++++- packages/drizzle-pkg/db.sqlite | Bin 282624 -> 282624 bytes server/api/users/[id].delete.ts | 36 ++ server/api/users/index.get.ts | 50 +++ server/api/users/index.post.ts | 83 +++++ 6 files changed, 975 insertions(+), 4 deletions(-) create mode 100644 server/api/users/[id].delete.ts create mode 100644 server/api/users/index.get.ts create mode 100644 server/api/users/index.post.ts diff --git a/AGENTS.md b/AGENTS.md index 3663118..294ceea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,8 @@ - 安装依赖时,必须写死依赖的版本,禁止任何升级的可能,如存在需要升级的场景,必须手动执行安装新版本命令 - 新建的mono包必须在根目录安装 - 使用nuxt时遇到不清楚的,必须先加载nuxt-remote查询文档再进行分析 +- 能够用toast提示的尽量用toast ## 设计方案 -开发页面与组件时必须参考 @DESIGN.md \ No newline at end of file +开发页面与组件时必须参考 DESIGN.md \ No newline at end of file diff --git a/app/pages/admin/users/index.vue b/app/pages/admin/users/index.vue index 7f7bc23..1cd513d 100644 --- a/app/pages/admin/users/index.vue +++ b/app/pages/admin/users/index.vue @@ -1,5 +1,806 @@ + + \ No newline at end of file + + + diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index 6d26200ea9a403f21e4f9b9fb220ea49eefdece7..1df6328e06c77df0c8da60b185ee0be728de11b4 100644 GIT binary patch delta 137 zcmZozAlR@#aDp^r-b5K^#=MOQ3-#r>?=Wz4<%{rl@=Np0=hNcd!|TLzg2#tDk?T2E zK3D!`L4gD8n;)3}VP;`qV94A2z*><-o__%YH-9|?|0e#+{HOS*@ZaVC%KwUg75^Uo nF8+G{`ptq0@%)qP{S`r4xSE&vw=eN$1Y#y2W}d#ppCtwWKL#qi delta 137 zcmZozAlR@#aDp^r?nD`9#@vkw3-#q07#MiC@L4gD8n;)3}VP?tU<7tcSmzfqxVKW&TtAQ~2-lFW~>m|B8PV{~rD> m{`$>=3i14t>-`m3IGM#6o0s^vFY#vtVkRJFp1#DNB?bW9c`4xl 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; + } +});