Files
BYW-VoiceHub/server/api/auth/wechat/bind-self.post.ts
T
Henry_Du 752a2eae87 feat(auth): 已登录账号绑定微信改为一键(不再要求填姓名密码)
站长反馈:登陆进去的账号绑定微信就不用再写账号信息了吧。

原来的绑定只有一个入口 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 → 调新接口);
- 「账号管理 → 绑定微信」与首页那条绑定提示都改成一步完成,
  不再跳绑定页填账号密码(绑定页保留给未登录场景:如登录页发现微信未绑账号时去注册/绑定)。
2026-10-01 00:08:06 +08:00

93 lines
3.6 KiB
TypeScript

// 与同目录/相邻的认证接口保持一致: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: '微信绑定成功' }
})