feat: 用户信息完善 + 一起记功能
- users 表添加 nickname、avatar_url 字段 - 用户信息 API(GET/PUT /me)+ 头像上传(multer) - 头像通过 API 路由获取(公开,不经过 auth) - 登录接口返回用户昵称和头像 - 用户信息 Store + 个人资料编辑页面 - 我的页面和首页显示真实用户信息 - 群组表(groups、group_members)+ transactions 添加 group_id - 群组 API(创建/加入/退出/解散) - 交易和统计 API 支持 group_id 视图切换 - 群组客户端 Store + 身份切换 - 群组管理页面 - App 初始化群组 Store - UPLOAD_DIR 配置化 - Node.js 备份替代 mysqldump - 统一前端 BASE_URL(config.ts) - uploads 加入 .gitignore - 修复 trust proxy 警告
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
// API 基础地址
|
||||
const BASE_URL = 'https://xiaocai.j35.site/api'
|
||||
import { API_BASE } from '@/config'
|
||||
|
||||
interface RequestOptions {
|
||||
url: string
|
||||
@@ -7,13 +6,49 @@ interface RequestOptions {
|
||||
data?: any
|
||||
}
|
||||
|
||||
interface LoginResult {
|
||||
token: string
|
||||
userId: number
|
||||
nickname: string
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
let isRedirectingToLogin = false
|
||||
/** 重登录期间挂起的请求,登录成功后统一重试 */
|
||||
let pendingRetries: Array<{ resolve: (v: any) => void; reject: (e: any) => void; options: RequestOptions }> = []
|
||||
|
||||
/** 保存登录结果到本地存储 */
|
||||
export function saveLoginResult(data: LoginResult) {
|
||||
uni.setStorageSync('xc:token', data.token)
|
||||
uni.setStorageSync('xc:userId', data.userId)
|
||||
uni.setStorageSync('xc:nickname', data.nickname)
|
||||
uni.setStorageSync('xc:avatar_url', data.avatar_url)
|
||||
}
|
||||
|
||||
/** 执行重登录 */
|
||||
async function reLogin(): Promise<void> {
|
||||
// H5 环境:demo 登录
|
||||
if (typeof window !== 'undefined') {
|
||||
const data = await request<LoginResult>({ url: '/auth/demo-login', method: 'POST' })
|
||||
saveLoginResult(data)
|
||||
return
|
||||
}
|
||||
|
||||
// 小程序环境:wx.login
|
||||
const loginRes = await new Promise<UniApp.LoginRes>((resolve, reject) => {
|
||||
uni.login({ provider: 'weixin', success: resolve, fail: reject })
|
||||
})
|
||||
if (!loginRes.code) throw new Error('wx.login failed')
|
||||
|
||||
const data = await request<LoginResult>({ url: '/auth/login', method: 'POST', data: { code: loginRes.code } })
|
||||
saveLoginResult(data)
|
||||
}
|
||||
|
||||
export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = uni.getStorageSync('xc:token')
|
||||
uni.request({
|
||||
url: BASE_URL + options.url,
|
||||
url: API_BASE + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: {
|
||||
@@ -23,45 +58,36 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
success: (res: any) => {
|
||||
const { statusCode, data } = res
|
||||
|
||||
// HTTP 401 - token invalid or expired
|
||||
if (statusCode === 401) {
|
||||
uni.removeStorageSync('xc:token')
|
||||
uni.removeStorageSync('xc:userId')
|
||||
|
||||
// 加入重试队列
|
||||
pendingRetries.push({ resolve, reject, options })
|
||||
|
||||
if (!isRedirectingToLogin) {
|
||||
isRedirectingToLogin = true
|
||||
// H5 环境自动获取 demo token 并重试
|
||||
if (typeof window !== 'undefined') {
|
||||
request<{ token: string; userId: number }>({
|
||||
url: '/auth/demo-login',
|
||||
method: 'POST'
|
||||
}).then((data) => {
|
||||
uni.setStorageSync('xc:token', data.token)
|
||||
uni.setStorageSync('xc:userId', data.userId)
|
||||
// 重试原请求
|
||||
request(options).then(resolve).catch(reject)
|
||||
}).catch(() => {
|
||||
const retries = [...pendingRetries]
|
||||
pendingRetries = []
|
||||
|
||||
reLogin()
|
||||
.then(() => retries.forEach(r => request(r.options).then(r.resolve).catch(r.reject)))
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '登录失败,请刷新页面', icon: 'none' })
|
||||
reject({ code: 40100, message: '未登录' })
|
||||
}).finally(() => { isRedirectingToLogin = false })
|
||||
} else {
|
||||
// 小程序环境自动重新登录
|
||||
reLogin().then(() => {
|
||||
request(options).then(resolve).catch(reject)
|
||||
}).catch(() => {
|
||||
reject({ code: 40100, message: '未登录' })
|
||||
}).finally(() => { isRedirectingToLogin = false })
|
||||
}
|
||||
} else {
|
||||
reject({ code: 40100, message: '未登录' })
|
||||
retries.forEach(r => r.reject({ code: 40100, message: '未登录' }))
|
||||
})
|
||||
.finally(() => { isRedirectingToLogin = false })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Business logic success
|
||||
if (data.code === 0) {
|
||||
resolve(data.data)
|
||||
} else {
|
||||
uni.showToast({ title: data.message || '请求失败', icon: 'none' })
|
||||
// "用户不存在" 不弹 toast(首次登录竞态)
|
||||
if (data.code !== 40400 || data.message !== '用户不存在') {
|
||||
uni.showToast({ title: data.message || '请求失败', icon: 'none' })
|
||||
}
|
||||
reject(data)
|
||||
}
|
||||
},
|
||||
@@ -72,40 +98,3 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 微信小程序自动重新登录
|
||||
function reLogin(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: async (loginRes) => {
|
||||
if (loginRes.code) {
|
||||
try {
|
||||
const data = await request<{ token: string; userId: number }>({
|
||||
url: '/auth/login',
|
||||
method: 'POST',
|
||||
data: { code: loginRes.code }
|
||||
})
|
||||
uni.setStorageSync('xc:token', data.token)
|
||||
uni.setStorageSync('xc:userId', data.userId)
|
||||
resolve()
|
||||
} catch (e) {
|
||||
console.error('[Auth] Re-login failed:', e)
|
||||
reject(e)
|
||||
}
|
||||
} else {
|
||||
reject(new Error('wx.login failed'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[Auth] wx.login failed:', err)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
resolve()
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user