资讯动态

Springboot+Vue个人中心实战:头像上传、数据联动与修改密码的配置骨架

发布时间:2026/9/25 14:24:35 来源:尧图企业网站定制
1. 个人中心模块到底要解决什么问题前后端分离项目里个人中心看起来只是几个表单实际落地时最容易卡在四个地方头像上传后前端不刷新、用户信息改了但顶部导航还是旧昵称、修改密码接口 404、密码校验逻辑写在了前端而后端没兜底。这篇就围绕 Springboot Vue 的个人中心实战把头像上传与回显、用户信息数据联动、修改密码校验这三块拆成可复制的配置骨架目标是一次跑通全流程。适合谁看已经能跑起一个 Springboot Vue 后台管理项目准备补上个人中心模块的同学或者头像上传成功但页面不更新、改完密码不知道哪里校验的同学。核心检索词就三个Springboot 个人中心接口、Vue 头像上传回显、修改密码校验。下面所有代码都可以直接往项目里贴接口路径、字段名、组件通信方式都标清楚你照着改包名和端口即可。我试过把这几块拆开单独调结果联调时发现头像路径存的是相对路径、前端拼了 baseURL 导致 404所以第 5 节专门列了这类坑。整体思路是后端提供/file/upload、/user/username/{username}、/user、/user/password四个接口前端用Person.vue和Password.vue两个页面承接顶部Header.vue通过Manage.vue拿到最新 user 对象做数据联动。2. TaoToken 前置把模型对话和接口调试放在一个工作台写个人中心这类模块最烦的不是写代码而是接口报错时不知道是前端传参问题还是后端没接住。我习惯在动手前先把调试环境准备好这里用 TaoToken 做两件事一是用模型对话快速确认某段 Vue 组件通信写法对不对二是把接口文档和 API Key 管理放在同一个控制台里联调时切来切去少一些。TaoToken 官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content具体操作路径模型对话https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 用来问「Vue 子组件 emit 后父组件怎么刷新」这类具体问题比翻文档快。API Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 联调接口时如果要用到模型能力做参数校验Key 在这里建。接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 接口字段和返回结构对照着看减少 404 和字段名写错。注意TaoToken 在这里的角色是开发辅助工作台不是让你把用户密码之类的敏感数据丢进去。个人中心涉及密码字段调试时用测试账号别用真实用户数据。如果你后面要做长期编码或者 Agent 类项目可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 把模型调用和项目开发流程串起来。但本篇重点还是个人中心本身TaoToken 只是帮你少踩接口联调的坑。3. 可复制配置后端四个接口 前端两个页面3.1 后端文件上传、用户查询、用户保存、修改密码先看修改密码需要的 DTO字段名要和前端表单对齐否则RequestBody接不到import lombok.Data; Data public class UserPasswordDTO { private String username; private String phone; private String password; // 原密码 private String newPassword; // 新密码 }Controller 层四个接口路径建议统一挂/user和/file下RestController RequestMapping(/user) public class UserController { Resource private UserService userService; // 按用户名查用户用于个人中心回显和顶部导航联动 GetMapping(/username/{username}) public Result getUserByUsername(PathVariable String username) { return Result.success(userService.getByUsername(username)); } // 保存用户信息昵称、邮箱、电话、地址、头像 PostMapping public Result save(RequestBody User user) { userService.updateById(user); return Result.success(); } // 修改密码 PostMapping(/password) public Result password(RequestBody UserPasswordDTO dto) { userService.updatePassword(dto); return Result.success(); } }文件上传单独一个 Controller返回文件访问路径RestController RequestMapping(/file) public class FileController { PostMapping(/upload) public String upload(RequestParam(file) MultipartFile file) throws IOException { String fileName UUID.randomUUID() _ file.getOriginalFilename(); String path System.getProperty(user.dir) /files/ fileName; File dest new File(path); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); // 返回可访问的 URL前端直接塞进 avatarUrl return http://localhost:9090/file/ fileName; } }Service 层修改密码核心是「更新条数小于 1 就说明原密码不对」Override public void updatePassword(UserPasswordDTO dto) { int update userMapper.updatePassword(dto); if (update 1) { throw new ServiceException(Constants.CODE_600, 原密码错误); } }Mapper 层用一条 SQL 同时校验原密码并更新Update(update sys_user set password #{newPassword} where username #{username} and password #{password}) int updatePassword(UserPasswordDTO dto);这里有个关键点where条件里带了password #{password}所以原密码错的时候更新条数是 0Service 直接抛异常前端收到code ! 200就提示「原密码错误」。这样校验逻辑在后端兜底前端只做两次新密码是否一致的检查。3.2 前端Person.vue 头像上传与信息回显Person.vue负责头像上传、表单回显、保存后通知父组件刷新。上传组件用el-uploadaction指向后端上传接口template div el-card stylewidth: 600px el-form label-width80px sizesmall div styletext-align: center; margin: 10px 0 el-upload classavatar-uploader actionhttp://localhost:9090/file/upload :show-file-listfalse :on-successhandleAvatarSuccess img v-ifform.avatarUrl :srcform.avatarUrl classavatar i v-else classel-icon-plus avatar-uploader-icon/i /el-upload /div el-form-item label用户名 el-input v-modelform.username disabled/el-input /el-form-item el-form-item label昵称 el-input v-modelform.nickname/el-input /el-form-item el-form-item label邮箱 el-input v-modelform.email/el-input /el-form-item el-form-item label电话 el-input v-modelform.phone/el-input /el-form-item el-form-item label地址 el-input v-modelform.address/el-input /el-form-item /el-form div styletext-align: center el-button typeprimary clicksave确 定/el-button /div /el-card /div /template脚本部分load()按用户名拉数据save()保存后$emit(refreshUser)通知父组件script export default { name: Person, data() { return { form: {}, user: localStorage.getItem(user) ? JSON.parse(localStorage.getItem(user)) : {} } }, created() { this.load() }, methods: { load() { const username this.user.username if (!username) { this.$message.error(当前无法获取用户信息) return } this.request.get(/user/username/ username).then(res { this.form res.data }) }, save() { this.request.post(/user, this.form).then(res { if (res.code 200) { this.$message.success(保存成功) this.load() this.$emit(refreshUser) } else { this.$message.error(保存失败) } }) }, handleAvatarSuccess(res) { // res 就是后端返回的文件访问路径 this.form.avatarUrl res } } } /script样式部分给上传框一个虚线边框和 138x138 的尺寸头像图片同样尺寸避免上传前后跳动style .avatar-uploader { text-align: center; padding-bottom: 10px; } .avatar-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; } .avatar-uploader .el-upload:hover { border-color: #409EFF; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 138px; height: 138px; line-height: 138px; text-align: center; } .avatar { width: 138px; height: 138px; display: block; } /style3.3 数据联动Manage.vue 与 Header.vue 的通信顶部导航要显示最新昵称和头像靠的是Manage.vue拿到 user 后传给Header.vue。Manage.vue里Header :collapseBtnClasscollapseBtnClass asideCollapsecollapse :useruser / router-view refreshUsergetUser /data() { return { user: {} } }, created() { this.getUser() }, methods: { getUser() { let username localStorage.getItem(user) ? JSON.parse(localStorage.getItem(user)).username : if (username) { this.request.get(/user/username/ username).then(res { this.user res.data }) } } }Header.vue用props接收 user下拉菜单里绑定user.avatarUrl和user.nicknameel-dropdown stylewidth: 150px; cursor: pointer; text-align: right div styledisplay: inline-block img :srcuser.avatarUrl alt stylewidth: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px span{{ user.nickname }}/span i classel-icon-arrow-down stylemargin-left: 5px/i /div el-dropdown-menu slotdropdown stylewidth: 100px; text-align: center el-dropdown-item stylefont-size: 14px; padding: 5px 0 router-link to/password修改密码/router-link /el-dropdown-item el-dropdown-item stylefont-size: 14px; padding: 5px 0 router-link to/person个人信息/router-link /el-dropdown-item el-dropdown-item stylefont-size: 14px; padding: 5px 0 span clicklogout退出/span /el-dropdown-item /el-dropdown-menu /el-dropdownprops: { user: Object }这样Person.vue保存后$emit(refreshUser)Manage.vue的getUser()重新拉数据Header.vue的user跟着更新头像和昵称就联动了。3.4 修改密码Password.vue 表单与校验Password.vue三个字段原密码、新密码、确认新密码。前端只校验两次新密码是否一致原密码对不对交给后端template el-card stylewidth: 500px; el-form label-width120px sizesmall :modelform :rulesrules refpass el-form-item label原密码 proppassword el-input v-modelform.password show-password/el-input /el-form-item el-form-item label新密码 propnewPassword el-input v-modelform.newPassword show-password/el-input /el-form-item el-form-item label确认新密码 propconfirmPassword el-input v-modelform.confirmPassword show-password/el-input /el-form-item el-form-item el-button typeprimary clicksave确 定/el-button /el-form-item /el-form /el-card /templateexport default { name: Password, data() { return { form: {}, user: localStorage.getItem(user) ? JSON.parse(localStorage.getItem(user)) : {}, rules: { password: [ { required: true, message: 请输入原密码, trigger: blur }, { min: 3, message: 长度不少于3位, trigger: blur } ], newPassword: [ { required: true, message: 请输入新密码, trigger: blur }, { min: 3, message: 长度不少于3位, trigger: blur } ], confirmPassword: [ { required: true, message: 请输入密码, trigger: blur }, { min: 3, message: 长度不少于3位, trigger: blur } ] } } }, created() { this.form.username this.user.username }, methods: { save() { this.$refs.pass.validate((valid) { if (valid) { if (this.form.newPassword ! this.form.confirmPassword) { this.$message.error(2次输入的新密码不相同) return false } this.request.post(/user/password, this.form).then(res { if (res.code 200) { this.$message.success(修改成功) this.$store.commit(logout) } else { this.$message.error(res.msg) } }) } }) } } }修改成功后调logout让用户重新登录避免旧 token 还能用。4. 验证请求接口联调与成功结果4.1 用 curl 先验后端四个接口启动 Springboot 后先别急着开前端用 curl 把接口过一遍。查用户curl http://localhost:9090/user/username/admin期望返回{ code: 200, msg: 成功, data: { id: 1, username: admin, nickname: 管理员, avatarUrl: http://localhost:9090/file/xxx.png } }上传文件curl -X POST http://localhost:9090/file/upload \ -F file/path/to/avatar.png期望返回一个字符串路径比如http://localhost:9090/file/abc_avatar.png。把这个路径填进用户表的avatar_url字段再查一次用户确认avatarUrl变了。修改密码curl -X POST http://localhost:9090/user/password \ -H Content-Type: application/json \ -d {username:admin,password:123,newPassword:456}原密码正确返回code: 200把password改成错的返回code: 600和「原密码错误」。这一步验证了后端校验是生效的。4.2 前端联调顺序前端按这个顺序点一遍能最快定位问题登录后进个人中心看表单是否回显了昵称、邮箱、头像。如果头像不显示打开 Network 看avatarUrl请求是不是 404。改昵称点确定看顶部导航昵称是否跟着变。不变说明$emit(refreshUser)没触发或Manage.vue没监听。上传头像看图片是否立刻替换。不替换看handleAvatarSuccess有没有拿到res。进修改密码页原密码填错看是否提示「原密码错误」两次新密码不一致看是否提示「2次输入的新密码不相同」都正确后看是否跳登录页。4.3 成功结果长什么样个人中心表单加载后用户名禁用、昵称邮箱电话地址可编辑、头像显示当前图片。改完昵称保存顶部导航昵称同步更新刷新页面后仍是新昵称。上传头像后图片立即替换刷新页面后头像仍是新的。修改密码原密码错误时提示「原密码错误」两次新密码不一致时提示「2次输入的新密码不相同」成功后退出登录。5. 本篇常见错排查5.1 头像上传成功但页面不显示最常见的原因是后端返回的是相对路径前端img :src直接用了相对路径浏览器按当前页面域名去请求自然 404。解决办法是后端返回完整 URL或者前端拼上baseURL。上面FileController返回的是http://localhost:9090/file/xxx前端直接用就行。另一个原因是handleAvatarSuccess(res)里的res不是字符串而是对象。如果你后端包了一层Result那res是{code, msg, data}要取res.data。上面示例后端直接返回字符串所以this.form.avatarUrl res是对的两种写法别混。5.2 修改信息后顶部导航不更新检查三处Person.vue的save()里有没有this.$emit(refreshUser)Manage.vue的router-view有没有写refreshUsergetUserHeader.vue有没有用props: { user: Object }接收。三处缺一处联动就断。还有一种情况是Manage.vue的getUser()里从localStorage取 username但登录后没往localStorage存 user导致 username 为空接口没发出去。登录成功后记得localStorage.setItem(user, JSON.stringify(res.data))。5.3 修改密码接口 404先确认前端请求路径是/user/password后端 Controller 类上是RequestMapping(/user)、方法上是PostMapping(/password)。如果前端request封装了baseURL比如baseURL: /api那实际请求是/api/user/password后端要么配代理要么把baseURL去掉。还有一种 404 是请求方法不对前端用了get而后端是PostMapping。修改密码涉及密码字段必须用 POST。5.4 原密码错误但没提示如果后端updatePassword返回 0 条更新但没抛异常前端会收到code: 200以为成功了。检查 Service 里有没有if (update 1) throw new ServiceException(...)。另外 Mapper 的 SQL 里where条件要同时带username和password只带username的话原密码错也会更新成功。5.5 上传文件后访问 404后端保存文件的目录和静态资源映射目录要一致。如果你用System.getProperty(user.dir) /files/保存那要配一个静态资源映射把/file/**指到该目录Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/file/**) .addResourceLocations(file: System.getProperty(user.dir) /files/); } }少了这段上传成功但图片打不开。6. 继续把个人中心接进你的开发流个人中心跑通后下一步通常是把它接进更大的后台体系权限校验、操作日志、多端同步。这时候接口调试和模型辅助会频繁切换TaoToken 的模型对话适合用来快速确认组件通信和接口字段API Keys 和接入文档放在一起联调时少开几个标签页。排障和接入相关先看 API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 再对照接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。验证模型返回和参数结构用模型对话https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。长期做编码或 Agent 项目看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。最后留一个实用技巧把Person.vue和Password.vue的接口路径抽成常量文件比如api/user.js改端口或加前缀时只改一处。个人中心这类模块接口不多但路径写散在各个组件里联调时最容易因为一个斜杠或大小写对不上而 404。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑