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:
2026-06-03 17:04:22 +08:00
parent 9162b2c87c
commit 21f4d81959
45 changed files with 3145 additions and 348 deletions

View File

@@ -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
})
}