目录
学到这里,我需要梳理一下我目前学习登录注册的逻辑问题。
用户管理系统的流程curd
注册
前端提交的注册数据,给到后端服务,
用户名是否重复,可以在添加数据表单时id添加唯一索引,也可以在接口书写是否已注册,返回未注册,
密码进行加密,写入用户数据(加密密码,创建时间),返回写入成功,关于邮箱注册与手机号注册,我还没有尝试过。待定
关于如何登录
前端页面提交数据(密码,用户信息),到数据库中查看用户信息是否存在,密码进行加密,查看加密密码与数据库的密码是否一致。
推荐使用 BCrypt(自动生成盐值,无需单独存储)
// 注册时加密
String encryptedPwd = new BCryptPasswordEncoder().encode(rawPassword);
// 登录时校验
boolean isMatch = new BCryptPasswordEncoder().matches(rawPassword, dbPassword);
练习可以使用 MD5(需自己处理盐值,已逐渐被淘汰)
因为数据不大
我需要将校验用户名是否存在数据库中,检测密码时需要加密,可以加盐,验证数据库的密码hash值,对比。
注册就是,用户名,密码,确认密码,
唯一标识校验
手机号 / 邮箱 / 用户名需全局唯一
-- 数据库唯一索引
ALTER TABLE `user` ADD UNIQUE `idx_user_phone` (`user_phone`);
-- 后端查询校验
User existUser = userMapper.findByPhone(userPhone);
if (existUser != null) {
throw new BizException("手机号已注册");
}
下面就是注意的一点,关于登录的需要注意的点,我可以允许他存在一台设备上,当进行二次登录,将第一次登录的设备会被弹出,这个时候的
- 登录态保持(Token 机制)
方案:JWT(JSON Web Token)
流程:
登录成功时生成 JWT:
// JJWT 库示例
String token = Jwts.builder()
.setSubject(userId)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 1小时过期
.signWith(SignatureAlgorithm.HS256, "your-secret-key")
.compact();
前端存储 Token(localStorage 或 Cookie)
后续请求携带 Token(Authorization 头)
com.bushuo.bushuopicturebackend.exception.BusinessException: 用户不存在或密码错误
数据库也是userAccount
在使用 QueryWrapper
进行查询时遇到了“用户不存在或密码错误”的异常。
现在的问题在于 userAccount
字段在数据库中的实际列名为 user_account
,而代码中直接使用了 userAccount
导致字段映射不一致。
解决方案
- 字段映射不一致:在 MyBatis Plus 中,如果实体类字段名与数据库表的实际列名不完全一致(包括大小写),需要使用
@TableField
注解进行显式映射。 - 推荐使用
LambdaQueryWrapper
:相比QueryWrapper
,LambdaQueryWrapper
提供更高的类型安全性和可维护性,避免字段名拼写错误。
修改代码
1. 在 User
实体类中添加 @TableField
注解
首先需要在 User
实体类中对 userAccount
字段添加 @TableField
注解,明确指定其对应的数据库列名。
2. 使用 LambdaQueryWrapper
进行查询
在 UserServiceImpl
类的 userLogin
方法中,推荐使用 LambdaQueryWrapper
来提高查询的安全性和可维护性。
问题小结
- 修改点 1:在
User
实体类中为userAccount
字段添加@TableField("user_account")
注解,确保字段映射正确。 - 修改点 2:在
UserServiceImpl
的userLogin
方法中,使用LambdaQueryWrapper
替代QueryWrapper
,提高查询的安全性和可维护性。
这样可以有效解决由于字段映射不一致导致的“用户不存在或密码错误”问题。
其实只需要将数据库名称的下划线由_变成-这个差不多就可以了
关于数据库,我安装了宝塔连接数据库后,不知道为什么数据库密码变了,我查了一轮,发现密码在宝塔里面,因为我不知道为什么重置mysql密码不行,我一看我的数据库密码。
密码也可以修改,关于数据库8.0版本,我看了需要修改地区时效
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/bushuo-picture?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
他还要我修改地址,将utc换成Asia/Shanghai,8.0以上需要。
还有数据库的驱动下载
寻找驱动下载,可以将5.0的删掉“-”;
解决了一点点问题。挺开心的。
添加群功能
✅ 可行性结论
模块 | 可行性 | 说明 |
---|---|---|
数据库设计 | ✅✅✅ | 表结构合理、索引完备、支持软删除与角色扩展 |
后端实现(Spring Boot + MyBatis) | ✅✅✅ | 分层清晰、事务控制、接口规范、易于维护 |
可扩展性 | ✅✅✅ | 支持角色、权限、昵称、禁言等后续功能扩展 |
安全性与健壮性 | ✅✅ | 提供了 JWT 校验、幂等性、状态管理等安全机制 |
团队协作与文档规范 | ✅✅ | 接口命名规范、代码结构清晰,适合多人协作 |
🧩 补充
1. 数据迁移脚本(如从 public_chat_status
迁移)
如果你之前使用了 public_chat_status
来记录用户群组关系,可以写一个迁移脚本将有效数据迁移到 group_members
中:
INSERT INTO group_members (group_id, user_id, role, status)
SELECT group_id, user_id, 'member', 1
FROM public_chat_status
WHERE status = 1;
- 后续逐步弃用
public_chat_status
的相关逻辑。
2. 枚举字段定义建议(MySQL ENUM
或 Java 枚举类)
MySQL 层面定义:
role ENUM('member', 'admin', 'owner') DEFAULT 'member'
Java 层面定义:
public enum GroupRole {
MEMBER("member"),
ADMIN("admin"),
OWNER("owner");
private final String value;
GroupRole(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
- 使用枚举能提升代码可读性和类型安全性。
3. 幂等性处理(防止重复加群)
在 Controller 层或 Service 层做幂等判断:
if (groupMembersMapper.findByGroupIdAndUserId(groupId, userId) != null) {
return ResponseEntity.badRequest().body("您已在该群中");
}
- 防止前端误操作或网络重发导致的重复请求。
4. 统一返回结构(推荐)
统一使用 Result<T>
返回结构,便于前端解析:
public class Result<T> {
private int code;
private String message;
private T data;
// success / error 方法
}
示例返回:
return Result.success("加入成功");
5. 日志与监控建议
- 在关键操作(如加群、退群)中添加日志记录。
- 可结合 AOP 实现接口调用日志追踪。
- 若接入监控系统(如 Prometheus),可记录加群频率、失败次数等指标。
- 数据库建表
CREATE TABLE group_members (
id INT PRIMARY KEY AUTO_INCREMENT,
group_id INT NOT NULL,
user_id INT NOT NULL,
join_time DATETIME DEFAULT CURRENT_TIMESTAMP,
role ENUM('member', 'admin', 'owner') DEFAULT 'member',
status INT DEFAULT 1, -- 1=正常,0=退出
UNIQUE KEY uq_group_user (group_id, user_id),
KEY idx_group_id (group_id),
KEY idx_user_id (user_id)
);
- Java 实体类
package com.life.lifechat.domain;
import lombok.Data;
import java.util.Date;
@Data
public class GroupMembers {
private Integer id;
private Integer groupId;
private Integer userId;
private Date joinTime;
private String role;
private Integer status;
// getters and setters
}
- Mapper 接口
package com.life.lifechat.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.life.lifechat.domain.GroupMembers;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface GroupMembersMapper extends BaseMapper<GroupMembers> {
GroupMembers findByGroupIdAndUserId(@Param("groupId") Integer groupId, @Param("userId") Integer userId);
List<GroupMembers> findByGroupId(@Param("groupId") Integer groupId);
List<GroupMembers> findByUserId(@Param("userId") Integer userId);
int updateStatusByGroupIdAndUserId(@Param("groupId") Integer groupId, @Param("userId") Integer userId, @Param("status") Integer status);
}
四、Mapper XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.life.lifechat.mapper.GroupMembersMapper">
<select id="findByGroupIdAndUserId" resultType="com.life.lifechat.domain.GroupMembers">
SELECT * FROM group_members WHERE group_id = #{groupId} AND user_id = #{userId} AND status = 1
</select>
<select id="findByGroupId" resultType="com.life.lifechat.domain.GroupMembers">
SELECT * FROM group_members WHERE group_id = #{groupId} AND status = 1
</select>
<select id="findByUserId" resultType="com.life.lifechat.domain.GroupMembers">
SELECT * FROM group_members WHERE user_id = #{userId} AND status = 1
</select>
<update id="updateStatusByGroupIdAndUserId">
UPDATE group_members SET status = #{status}
WHERE group_id = #{groupId} AND user_id = #{userId}
</update>
</mapper>
五、Service 层
新建 GroupMembersService.java
package com.life.lifechat.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.life.lifechat.domain.GroupMembers;
/**
* 群成员服务接口
*/
public interface GroupMembersService extends IService<GroupMembers> {
}
- 新建实现类 GroupMembersServiceImpl.java
package com.life.lifechat.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.life.lifechat.domain.GroupMembers;
import com.life.lifechat.mapper.GroupMembersMapper;
import com.life.lifechat.service.GroupMembersService;
import org.springframework.stereotype.Service;
@Service
public class GroupMembersServiceImpl extends ServiceImpl<GroupMembersMapper, GroupMembers> implements GroupMembersService {
}
✅ 在原有 GroupsService 中调用新服务
如果你在 GroupsService 中需要操作群成员数据,可以注入 GroupMembersService:
package com.life.lifechat.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.life.lifechat.domain.Groups;
import com.life.lifechat.domain.GroupMembers;
import com.life.lifechat.dto.GroupWithLastMessageDTO;
import java.util.List;
/**
* 原有群组服务接口
*/
public interface GroupsService extends IService<Groups> {
List<GroupWithLastMessageDTO> getGroupsWithLastMessage(Integer userId);
// 示例方法:获取用户加入的群成员列表
List<GroupMembers> getUserGroups(Integer userId);
}
对应的实现类中注入并使用:
@Autowired
private GroupMembersService groupMembersService;
@Override
public List<GroupMembers> getUserGroups(Integer userId) {
return groupMembersService.listByUserId(userId); // 需要你在 GroupMembersService 中定义该方法
}
✅ 补充:在 GroupMembersService 中加常用方法
修改 GroupMembersService.java
public interface GroupMembersService extends IService<GroupMembers> {
List<GroupMembers> listByGroupId(Integer groupId);
List<GroupMembers> listByUserId(Integer userId);
boolean joinGroup(Integer groupId, Integer userId);
boolean quitGroup(Integer groupId, Integer userId);
}
修改 GroupMembersServiceImpl.java
@Override
public List<GroupMembers> listByGroupId(Integer groupId) {
return lambdaQuery()
.eq(GroupMembers::getGroupId, groupId)
.eq(GroupMembers::getStatus, 1)
.list();
}
@Override
public List<GroupMembers> listByUserId(Integer userId) {
return lambdaQuery()
.eq(GroupMembers::getUserId, userId)
.eq(GroupMembers::getStatus, 1)
.list();
}
@Override
@Transactional
public boolean joinGroup(Integer groupId, Integer userId) {
GroupMembers existing = lambdaQuery()
.eq(GroupMembers::getGroupId, groupId)
.eq(GroupMembers::getUserId, userId)
.eq(GroupMembers::getStatus, 1)
.one();
if (existing != null) return false;
GroupMembers member = new GroupMembers();
member.setGroupId(groupId);
member.setUserId(userId);
member.setRole("member");
member.setStatus(1);
return save(member);
}
@Override
public boolean quitGroup(Integer groupId, Integer userId) {
return lambdaUpdate()
.eq(GroupMembers::getGroupId, groupId)
.eq(GroupMembers::getUserId, userId)
.set(GroupMembers::getStatus, 0)
.update();
}
加群前端代码
前端目录
src/
├── api/ # 接口封装
│ ├── axios.js # Axios 实例配置
│ └── group.js # 群相关接口
├── views/ # 页面组件
│ ├── Groups.vue # 群列表页
│ └── GroupChat.vue # 群聊天页(可选)
├── store/ # 状态管理
│ └── userStore.js # 用户信息存储
├── utils/ # 工具函数
│ └── errorHandler.js # 统一错误处理
├── router/ # 路由配置
│ └── index.js
├── App.vue
└── main.js
- axios.js:统一请求配置
// src/api/axios.js
import axios from 'axios';
const instance = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api',
timeout: 5000,
});
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default instance;
- group.js:群相关 API 封装
// src/api/group.js
import axios from '@/api/axios';
export function joinGroup(groupId) {
return axios.post(`/groups/${groupId}/join`);
}
export function getGroupList() {
return axios.get('/groups/list');
}
- errorHandler.js:统一错误提示工具
// src/utils/errorHandler.js
import { ElMessage } from 'element-plus';
export function handleApiError(error, defaultMessage = '操作失败') {
const message = error.response?.data?.message || defaultMessage;
ElMessage.error(message);
}
- userStore.js:用户状态管理(Pinia)
// src/store/userStore.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
userId: null,
nickname: '',
avatar: '',
}),
actions: {
setUser(userInfo) {
Object.assign(this, userInfo);
},
clearUser() {
this.userId = null;
this.nickname = '';
this.avatar = '';
}
}
});
- Groups.vue:群列表页面组件
<template>
<div class="groups-container">
<h2>群聊列表</h2>
<el-table :data="groupList" border style="width: 100%">
<el-table-column prop="groupName" label="群名称" />
<el-table-column label="操作">
<template #default="scope">
<el-button
type="primary"
@click="handleJoin(scope.row.id)"
:loading="loading"
:disabled="scope.row.joined"
>
{{ scope.row.joined ? '已加入' : '加入群聊' }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useUserStore } from '@/store/userStore';
import { useRouter } from 'vue-router';
import { joinGroup, getGroupList } from '@/api/group';
import { handleApiError } from '@/utils/errorHandler';
const userStore = useUserStore();
const router = useRouter();
const groupList = ref([]);
const loading = ref(false);
onMounted(() => {
fetchGroupList();
});
function fetchGroupList() {
getGroupList()
.then(res => {
groupList.value = res.data.data || [];
})
.catch(err => {
handleApiError(err, '获取群列表失败');
});
}
function handleJoin(groupId) {
loading.value = true;
joinGroup(groupId)
.then(res => {
ElMessage.success(res.data.message || '加入成功');
const group = groupList.value.find(g => g.id === groupId);
if (group) group.joined = true;
// 跳转到群聊天页
router.push({ name: 'GroupChat', params: { groupId } });
})
.catch(err => {
handleApiError(err, '加入失败');
})
.finally(() => {
loading.value = false;
});
}
</script>
<style scoped>
.groups-container {
padding: 20px;
}
</style>
- router/index.js:路由配置
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Groups from '../views/Groups.vue';
import GroupChat from '../views/GroupChat.vue';
const routes = [
{ path: '/groups', component: Groups, name: 'Groups' },
{ path: '/group/:groupId/chat', component: GroupChat, name: 'GroupChat' },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
- main.js:主入口文件
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { createPinia } from '@pinia/vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
const app = createApp(App);
const pinia = createPinia();
app.use(router);
app.use(pinia);
app.use(ElementPlus);
app.mount('#app');
1.运行
npm install pinia
2.修改 src/main.js:
import { createPinia } from ‘pinia’;
// …
app.use(createPinia());
解决方法
- 用的是 Vue CLI(不是 Vite)
- 如何修复?
import axios from 'axios';
// Vue CLI 用 process.env.VUE_APP_API_URL
const baseURL = process.env.VUE_APP_API_URL || '/api';
const instance = axios.create({
baseURL,
timeout: 5000,
});
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default instance;
后续在index修改添加加群按钮
你输入内容,点击“搜索”按钮,前端请求 /groups/list?keyword=xxx。
后端返回群聊列表,前端渲染出来。
点击“加入群聊”按钮,前端请求 /groups/{groupId}/join,成功后按钮变为“已加入”。
/groups/list?keyword=1
新增“全局群搜索”接口 /groups/list
✅ 一、新增接口:/groups/list 用于全局群搜索
- Controller 层
// GroupsController.java
@GetMapping("/groups/list")
public Map<String, Object> searchGroups(@RequestParam String keyword) {
Map<String, Object> result = new HashMap<>();
try {
List<GroupWithLastMessageDTO> groups = groupsService.searchGroups(keyword);
result.put("code", 200);
result.put("msg", "success");
result.put("data", groups);
} catch (Exception e) {
result.put("code", 500);
result.put("msg", "系统繁忙,请稍后重试");
}
return result;
}
✅ 二、Service 层
- GroupsService.java 接口定义
// GroupsService.java
List<GroupWithLastMessageDTO> searchGroups(String keyword);
- GroupsServiceImpl.java 实现类
// GroupsServiceImpl.java
@Autowired
private GroupsMapper groupsMapper;
@Override
public List<GroupWithLastMessageDTO> searchGroups(String keyword) {
return groupsMapper.searchGroups(keyword);
}
✅ 三、Mapper 层
- GroupsMapper.java
// GroupsMapper.java
List<GroupWithLastMessageDTO> searchGroups(String keyword);
- GroupsMapper.xml
<!-- GroupsMapper.xml -->
<select id="searchGroups" parameterType="string" resultType="com.life.lifechat.dto.GroupWithLastMessageDTO">
SELECT
g.id,
g.group_name AS groupName,
g.group_img AS groupImg,
NULL AS lastMessage, -- 可选字段,加群页可能不需要显示最后一条消息
NULL AS time
FROM groups g
WHERE g.group_name LIKE CONCAT('%', #{keyword}, '%')
</select>
✅ 四、DTO 类定义
// GroupWithLastMessageDTO.java
package com.life.lifechat.dto;
import java.util.Date;
public class GroupWithLastMessageDTO {
private Integer id;
private String groupName;
private String groupImg;
private String lastMessage;
private Date time;
// Getters and Setters
}
SELECT
g.id AS id,
g.group_name AS groupName,
g.group_img AS groupImg FROMgroups
g WHERE g.group_name LIKE ‘%技术%’
梳理如何将“加群/退群”功能完整落地,
直接集成和运行。
- 后端实现(Spring Boot + MyBatis Plus)
1.1 DTO
在 lifeChat/src/main/java/com/life/lifechat/dto/ 下新建 GroupJoinDTO.java
package com.life.lifechat.dto;
public class GroupJoinDTO {
private Integer groupId;
private Integer userId;
// getters and setters
}
1.2 Mapper
在 GroupMembersMapper.java 中补充:
@Select("SELECT * FROM group_members WHERE group_id = #{groupId} AND user_id = #{userId} AND status = 1")
GroupMembers selectByGroupIdAndUserId(@Param("groupId") Integer groupId, @Param("userId") Integer userId);
@Update("UPDATE group_members SET status = 0 WHERE group_id = #{groupId} AND user_id = #{userId}")
int quitGroup(@Param("groupId") Integer groupId, @Param("userId") Integer userId);
- Service 层
GroupMembersService.java
public interface GroupMembersService {
boolean joinGroup(Integer groupId, Integer userId);
boolean quitGroup(Integer groupId, Integer userId);
}
实现 GroupMembersServiceImpl.java:
@Service
public class GroupMembersServiceImpl implements GroupMembersService {
@Override
public boolean joinGroup(Integer groupId, Integer userId) {
GroupMembers existing = baseMapper.selectByGroupIdAndUserId(groupId, userId);
if (existing != null && existing.getStatus() == 1) {
return false;
}
GroupMembers member = new GroupMembers();
member.setGroupId(groupId);
member.setUserId(userId);
member.setRole("member");
member.setStatus(1);
return baseMapper.insert(member) > 0; }
@Override
public boolean quitGroup(Integer groupId, Integer userId) {
return baseMapper.quitGroup(groupId, userId) > 0;
}
}
1.4 Controller
在 GroupsController.java 中添加:
@Autowired
private GroupMembersService groupMembersService; // 👈 注意变量名和类型
@PostMapping("/groups/join")
public Map<String, Object> joinGroup(@RequestBody GroupJoinDTO dto) {
Map<String, Object> result = new HashMap<>();
try {
boolean success = groupMembersService.joinGroup(dto.getGroupId(), dto.getUserId());
if (success) {
result.put("code", 200);
result.put("msg", "加入成功");
} else {
result.put("code", 400);
result.put("msg", "您已在该群中或群不存在");
}
} catch (Exception e) {
e.printStackTrace();
result.put("code", 500);
result.put("msg", "系统繁忙,请稍后再试");
}
return result;
}
@PostMapping("/groups/quit")
public Map<String, Object> quitGroup(@RequestBody GroupJoinDTO dto) {
Map<String, Object> result = new HashMap<>();
try {
boolean success = groupMembersService.quitGroup(dto.getGroupId(), dto.getUserId());
if (success) {
result.put("code", 200);
result.put("msg", "退出成功");
} else {
result.put("code", 400);
result.put("msg", "退出失败,请确认是否在群中");
}
} catch (Exception e) {
e.printStackTrace();
result.put("code", 500);
result.put("msg", "系统繁忙,请稍后再试");
}
return result;
}
总结
完成登录注册基本流程,包括 BCrypt 密码加密、JWT Token 生成与验证、数据库交互,并解决了字段映射不一致(@TableField注解)和 MySQL 8.0 连接(时区 + 驱动)问题。
明确了数据库设计(group_members表)、前后端实现步骤(实体类 / 接口 / 页面 / 状态管理),定义了核心逻辑(加群查重、退群软删除)和调试方法。
待弥补 ,优化。Token 管理优化与 加密与安全增强、 前后端交互与用户体验