diff --git a/backend/internal/modules/auth/repository.go b/backend/internal/modules/auth/repository.go index 78e4d4b..94e058d 100644 --- a/backend/internal/modules/auth/repository.go +++ b/backend/internal/modules/auth/repository.go @@ -26,6 +26,16 @@ func (r *UserRepository) FindByID(id uint64) (*model.User, error) { return &user, nil } +func (r *UserRepository) UpdateProfile(id uint64, nickname string, avatarURL string) (*model.User, error) { + if err := r.db.Model(&model.User{}).Where("id = ?", id).Updates(map[string]any{ + "nickname": nickname, + "avatar_url": avatarURL, + }).Error; err != nil { + return nil, err + } + return r.FindByID(id) +} + func (r *UserRepository) FindOrCreateByPhone(phone string) (*model.User, error) { now := time.Now() user := model.User{ diff --git a/backend/internal/modules/user/handler.go b/backend/internal/modules/user/handler.go index 181febd..6b640f3 100644 --- a/backend/internal/modules/user/handler.go +++ b/backend/internal/modules/user/handler.go @@ -2,6 +2,7 @@ package user import ( "errors" + "strings" "hfb_sys/backend/internal/middleware" "hfb_sys/backend/internal/modules/auth" @@ -15,6 +16,11 @@ type Handler struct { users *auth.UserRepository } +type UpdateProfileRequest struct { + Nickname string `json:"nickname"` + AvatarURL string `json:"avatar_url"` +} + func NewHandler(users *auth.UserRepository) *Handler { return &Handler{users: users} } @@ -40,3 +46,47 @@ func (h *Handler) Me(c *gin.Context) { } response.OK(c, user) } + +func (h *Handler) UpdateMe(c *gin.Context) { + if h.users == nil { + response.ServiceUnavailable(c, "数据库未连接") + return + } + userID, ok := c.Get(middleware.ContextUserID) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + + var req UpdateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "资料格式不正确") + return + } + + nickname := strings.TrimSpace(req.Nickname) + avatarURL := strings.TrimSpace(req.AvatarURL) + if nickname == "" { + response.BadRequest(c, "昵称不能为空") + return + } + if len([]rune(nickname)) > 24 { + response.BadRequest(c, "昵称不能超过 24 个字符") + return + } + if len(avatarURL) > 512 { + response.BadRequest(c, "头像地址过长") + return + } + + user, err := h.users.UpdateProfile(userID.(uint64), nickname, avatarURL) + if errors.Is(err, gorm.ErrRecordNotFound) { + response.Unauthorized(c, "用户不存在") + return + } + if err != nil { + response.ServiceUnavailable(c, "资料更新失败") + return + } + response.OK(c, user) +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 9218158..860e8d6 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -140,6 +140,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { } api.GET("/me", requireAuth, userHandler.Me) + api.PUT("/me", requireAuth, userHandler.UpdateMe) listingRoutes := api.Group("/listings") { diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 30c7b27..e7e24a9 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -4,6 +4,7 @@ export interface AuthUser { id: number phone: string nickname: string + avatar_url: string realname_status: string risk_status: string credit_score: number @@ -44,3 +45,8 @@ export async function fetchMe() { const { data } = await apiClient.get>('/me') return data.data } + +export async function updateMe(payload: Pick) { + const { data } = await apiClient.put>('/me', payload) + return data.data +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index ddd6e69..a03edad 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -38,6 +38,9 @@ apiClient.interceptors.response.use( localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') localStorage.removeItem('user_id') + localStorage.removeItem('phone') + localStorage.removeItem('nickname') + localStorage.removeItem('avatar_url') localStorage.removeItem('realname_status') if (!window.location.pathname.startsWith('/m/login') && !window.location.pathname.startsWith('/m/register')) { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index d03d65b..c9d7b43 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -218,8 +218,18 @@ router.beforeEach(async (to) => { return { path: toMobilePath(to.path), query: to.query, hash: to.hash }; } - if (to.meta.requiresAuth && !localStorage.getItem("access_token")) { - return { path: "/m/login", query: { redirect: to.fullPath } }; + if (to.meta.requiresAuth) { + const session = useSessionStore(); + if (!session.token) { + return { path: "/m/login", query: { redirect: to.fullPath } }; + } + if (!session.phone) { + try { + await session.loadMe(); + } catch { + return { path: "/m/login", query: { redirect: to.fullPath } }; + } + } } if (to.meta.requiresRealname) { diff --git a/frontend/src/stores/session.ts b/frontend/src/stores/session.ts index c046637..e551ec6 100644 --- a/frontend/src/stores/session.ts +++ b/frontend/src/stores/session.ts @@ -1,13 +1,15 @@ import { defineStore } from 'pinia' -import { fetchMe, loginWithSms, type AuthUser } from '@/api/auth' +import { fetchMe, loginWithSms, updateMe, type AuthUser } from '@/api/auth' export const useSessionStore = defineStore('session', { state: () => ({ token: localStorage.getItem('access_token') || '', refreshToken: localStorage.getItem('refresh_token') || '', userId: Number(localStorage.getItem('user_id') || 0), - phone: '', + phone: localStorage.getItem('phone') || '', + nickname: localStorage.getItem('nickname') || '', + avatarUrl: localStorage.getItem('avatar_url') || '', realnameStatus: localStorage.getItem('realname_status') || 'unknown', }), actions: { @@ -21,6 +23,11 @@ export const useSessionStore = defineStore('session', { this.applyUser(user) return user }, + async updateProfile(payload: { nickname: string; avatar_url: string }) { + const user = await updateMe(payload) + this.applyUser(user) + return user + }, logout() { this.token = '' this.refreshToken = '' @@ -30,6 +37,9 @@ export const useSessionStore = defineStore('session', { localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') localStorage.removeItem('user_id') + localStorage.removeItem('phone') + localStorage.removeItem('nickname') + localStorage.removeItem('avatar_url') localStorage.removeItem('realname_status') }, applySession(user: AuthUser, accessToken: string, refreshToken: string) { @@ -45,6 +55,11 @@ export const useSessionStore = defineStore('session', { this.userId = user.id localStorage.setItem('user_id', String(user.id)) this.phone = user.phone + localStorage.setItem('phone', user.phone) + this.nickname = user.nickname + localStorage.setItem('nickname', user.nickname) + this.avatarUrl = user.avatar_url + localStorage.setItem('avatar_url', user.avatar_url) this.realnameStatus = user.realname_status localStorage.setItem('realname_status', user.realname_status) }, diff --git a/frontend/src/views/mobile/MobileProfileView.vue b/frontend/src/views/mobile/MobileProfileView.vue index 4275ea0..b190537 100644 --- a/frontend/src/views/mobile/MobileProfileView.vue +++ b/frontend/src/views/mobile/MobileProfileView.vue @@ -1,5 +1,5 @@