登录注册接入数据库

This commit is contained in:
2026-03-10 17:01:56 +08:00
parent 8561d1debc
commit 4df88ea502
6 changed files with 210 additions and 172 deletions

View File

@@ -0,0 +1,85 @@
# 多端用户注册与身份识别实现指南 (Multi-Terminal Registration Guide)
本档说明了如何在当前的 Supabase 架构下实现“消费者端”、“商家端”及“管理端”的统一注册逻辑并确保用户身份Role在入库时能够自动、准确地被识别。
## 1. 核心架构原理
系统采用 **“前端声明意图 + 后端自动触发”** 的模式:
1. **前端 App**:在调用接口注册时,通过 `raw_user_meta_data` 声明用户的目标角色(如 `consumer``merchant`)。
2. **Supabase Auth**:接收并存储这些元数据。
3. **数据库触发器 (Trigger)**:在 `auth.users` 产生新记录的一瞬间,由数据库自动读取元数据,并将用户信息连带其正确的角色属性同步到业务表 `ak_users` 中。
---
## 2. 前端实现步骤 (代码参考)
在各端 App 的注册逻辑中,需在调用 `signUp` 接口时传递 `options.data`
### 消费者端 (Consumer App)
`pages/user/register.uvue` 中:
```typescript
const options = new UTSJSONObject()
const metaData = new UTSJSONObject()
metaData.set('user_role', 'consumer') // 核心:声明为消费者
options.set('data', metaData)
const result = await supa.signUp(email, password, options)
```
### 商家端 (Merchant App)
在商家端的注册页面中,只需修改 `user_role` 的值:
```typescript
metaData.set('user_role', 'merchant') // 核心:声明为商家
```
---
## 3. 数据库实现步骤 (SQL 设置)
为了让数据库能够“看碟下菜”,必须在 Supabase SQL Editor 中运行以下脚本,安装/更新智能触发器:
```sql
-- 1. 创建或更新处理函数
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger AS $$
BEGIN
-- 向业务表插入数据,并智能识别角色
INSERT INTO public.ak_users (id, auth_id, email, role, nickname, status)
VALUES (
NEW.id,
NEW.id, -- 统一使用 Auth ID
NEW.email,
-- 核心逻辑:读取 metadata 中的 user_role如果没有传则默认为 'consumer'
COALESCE(NEW.raw_user_meta_data->>'user_role', 'consumer'),
-- 默认昵称取邮箱前缀
split_part(NEW.email, '@', 1),
1 -- 默认激活状态
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 2. 绑定触发器
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
```
---
## 4. 三端互不干扰的优势
* **全自动入库**:一旦 SQL 触发器设置完成,前端不再需要手动调用 `ensureUserProfile``insert` 接口,减少了网络请求和前端报错几率。
* **物理隔离与 RLS 安全**:通过 `ak_users` 表的 RLS 策略(`auth.uid() = id`),确保即使用户通过 API 尝试修改他人数据,也会被数据库直接拦截。
* **统一维护**:所有端的注册逻辑在数据库层面是统一的,未来若需增加新角色(如 `admin_manager`),只需修改触发器逻辑即可,无需大规模重构代码。
---
## 5. 开发建议
* **强制校验**:在生产环境下,可以在触发器内增加校验逻辑,防止普通用户通过伪造元数据获得 `admin` 角色。
* **日志排查**:如果新用户注册后 `ak_users` 表没有数据,请检查 Supabase 控制台的 `Database -> Logs`,查看触发器执行是否有报错(通常是唯一索引冲突导致)。
---
*最后更新时间2026-03-10*

View File

@@ -1,100 +1,10 @@
index.uvue:991 GET http://localhost:5173/pages/mall/admin/product/product-management/index.uvue?vue&type=style&index=0&scoped=6161a702&lang.scss net::ERR_ABORTED 500 (Internal Server Error)
main.uts:16 [Vue warn]: Unhandled error during execution of async component loader
at <AsyncComponentWrapper>
at <PageBody>
at <Page>
at <Anonymous>
at <KeepAlive>
at <RouterView>
at <Layout>
at <App>
warnHandler @ uni-h5.es.js:19975
callWithErrorHandling @ vue.runtime.esm.js:1381
warn$1 @ vue.runtime.esm.js:1207
logError @ vue.runtime.esm.js:1438
errorHandler @ uni-h5.es.js:19600
callWithErrorHandling @ vue.runtime.esm.js:1381
handleError @ vue.runtime.esm.js:1421
onError @ vue.runtime.esm.js:3724
(anonymous) @ vue.runtime.esm.js:3767
Promise.catch
setup @ vue.runtime.esm.js:3766
callWithErrorHandling @ vue.runtime.esm.js:1381
setupStatefulComponent @ vue.runtime.esm.js:8985
setupComponent @ vue.runtime.esm.js:8946
mountComponent @ vue.runtime.esm.js:7262
processComponent @ vue.runtime.esm.js:7228
patch @ vue.runtime.esm.js:6694
mountChildren @ vue.runtime.esm.js:6942
processFragment @ vue.runtime.esm.js:7158
patch @ vue.runtime.esm.js:6668
mountChildren @ vue.runtime.esm.js:6942
processFragment @ vue.runtime.esm.js:7158
patch @ vue.runtime.esm.js:6668
mountChildren @ vue.runtime.esm.js:6942
mountElement @ vue.runtime.esm.js:6849
processElement @ vue.runtime.esm.js:6814
patch @ vue.runtime.esm.js:6682
mountChildren @ vue.runtime.esm.js:6942
mountElement @ vue.runtime.esm.js:6849
processElement @ vue.runtime.esm.js:6814
patch @ vue.runtime.esm.js:6682
mountChildren @ vue.runtime.esm.js:6942
processFragment @ vue.runtime.esm.js:7158
patch @ vue.runtime.esm.js:6668
componentUpdateFn @ vue.runtime.esm.js:7372
run @ vue.runtime.esm.js:153
instance.update @ vue.runtime.esm.js:7497
setupRenderEffect @ vue.runtime.esm.js:7507
mountComponent @ vue.runtime.esm.js:7274
processComponent @ vue.runtime.esm.js:7228
patch @ vue.runtime.esm.js:6694
mountChildren @ vue.runtime.esm.js:6942
mountElement @ vue.runtime.esm.js:6849
processElement @ vue.runtime.esm.js:6814
patch @ vue.runtime.esm.js:6682
componentUpdateFn @ vue.runtime.esm.js:7372
run @ vue.runtime.esm.js:153
instance.update @ vue.runtime.esm.js:7497
setupRenderEffect @ vue.runtime.esm.js:7507
mountComponent @ vue.runtime.esm.js:7274
processComponent @ vue.runtime.esm.js:7228
patch @ vue.runtime.esm.js:6694
componentUpdateFn @ vue.runtime.esm.js:7372
run @ vue.runtime.esm.js:153
instance.update @ vue.runtime.esm.js:7497
setupRenderEffect @ vue.runtime.esm.js:7507
mountComponent @ vue.runtime.esm.js:7274
processComponent @ vue.runtime.esm.js:7228
patch @ vue.runtime.esm.js:6694
componentUpdateFn @ vue.runtime.esm.js:7453
run @ vue.runtime.esm.js:153
instance.update @ vue.runtime.esm.js:7497
updateComponent @ vue.runtime.esm.js:7305
processComponent @ vue.runtime.esm.js:7239
patch @ vue.runtime.esm.js:6694
componentUpdateFn @ vue.runtime.esm.js:7453
run @ vue.runtime.esm.js:153
instance.update @ vue.runtime.esm.js:7497
callWithErrorHandling @ vue.runtime.esm.js:1381
flushJobs @ vue.runtime.esm.js:1585
Promise.then
queueFlush @ vue.runtime.esm.js:1494
queueJob @ vue.runtime.esm.js:1488
scheduler @ vue.runtime.esm.js:3179
resetScheduling @ vue.runtime.esm.js:236
triggerEffects @ vue.runtime.esm.js:280
triggerRefValue @ vue.runtime.esm.js:1033
set value @ vue.runtime.esm.js:1078
finalizeNavigation @ vue-router.mjs?v=ed041164:2474
(anonymous) @ vue-router.mjs?v=ed041164:2384
Promise.then
pushWithRedirect @ vue-router.mjs?v=ed041164:2352
push @ vue-router.mjs?v=ed041164:2278
install @ vue-router.mjs?v=ed041164:2631
use @ vue.runtime.esm.js:5190
initRouter @ uni-h5.es.js:19886
install @ uni-h5.es.js:19955
use @ vue.runtime.esm.js:5190
(anonymous) @ main.uts:16
main.uts:16 TypeError: Failed to fetch dynamically imported module: http://localhost:5173/pages/mall/admin/homePage/index.uvue?import
signIn result:
AkSupaSignInResult {access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4Y…HNlfQ.5BvQq26lJUF23AEglMvA7EzJZvYN_hrp1Q2JA6o0s-w', refresh_token: 'ehhxnwgvgeyk', expires_at: 1773136334, user: UTSJSONObject2, token_type: 'bearer', …}
login.uvue:175 🔍 开始校验商家端角色 -> UID: 8bdf11be-2838-4d96-8552-0949cde076d4, Email: test19@163.com
login.uvue:216 ❌ 查询角色过程异常: TypeError: res.getData is not a function
at login.uvue:180:23
at Generator.next (<anonymous>)
login.uvue:435 登录错误: Error: 商家身份校验失败,请联系管理员检查用户数据
at login.uvue:221:9
at Generator.next (<anonymous>)。
打印这些东西

View File

@@ -166,6 +166,62 @@ const captcha = ref<string>('')
const isLoading = ref<boolean>(false)
/**
* 【核心函数】:登录成功后,多条件校验是否为商家角色
* 优先级: session_uid (auth_id) -> id -> normalized email
*/
const checkMerchantAccess = async (uid: string, rawEmail: string) : Promise<boolean> => {
const email = rawEmail.trim().toLowerCase()
console.log(`🔍 开始校验商家端角色 -> UID: ${uid}, Email: ${email}`)
try {
// 1. 尝试按 auth_id 查询
let res = await supa.from('ak_users').select('role').eq('auth_id', uid).execute()
let dataArray = res.data
if (Array.isArray(dataArray) && dataArray.length > 0) {
const role = (dataArray[0] as UTSJSONObject).getString('role')
console.log('✅ 按 auth_id 匹配成功role:', role)
return role === 'merchant'
}
// 2. 尝试按 id 查询 (兼容老数据)
res = await supa.from('ak_users').select('role').eq('id', uid).execute()
dataArray = res.data
if (Array.isArray(dataArray) && dataArray.length > 0) {
const role = (dataArray[0] as UTSJSONObject).getString('role')
console.log('✅ 按 id 匹配成功role:', role)
return role === 'merchant'
}
// 3. 尝试按 email 兜底查询
if (email !== '') {
res = await supa.from('ak_users').select('role').eq('email', email).execute()
dataArray = res.data
if (Array.isArray(dataArray) && dataArray.length > 0) {
// 如果按邮箱查出来多条,可能存在脏数据,只取第一条并记录日志
if (dataArray.length > 1) {
console.error('⚠️ 警告: 按 email 查到多条 ak_users 记录取第一条校验。Email:', email)
}
const role = (dataArray[0] as UTSJSONObject).getString('role')
console.log('✅ 按 email 匹配成功role:', role)
return role === 'merchant'
}
}
console.error('❌ 未能在 ak_users 中找到该用户的任何记录')
// 查无此人,跑出自定义错误以与普通系统报错区分
throw new Error('NOT_REGISTERED')
} catch (e) {
console.error('❌ 查询角色过程异常:', e)
if (e instanceof Error && e.message === 'NOT_REGISTERED') {
throw new Error('您还没有注册商家端账户,快去注册一个')
}
// 真实的查询异常/RLS异常抛出防止误会为"未注册"
throw new Error('商家身份校验失败,请联系管理员检查用户数据')
}
}
const codeDisabled = ref<boolean>(false)
const codeText = ref<string>('获取验证码')
let codeTimer: number | null = null
@@ -260,8 +316,8 @@ const getCode = async () => {
const handleLogin = async () => {
if (!validateAccount()) return
// 特殊账号处理:admin/admin 直接跳转
if (account.value === 'admin' && password.value === 'admin') {
// 特殊账号处理:仅在测试模式下保留直登逻辑,生产环境强制校验角色
if (IS_TEST_MODE && account.value === 'admin' && password.value === 'admin') {
setIsLoggedIn(true)
const adminProfile = {
id: 'admin',
@@ -301,13 +357,12 @@ const handleLogin = async () => {
if (loginType.value === 0) {
const isEmail = account.value.includes('@')
if (isEmail) {
// 邮箱 + 密码登录(Supabase Auth
// 1) 调用 Supabase Auth 登录
const result = await supa.signIn(account.value.trim(), password.value)
console.log('signIn result:', result)
// 检查登录是否失败
if (result.user == null) {
// 检查是否是邮箱未确认的错误
const rawData = result.raw as UTSJSONObject
const errorMsg = rawData?.getString('msg') ?? ''
const errorCode = rawData?.getString('error_code') ?? ''
@@ -317,12 +372,25 @@ const handleLogin = async () => {
errorMsg.includes('邮箱') && errorMsg.includes('确认')) {
throw new Error('邮箱未确认,请先检查邮箱并点击确认链接')
} else if (errorMsg.includes('Invalid login credentials') ||
errorCode === 'invalid_credentials') {
errorCode === 'invalid_credentials' ||
errorMsg.includes('Invalid credentials')) {
throw new Error('邮箱或密码错误')
} else {
throw new Error(errorMsg || '登录失败,请重试')
}
}
// 2) 【核心逻辑】:执行商家端角色准入校验
// 优先使用 session user id 来查询数据库里的真实 user 数据兜底校验
const sessionUser = result.user
let sessionUid = sessionUser?.getString('id') ?? ''
const isMerchant = await checkMerchantAccess(sessionUid, account.value)
if (!isMerchant) {
await supa.signOut()
logout()
throw new Error('您还没有注册商家端账户,快去注册一个')
}
} else {
uni.showToast({ title: '手机号密码登录功能开发中', icon: 'none' })
return
@@ -332,27 +400,26 @@ const handleLogin = async () => {
return
}
// 尝试获取/补全用户资料,但失败时不再阻塞登录
// 更新 store 中的用户资料
try {
const profile = await getCurrentUser()
console.log('current user profile:', profile)
console.log('fetch profile success:', profile)
} catch (e) {
console.error('获取用户信息失败(忽略,不阻塞登录:', e)
console.error('获取用户信息失败(忽略):', e)
}
// 显式保存用户ID到本地存储,确保页面刷新或重启后 SupabaseService 能恢复身份
// 显式保存用户ID到本地存储
const currentSession = supa.getSession()
if (currentSession.user != null) {
const uid = currentSession.user?.getString('id')
if (uid != null) {
uni.setStorageSync('user_id', uid)
console.log('用户ID已保存到本地存储:', uid)
}
}
uni.showToast({ title: '登录成功', icon: 'success' })
// 即使在测试模式下,点击登录后也执行跳转,确保进入首页
// 成功跳转逻辑
setTimeout(() => {
const pages = getCurrentPages() as any[]
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null

View File

@@ -93,7 +93,6 @@
<script setup lang="uts">
import { ref } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { ensureUserProfile } from '@/utils/sapi.uts'
// 响应式数据
const email = ref<string>('')
@@ -202,95 +201,55 @@
try {
// 使用 Supabase Auth邮箱 + 密码注册
const result = await supa.signUp(email.value.trim(), password.value)
const options = new UTSJSONObject()
const metaData = new UTSJSONObject()
// 【核心修改】:商家端注册时,固定声明 user_role 为 'merchant'
// 该元数据会被 Supabase Auth 存储,并由数据库触发器自动同步到 ak_users 业务表的 role 字段
metaData.set('user_role', 'merchant')
options.set('data', metaData)
const result = await supa.signUp(email.value.trim(), password.value, options)
console.log('📝 注册返回结果:', result)
console.log('📝 注册返回结果JSON:', JSON.stringify(result))
// 检查是否有错误(邮件发送失败等)
// 检查是否有错误
const errorCode = result?.getString('error_code') ?? ''
const errorMsg = result?.getString('msg') ?? ''
const code = result?.getNumber('code') ?? 0
console.log('📝 错误代码:', errorCode, '错误信息:', errorMsg, '状态码:', code)
// 如果返回 500 错误且是邮件发送失败,但用户可能已创建
if (code === 500 && (errorCode === 'unexpected_failure' || errorMsg.includes('confirmation email'))) {
console.warn('⚠️ 邮件发送失败,但用户可能已创建,尝试获取用户信息')
// 即使邮件发送失败,用户可能已经在 auth.users 中创建
// 这里我们仍然尝试创建用户资料
}
// signUp 返回的是 UTSJSONObjectSupabase signup API 返回结构:
// { user: {...}, session: {...} } - 如果邮箱验证未开启
// { user: {...} } - 如果邮箱验证已开启(需要验证邮箱后才能登录)
// { code: 500, error_code: ..., msg: ... } - 如果发生错误(但用户可能已创建)
let user: UTSJSONObject | null = null
let hasSession = false
if (result != null) {
// 尝试获取 user 字段
const userField = result.getJSON('user')
if (userField != null) {
user = userField
console.log('✅ 找到 user 字段:', user.getString('id'), user.getString('email'))
} else {
// 如果没有 user 字段,可能 result 本身就是 user 对象
const id = result.getString('id')
if (id != null && id !== '') {
user = result
console.log('✅ result 本身就是 user 对象:', id)
} else {
console.warn('⚠️ 未找到 user 信息,检查所有字段:', Object.keys(result.toMap() || {}))
}
}
// 检查是否有 session表示注册后自动登录成功
const sessionField = result.getJSON('session')
if (sessionField != null) {
hasSession = true
console.log('✅ 找到 session已自动登录')
// 如果有 session说明已经自动登录token 应该已经设置
// 此时可以直接创建用户资料
} else {
console.log(' 未找到 session可能需要邮箱验证')
}
}
// 如果返回错误且没有用户信息,说明注册失败
if (user == null && code !== 0 && code !== 200) {
// 如果是邮件发送失败,给出明确的错误提示
if (code === 500 && errorMsg.includes('confirmation email')) {
throw new Error('注册失败:邮件服务配置错误,请联系管理员或修改 Supabase 配置(设置 ENABLE_EMAIL_AUTOCONFIRM=true')
throw new Error('注册失败:邮件服务配置错误')
} else {
throw new Error(errorMsg || '注册失败,请重试')
}
}
// 如果获取到 user尝试创建业务侧用户资料ak_users
// 【核心修改】:移除手动调用 ensureUserProfile 逻辑
// 既然已经设置了数据库触发器 (Trigger),用户信息会自动从 auth.users 同步到 ak_users
// 前端不再执行二次插入,避免并发冲突或重复写入
if (user != null) {
try {
const profileResult = await ensureUserProfile(user)
if (profileResult != null) {
console.log('✅ 用户资料创建成功:', profileResult.id)
} else {
console.warn('⚠️ 用户资料创建失败,但注册已成功')
// 如果创建失败,可能是因为 RLS 策略限制
// 建议用户登录后再自动创建(在 getCurrentUser 中处理)
}
} catch (profileError) {
console.error('❌ 创建用户资料异常:', profileError)
// 即使创建资料失败,也不阻止注册流程
// 用户登录时会自动创建(见 utils/store.uts 的 getCurrentUser
// 注册后立即登出,确保用户必须通过登录流程(且经过角色校验)才能进入首页
await supa.signOut()
} catch (signOutError) {
console.error('❌ 登出异常:', signOutError)
}
} else {
console.warn('⚠️ 注册成功但未获取到用户信息')
// 可能需要邮箱验证,用户验证邮箱后登录时会自动创建资料
}
// 如果注册后没有自动登录(需要邮箱验证),提示用户
if (!hasSession && user != null) {
console.log(' 需要邮箱验证,验证后登录时会自动创建用户资料')
}
uni.showToast({