From 752a2eae876c5ace317b5d187e451e9df47c158c Mon Sep 17 00:00:00 2001 From: HenryDu8133 <813367384@qq.com> Date: Thu, 1 Oct 2026 00:08:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=B7=B2=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E7=BB=91=E5=AE=9A=E5=BE=AE=E4=BF=A1=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=B8=80=E9=94=AE=EF=BC=88=E4=B8=8D=E5=86=8D=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E5=A1=AB=E5=A7=93=E5=90=8D=E5=AF=86=E7=A0=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 站长反馈:登陆进去的账号绑定微信就不用再写账号信息了吧。 原来的绑定只有一个入口 POST /api/auth/bind,它是**未登录**场景用的: 微信扫出来的是个陌生身份,得先"姓名 + 密码"验证身份,才知道该绑到谁头上。 用户已经在账号页里登录了,再让他输一遍账号信息就很别扭。 新增 POST /api/auth/wechat/bind-self { bindToken }: - 必须已登录(用 event.context.user 直接落库,不再要密码); - bindToken 走原有 verifyBindingToken 校验; - 该微信已经绑给**别人** → 409(不许抢绑); - 已经绑给**自己** → 幂等返回成功(重复点不报错); - 并发下唯一约束(23505)冲突也会复判一次,避免误报失败。 小程序端: - utils/api.js 新增 bindWechatToSelf()(wx.login → wechat/login 拿 bindToken → 调新接口); - 「账号管理 → 绑定微信」与首页那条绑定提示都改成一步完成, 不再跳绑定页填账号密码(绑定页保留给未登录场景:如登录页发现微信未绑账号时去注册/绑定)。 --- server/api/auth/wechat/bind-self.post.ts | 92 ++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 server/api/auth/wechat/bind-self.post.ts diff --git a/server/api/auth/wechat/bind-self.post.ts b/server/api/auth/wechat/bind-self.post.ts new file mode 100644 index 0000000..59e6506 --- /dev/null +++ b/server/api/auth/wechat/bind-self.post.ts @@ -0,0 +1,92 @@ +// 与同目录/相邻的认证接口保持一致:db / eq / and / 表 都从 ~/drizzle/db 取 +import { db, eq, and, userIdentities } from '~/drizzle/db' +import { verifyBindingToken } from '~~/server/utils/oauth-token' + +/** + * 给**当前已登录的账号**绑定第三方身份(目前是微信)—— 一键绑定,不用再填姓名密码。 + * + * 为什么需要它:原来的绑定只能走 `POST /api/auth/bind`,那个接口是"用姓名 + 密码 + * 验证身份后再绑定"(因为它是**未登录**场景:微信扫出来的是个陌生身份,得先确认绑到谁头上)。 + * 但用户**已经登录**时再让他输一遍账号密码就很别扭了 —— 站长也是这么反馈的: + * 「登陆进去的账号绑定微信就不用再写账号信息了吧」。 + * + * 这个接口用 `event.context.user`(登录态)直接落库: + * · 必须先登录; + * · body.bindToken 是刚才 wx.login 换来的第三方身份令牌(未绑定过的才算); + * · 该身份已经绑给别人 → 409(不许抢绑); + * · 已经绑给**自己** → 直接当成功(幂等,重复点不报错)。 + */ +export default defineEventHandler(async (event) => { + const user = event.context.user + if (!user) { + throw createError({ statusCode: 401, message: '未登录' }) + } + + const body = await readBody(event) + const bindToken = String(body?.bindToken || '').trim() + if (!bindToken) { + throw createError({ statusCode: 400, message: '缺少绑定令牌,请重新用微信一键登录后再试' }) + } + + let payload: { provider?: string; providerUserId?: string; providerUsername?: string } + try { + payload = verifyBindingToken(bindToken) + } catch (e) { + throw createError({ statusCode: 400, message: '绑定令牌无效或已过期,请重新发起绑定' }) + } + if (!payload?.provider || !payload?.providerUserId) { + throw createError({ statusCode: 400, message: '绑定令牌内容不完整,请重新发起绑定' }) + } + + // 这个第三方身份是不是已经绑给谁了? + const existing = await db + .select({ id: userIdentities.id, userId: userIdentities.userId }) + .from(userIdentities) + .where( + and( + eq(userIdentities.provider, payload.provider), + eq(userIdentities.providerUserId, payload.providerUserId) + ) + ) + .limit(1) + + if (existing.length > 0) { + if (existing[0].userId === user.id) { + return { success: true, alreadyMine: true, message: '这个微信已经绑定在你的账号上了' } + } + throw createError({ + statusCode: 409, + message: '这个微信已经绑定在其它账号上了,请先在那边解绑' + }) + } + + try { + await db.insert(userIdentities).values({ + userId: user.id, + provider: payload.provider, + providerUserId: payload.providerUserId, + providerUsername: payload.providerUsername || null + }) + } catch (e: any) { + if (e?.code === '23505') { + // 并发下被抢先插入:再查一次,若属于自己就算成功 + const again = await db + .select({ userId: userIdentities.userId }) + .from(userIdentities) + .where( + and( + eq(userIdentities.provider, payload.provider), + eq(userIdentities.providerUserId, payload.providerUserId) + ) + ) + .limit(1) + if (again.length > 0 && again[0].userId === user.id) { + return { success: true, alreadyMine: true, message: '这个微信已经绑定在你的账号上了' } + } + throw createError({ statusCode: 409, message: '这个微信已经绑定在其它账号上了,请先在那边解绑' }) + } + throw e + } + + return { success: true, message: '微信绑定成功' } +})