SchoolDash Alpha冲刺随笔2 - Day 3
课程与作业信息
所属课程:软件工程实践
作业要求来源:第五次作业——Alpha冲刺
本篇目标:记录冲刺第3天进度,展示燃尽图、运行效果、成员工作成果
项目燃尽图(Burn-up Chart)
已完成需求的75%本日冲刺整体进展
完成用户注册、登录功能,后端JWT验证,前端多角色Token存储。
解决角色权限问题:实现不同角色(用户、骑手、管理员)独立Token管理。
- 项目最新运行效果
用户端注册界面
用户端登录界面
管理员端登录界面
骑手端登录界面
- 今日工作成果
(后端开发)
认证路由与JWT实现
const express = require('express'); const router = express.Router(); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const User = require('../models/User'); // 注册接口(前端请求的 /api/auth/register 对应这里) router.post('/register', async (req, res) => { try { const { username, password, phone } = req.body; // 校验参数 if (!username || !password) { return res.status(400).json({ code: 400, msg: '用户名和密码不能为空' }); } // 检查用户名是否重复 const existingUser = await User.findOne({ where: { username } }); if (existingUser) { return res.status(400).json({ code: 400, msg: '用户名已存在' }); } // 创建用户(密码由User模型自动加密) const user = await User.create({ username, password: password, // 传递明文密码,模型会自动加密 phone: phone || '', role: 'user' // 默认注册为普通用户 }); // 返回成功结果 res.status(201).json({ code: 200, msg: '注册成功', data: { id: user.id, username: user.username, role: user.role } }); } catch (error) { console.error('注册接口报错:', error); res.status(500).json({ code: 500, msg: '服务器错误,注册失败' }); } }); // 登录接口(配套前端登录功能) router.post('/login', async (req, res) => { try { const { username, password } = req.body; // 查找用户 const user = await User.findOne({ where: { username } }); if (!user) { return res.status(400).json({ code: 400, msg: '用户名不存在,请检查输入或前往注册' }); } // 验证密码 const isPasswordValid = bcrypt.compareSync(password, user.password); if (!isPasswordValid) { return res.status(400).json({ code: 400, msg: '密码不正确,请重新输入' }); } // 生成 JWT Token const token = jwt.sign( { id: user.id, role: user.role }, process.env.JWT_SECRET || 'school_dash_jwt_secret_2025', { expiresIn: '24h' } ); // 返回登录结果 res.json({ code: 200, msg: '登录成功', data: { token, username: user.username, role: user.role } }); } catch (error) { console.error('登录接口报错:', error); res.status(500).json({ code: 500, msg: '服务器繁忙,请稍后再试' }); } }); // 管理员登录接口 router.post('/admin/login', async (req, res) => { try { const { username, password } = req.body; const user = await User.findOne({ where: { username } }); if (!user) { return res.status(400).json({ code: 400, msg: '管理员账号不存在,请联系系统管理员' }); } if (user.role !== 'admin') { return res.status(403).json({ code: 403, msg: '该账号没有管理员权限,请使用管理员账号登录' }); } const isPasswordValid = bcrypt.compareSync(password, user.password); if (!isPasswordValid) { return res.status(400).json({ code: 400, msg: '管理员密码不正确,请重新输入' }); } const token = jwt.sign( { id: user.id, role: user.role }, process.env.JWT_SECRET || 'school_dash_jwt_secret_2025', { expiresIn: '24h' } ); res.json({ code: 200, msg: '管理员登录成功', data: { token, username: user.username, role: user.role } }); } catch (error) { console.error('管理员登录接口报错:', error); res.status(500).json({ code: 500, msg: '服务器繁忙,请稍后再试' }); } }); // 骑手登录接口 router.post('/rider/login', async (req, res) => { try { const { username, password } = req.body; const user = await User.findOne({ where: { username } }); if (!user) { return res.status(400).json({ code: 400, msg: '骑手账号不存在,请联系管理员开通账号' }); } if (user.role !== 'rider') { return res.status(403).json({ code: 403, msg: '该账号没有骑手权限,请使用骑手账号登录' }); } const isPasswordValid = bcrypt.compareSync(password, user.password); if (!isPasswordValid) { return res.status(400).json({ code: 400, msg: '骑手密码不正确,请重新输入' }); } const token = jwt.sign( { id: user.id, role: user.role }, process.env.JWT_SECRET || 'school_dash_jwt_secret_2025', { expiresIn: '24h' } ); res.json({ code: 200, msg: '骑手登录成功', data: { token, username: user.username, role: user.role } }); } catch (error) { console.error('骑手登录接口报错:', error); res.status(500).json({ code: 500, msg: '服务器繁忙,请稍后再试' }); } }); module.exports = router;这是基于 Express 的用户认证路由,提供普通用户注册(/register)及普通用户、管理员、骑手三类专属登录接口,通过 bcrypt 加密校验密码,JWT 生成 24 小时身份令牌,同时校验角色权限并处理异常,接口路径统一挂载在 /api/auth/ 下。
(前端开发)
用户登录界面
<template> <div class="login-page"> <div class="back-button" @click="$router.push('/')"> <font-awesome-icon icon="fa-solid fa-arrow-left" class="back-icon" /> </div> <div class="login-container"> <!-- Logo区域 --> <div class="school-dash-logo"> <font-awesome-icon icon="fa-solid fa-graduation-cap" class="logo-icon" /> <div class="logo-text">School Dash</div> </div> <!-- 标题 --> <div class="login-title">用户登录</div> <!-- 用户名输入框 --> <div class="form-item"> <label class="form-label">用户名</label> <input type="text" class="form-input" v-model="username" placeholder="请输入用户名" @keyup.enter="handleLogin" /> </div> <!-- 密码输入框 --> <div class="form-item"> <label class="form-label">密码</label> <input type="password" class="form-input" v-model="password" placeholder="请输入密码" @keyup.enter="handleLogin" /> </div> <!-- 登录按钮 --> <button class="login-btn" @click="handleLogin">登录</button> <!-- 注册链接 --> <div class="register-link"> 还没有账号?<span @click="$router.push('/user/register')">立即注册</span> </div> </div> </div> </template> <script setup> import { ref } from 'vue'; import { useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { faGraduationCap, faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import request from '../../utils/request'; // 路由实例 const router = useRouter(); // 表单数据绑定 const username = ref(''); const password = ref(''); // 登录核心逻辑 const handleLogin = async () => { if (!username.value) return ElMessage.warning('请输入用户名'); if (!password.value) return ElMessage.warning('请输入密码'); try { const res = await request({ url: '/auth/login', method: 'POST', data: { username: username.value.trim(), password: password.value } }); if (!res) { return ElMessage.error('登录失败,服务器无响应'); } if (res.code === 200) { ElMessage.success('登录成功!'); // 关键1:存储后端返回的真实token和用户信息 localStorage.setItem('token', res.data.token); localStorage.setItem('role', res.data.role); localStorage.setItem('username', res.data.username); // 关键2:跳转到用户首页 router.push('/user'); } else { ElMessage.error(res.msg || '登录失败,请检查账号密码'); } } catch (error) { console.error('登录请求异常:', error); const errMsg = error.message || '网络异常,请稍后重试'; ElMessage.error(errMsg); } }; </script> <style scoped> /* 页面整体布局 */ .login-page { min-height: 100vh; background-color: #f5f5f5; display: flex; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; position: relative; } /* 返回按钮样式 */ .back-button { position: absolute; top: 20px; left: 20px; width: 40px; height: 40px; background-color: #ffffff; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); cursor: pointer; transition: all 0.2s ease; z-index: 10; } .back-button:hover { background-color: #f0f0f0; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .back-icon { font-size: 18px; color: #4299e1; } /* 登录容器 */ .login-container { width: 100%; max-width: 350px; background-color: #ffffff; padding: 32px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); box-sizing: border-box; } /* Logo样式 */ .school-dash-logo { text-align: center; margin-bottom: 24px; } .logo-icon { font-size: 48px; color: #4299e1; margin-bottom: 12px; } .logo-text { font-size: 24px; font-weight: 600; color: #333333; } /* 标题样式 */ .login-title { font-size: 20px; font-weight: 600; color: #333333; margin-bottom: 24px; text-align: center; } /* 表单项目样式 */ .form-item { margin-bottom: 20px; } .form-label { display: block; font-size: 14px; color: #333333; margin-bottom: 8px; text-align: left; } /* 输入框样式 */ .form-input { width: 100%; padding: 12px 16px; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box; transition: all 0.2s ease; } .form-input:focus { border-color: #4299e1; box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.1); } .form-input::placeholder { color: #999999; } /* 登录按钮样式 */ .login-btn { width: 100%; padding: 14px; background-color: #4299e1; color: #ffffff; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background-color 0.2s ease; margin-bottom: 16px; } .login-btn:hover { background-color: #3a86cf; } .login-btn:active { background-color: #3182ce; } .login-btn:disabled { background-color: #a7c0ff; cursor: not-allowed; } /* 注册链接样式 */ .register-link { text-align: center; font-size: 14px; color: #666666; } .register-link span { color: #4299e1; cursor: pointer; margin-left: 4px; } .register-link span:hover { text-decoration: underline; } </style>管理员登陆界面
<template> <div class="admin-login-page"> <div class="back-button" @click="$router.push('/')"> <font-awesome-icon icon="fa-solid fa-arrow-left" class="back-icon" /> </div> <div class="login-container"> <div class="school-dash-logo"> <font-awesome-icon icon="fa-solid fa-graduation-cap" class="logo-icon" /> <div class="logo-text">School Dash 管理后台</div> </div> <div class="login-title">管理员登录</div> <div class="form-item"> <label class="form-label">用户名</label> <input type="text" class="form-input" v-model="username" placeholder="请输入管理员用户名" @keyup.enter="handleLogin" /> </div> <div class="form-item"> <label class="form-label">密码</label> <input type="password" class="form-input" v-model="password" placeholder="请输入管理员密码" @keyup.enter="handleLogin" /> </div> <button class="login-btn" @click="handleLogin">登录</button> </div> </div> </template> <script setup> import { ref } from 'vue'; import { useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { faGraduationCap, faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import request from '../../utils/request'; const router = useRouter(); const username = ref(''); const password = ref(''); const handleLogin = async () => { // 表单校验 if (!username.value.trim()) return ElMessage.warning('请输入用户名'); if (!password.value.trim()) return ElMessage.warning('请输入密码'); try { const res = await request({ url: '/auth/admin/login', method: 'POST', data: { username: username.value.trim(), password: password.value.trim() } }); if (res.code === 200) { // 存储管理员token和名称 localStorage.setItem('adminToken', res.data.token); // 后端返回的是 data.username(非 data.user.username) localStorage.setItem('adminName', res.data.username); ElMessage.success('登录成功'); router.push('/admin/dashboard'); } else { ElMessage.error(res.msg); } } catch (error) { ElMessage.error('登录失败,请检查后端服务'); console.error('登录报错:', error); } }; </script> <style scoped> .admin-login-page { min-height: 100vh; background-color: #f5f5f5; display: flex; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; position: relative; } /* 返回按钮样式 */ .back-button { position: absolute; top: 20px; left: 20px; width: 40px; height: 40px; background-color: #ffffff; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); cursor: pointer; transition: all 0.2s ease; z-index: 10; } .back-button:hover { background-color: #f0f0f0; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .back-icon { font-size: 18px; color: #4299e1; } .login-container { width: 100%; max-width: 350px; background-color: #ffffff; padding: 32px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); box-sizing: border-box; } .school-dash-logo { text-align: center; margin-bottom: 24px; } .logo-icon { font-size: 48px; color: #4299e1; margin-bottom: 12px; } .logo-text { font-size: 24px; font-weight: 600; color: #333; } .login-title { font-size: 20px; font-weight: 600; color: #333; margin-bottom: 24px; text-align: center; } .form-item { margin-bottom: 20px; } .form-label { display: block; font-size: 14px; color: #333; margin-bottom: 8px; text-align: left; } .form-input { width: 100%; padding: 12px 16px; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box; transition: all 0.2s ease; } .form-input:focus { border-color: #4299e1; box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.1); } .login-btn { width: 100%; padding: 14px; background-color: #4299e1; color: #ffffff; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background-color 0.2s ease; margin-bottom: 16px; } .login-btn:hover { background-color: #3a86cf; } </style>骑手登陆界面
<template> <div class="rider-login-page"> <div class="back-button" @click="$router.push('/')"> <font-awesome-icon icon="fa-solid fa-arrow-left" class="back-icon" /> </div> <div class="login-container"> <div class="school-dash-logo"> <font-awesome-icon icon="fa-solid fa-motorcycle" class="logo-icon" /> <div class="logo-text">School Dash 骑手端</div> </div> <div class="login-title">骑手登录</div> <div class="form-item"> <label class="form-label">用户名</label> <input type="text" class="form-input" v-model="username" placeholder="请输入用户名" @keyup.enter="handleLogin" /> </div> <div class="form-item"> <label class="form-label">密码</label> <input type="password" class="form-input" v-model="password" placeholder="请输入密码" @keyup.enter="handleLogin" /> </div> <button class="login-btn" @click="handleLogin">登录</button> </div> </div> </template> <script setup> import { ref } from 'vue'; import { useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { faMotorcycle, faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import request from '../../utils/request'; const router = useRouter(); const username = ref(''); const password = ref(''); const handleLogin = async () => { // 表单校验 if (!username.value.trim()) return ElMessage.warning('请输入用户名'); if (!password.value.trim()) return ElMessage.warning('请输入密码'); try { const res = await request({ url: '/auth/rider/login', method: 'POST', data: { username: username.value.trim(), password: password.value.trim() } }); if (res.code === 200) { // 存储骑手token和名称 localStorage.setItem('riderToken', res.data.token); // 后端返回的是 data.username localStorage.setItem('riderName', res.data.username); ElMessage.success('登录成功'); router.push('/rider/dashboard'); } else { ElMessage.error(res.msg); } } catch (error) { ElMessage.error('登录失败,请检查后端服务'); console.error('登录报错:', error); } }; </script> <style scoped> .rider-login-page { min-height: 100vh; background-color: #f5f5f5; display: flex; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; position: relative; } /* 返回按钮样式 */ .back-button { position: absolute; top: 20px; left: 20px; width: 40px; height: 40px; background-color: #ffffff; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); cursor: pointer; transition: all 0.2s ease; z-index: 10; } .back-button:hover { background-color: #f0f0f0; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .back-icon { font-size: 18px; color: #4299e1; } .login-container { width: 100%; max-width: 350px; background-color: #ffffff; padding: 32px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); box-sizing: border-box; } .school-dash-logo { text-align: center; margin-bottom: 24px; } .logo-icon { font-size: 48px; color: #4299e1; margin-bottom: 12px; } .logo-text { font-size: 24px; font-weight: 600; color: #333; } .login-title { font-size: 20px; font-weight: 600; color: #333; margin-bottom: 24px; text-align: center; } .form-item { margin-bottom: 20px; } .form-label { display: block; font-size: 14px; color: #333; margin-bottom: 8px; text-align: left; } .form-input { width: 100%; padding: 12px 16px; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box; transition: all 0.2s ease; } .form-input:focus { border-color: #4299e1; box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.1); } .login-btn { width: 100%; padding: 14px; background-color: #4299e1; color: #ffffff; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background-color 0.2s ease; margin-bottom: 16px; } .login-btn:hover { background-color: #3a86cf; } </style>(测试)
任务:登录功能测试
成果:完成测试(正常登录、错误密码、角色切换),无bug
- 本日小结与明日计划
今日总结:认证模块稳定,支持多角色。
存在问题:跨域配置需完善
明日计划:商品列表与分类模块
进度稳健,继续冲!