项目简介
本项目是一个基于Spring Boot + Vue.js的校园物业服务管理平台,旨在提供便捷的校园物业服务预约和管理功能。
技术栈
后端
- Spring Boot 2.7.x
- Spring Security
- MyBatis-Plus
- MySQL 8.0
- Redis
- JWT
前端
- Vue.js 3
- Element Plus
- Axios
- Vite
- Pinia
核心功能
-
用户管理
- 学生/教职工注册登录
- 角色权限控制
- 个人信息管理
-
报修服务
- 在线报修申请
- 维修进度追踪
- 维修评价反馈
-
物业公告
- 通知发布
- 公告管理
- 消息推送
-
设施预约
- 会议室预约
- 体育场地预约
- 预约记录查询
项目亮点
- 前后端分离架构
java:src/main/java/com/example/config/CorsConfig.java">@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
- JWT认证
java:src/main/java/com/example/util/JwtUtil.java">@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
}
- Redis缓存
java:src/main/java/com/example/config/RedisConfig.java">@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
项目部署
- 采用Docker容器化部署
- Nginx反向代理
- Jenkins自动化部署
项目成果
- 显著提升校园物业服务效率
- 优化资源调配
- 提高用户满意度
- 实现数据可视化管理
技术难点解决
- 使用Redis解决高并发场景
- 采用分布式锁处理资源预约冲突
- 实现WebSocket推送实时消息
- 优化大数据量查询性能
总结与展望
本项目实现了校园物业服务的信息化、智能化管理,未来计划:
- 引入AI智能客服
- 开发移动端应用
- 接入物联网设备
- 扩展更多便民服务功能
校园物业服务平台功能详解
一、用户管理模块
1. 用户注册与登录
数据库设计
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`real_name` varchar(50) COMMENT '真实姓名',
`user_type` tinyint NOT NULL COMMENT '用户类型:1学生 2教职工 3维修人员 4管理员',
`student_id` varchar(20) COMMENT '学号/工号',
`phone` varchar(11) COMMENT '手机号',
`email` varchar(50) COMMENT '邮箱',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注册接口
java:src/main/java/com/example/controller/UserController.java">@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping("/register")
public Result register(@RequestBody UserRegisterDTO dto) {
// 参数校验
if (!validateUserInput(dto)) {
return Result.error("参数不合法");
}
// 密码加密
String encodedPassword = passwordEncoder.encode(dto.getPassword());
// 保存用户信息
User user = new User();
BeanUtils.copyProperties(dto, user);
user.setPassword(encodedPassword);
userService.save(user);
return Result.success();
}
}
登录实现
java:src/main/java/com/example/service/impl/UserServiceImpl.java">@Service
public class UserServiceImpl implements UserService {
@Autowired
private JwtUtil jwtUtil;
@Override
public LoginVO login(LoginDTO loginDTO) {
// 验证用户名密码
User user = userMapper.selectByUsername(loginDTO.getUsername());
if (user == null || !passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
// 生成token
String token = jwtUtil.generateToken(user.getUsername());
// 存入Redis
redisTemplate.opsForValue().set(
"token:" + token,
user,
24,
TimeUnit.HOURS
);
return new LoginVO(token, user);
}
}
2. 角色权限控制
角色表设计
CREATE TABLE `role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_name` varchar(50) NOT NULL COMMENT '角色名称',
`role_code` varchar(50) NOT NULL COMMENT '角色编码',
PRIMARY KEY (`id`)
);
CREATE TABLE `user_role` (
`user_id` bigint NOT NULL,
`role_id` bigint NOT NULL,
PRIMARY KEY (`user_id`, `role_id`)
);
权限拦截器
java:src/main/java/com/example/interceptor/AuthInterceptor.java">@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 获取token
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new UnauthorizedException("未登录");
}
// 验证token
String username = jwtUtil.validateToken(token);
// 获取用户权限
User user = (User) redisTemplate.opsForValue().get("token:" + token);
List<String> permissions = roleService.getUserPermissions(user.getId());
// 判断是否有权限
RequiresPermissions annotation = ((HandlerMethod) handler).getMethodAnnotation(RequiresPermissions.class);
if (annotation != null && !permissions.contains(annotation.value())) {
throw new ForbiddenException("无权限访问");
}
return true;
}
}
二、报修服务模块
1. 报修申请
报修表设计
CREATE TABLE `repair_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '申请人ID',
`repair_type` tinyint NOT NULL COMMENT '报修类型:1水电 2家具 3空调 4其他',
`location` varchar(100) NOT NULL COMMENT '报修地点',
`description` varchar(500) NOT NULL COMMENT '问题描述',
`images` varchar(500) COMMENT '图片地址,多个逗号分隔',
`status` tinyint NOT NULL COMMENT '状态:0待处理 1已派单 2维修中 3已完成 4已评价',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
报修提交接口
java:src/main/java/com/example/controller/RepairController.java">@RestController
@RequestMapping("/api/repair")
public class RepairController {
@PostMapping("/submit")
public Result submit(@RequestBody RepairDTO dto) {
// 上传图片
List<String> imageUrls = uploadImages(dto.getImageFiles());
// 创建工单
RepairOrder order = new RepairOrder();
BeanUtils.copyProperties(dto, order);
order.setImages(String.join(",", imageUrls));
order.setStatus(0);
repairService.save(order);
// 发送通知给管理员
notifyAdmin(order);
return Result.success();
}
}
2. 维修进度追踪
进度更新接口
java:src/main/java/com/example/service/impl/RepairServiceImpl.java">@Service
public class RepairServiceImpl implements RepairService {
@Override
@Transactional
public void updateProgress(RepairProgressDTO dto) {
// 更新工单状态
RepairOrder order = getById(dto.getOrderId());
order.setStatus(dto.getStatus());
updateById(order);
// 记录处理日志
RepairLog log = new RepairLog();
log.setOrderId(dto.getOrderId());
log.setOperator(dto.getOperator());
log.setContent(dto.getContent());
repairLogService.save(log);
// 发送进度通知
if (dto.getStatus() == 2) {
// 维修中状态,通知用户预计到达时间
notifyUser(order.getUserId(), "维修人员正在前往处理,预计" + dto.getEstimatedTime() + "到达");
} else if (dto.getStatus() == 3) {
// 完成状态,通知用户评价
notifyUser(order.getUserId(), "维修已完成,请及时评价");
}
}
}
3. 维修评价
评价表设计
CREATE TABLE `repair_rating` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_id` bigint NOT NULL COMMENT '工单ID',
`user_id` bigint NOT NULL COMMENT '评价人ID',
`rating` tinyint NOT NULL COMMENT '评分:1-5',
`comment` varchar(500) COMMENT '评价内容',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`)
);
评价提交接口
java:src/main/java/com/example/controller/RepairController.java">@RestController
@RequestMapping("/api/repair")
public class RepairController {
@PostMapping("/rate")
public Result rate(@RequestBody RatingDTO dto) {
// 检查工单状态
RepairOrder order = repairService.getById(dto.getOrderId());
if (order.getStatus() != 3) {
return Result.error("工单状态不正确");
}
// 保存评价
RepairRating rating = new RepairRating();
BeanUtils.copyProperties(dto, rating);
ratingService.save(rating);
// 更新工单状态为已评价
order.setStatus(4);
repairService.updateById(order);
// 统计维修人员评分
updateRepairmanRating(order.getRepairmanId());
return Result.success();
}
}
校园物业服务平台功能详解(续)
三、物业公告模块
1. 通知公告表设计
CREATE TABLE `notice` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '标题',
`content` text NOT NULL COMMENT '内容',
`type` tinyint NOT NULL COMMENT '类型:1通知 2公告 3新闻',
`status` tinyint NOT NULL COMMENT '状态:0草稿 1已发布 2已下架',
`publisher_id` bigint NOT NULL COMMENT '发布人ID',
`publish_time` datetime COMMENT '发布时间',
`top` tinyint DEFAULT 0 COMMENT '是否置顶:0否 1是',
`view_count` int DEFAULT 0 COMMENT '浏览次数',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 公告管理接口
java:src/main/java/com/example/controller/NoticeController.java">@RestController
@RequestMapping("/api/notice")
@RequiresPermissions("notice")
public class NoticeController {
@PostMapping("/publish")
public Result publish(@RequestBody NoticeDTO dto) {
Notice notice = new Notice();
BeanUtils.copyProperties(dto, notice);
notice.setStatus(1);
notice.setPublishTime(new Date());
notice.setPublisherId(getCurrentUserId());
noticeService.save(notice);
// 发送消息推送
if (dto.isNeedPush()) {
pushNoticeToUsers(notice);
}
return Result.success();
}
@PostMapping("/top/{id}")
public Result setTop(@PathVariable Long id, @RequestParam Boolean top) {
noticeService.update()
.set("top", top)
.eq("id", id)
.update();
return Result.success();
}
}
3. WebSocket消息推送
java:src/main/java/com/example/websocket/WebSocketServer.java">@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
sessionMap.put(userId, session);
}
@OnClose
public void onClose(@PathParam("userId") String userId) {
sessionMap.remove(userId);
}
public void pushMessage(String userId, String message) {
Session session = sessionMap.get(userId);
if (session != null) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("消息推送失败", e);
}
}
}
}
四、设施预约模块
1. 设施和预约表设计
-- 设施表
CREATE TABLE `facility` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '名称',
`type` tinyint NOT NULL COMMENT '类型:1会议室 2体育场地',
`location` varchar(100) COMMENT '位置',
`capacity` int COMMENT '容量',
`description` varchar(500) COMMENT '描述',
`status` tinyint NOT NULL COMMENT '状态:0停用 1启用',
PRIMARY KEY (`id`)
);
-- 预约表
CREATE TABLE `reservation` (
`id` bigint NOT NULL AUTO_INCREMENT,
`facility_id` bigint NOT NULL COMMENT '设施ID',
`user_id` bigint NOT NULL COMMENT '预约人ID',
`date` date NOT NULL COMMENT '预约日期',
`start_time` time NOT NULL COMMENT '开始时间',
`end_time` time NOT NULL COMMENT '结束时间',
`purpose` varchar(200) COMMENT '用途',
`status` tinyint NOT NULL COMMENT '状态:0待审核 1已通过 2已拒绝 3已取消',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_facility_date` (`facility_id`, `date`)
);
2. 预约服务实现
java:src/main/java/com/example/service/impl/ReservationServiceImpl.java">@Service
public class ReservationServiceImpl implements ReservationService {
@Autowired
private RedisTemplate redisTemplate;
@Override
@Transactional
public Result reserve(ReservationDTO dto) {
// 检查设施是否可用
Facility facility = facilityService.getById(dto.getFacilityId());
if (facility.getStatus() != 1) {
return Result.error("设施不可用");
}
// 使用分布式锁防止并发预约
String lockKey = "reservation:lock:" + dto.getFacilityId() + ":" + dto.getDate();
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
return Result.error("操作太频繁,请稍后重试");
}
try {
// 检查时间段是否已被预约
boolean timeConflict = checkTimeConflict(dto);
if (timeConflict) {
return Result.error("该时间段已被预约");
}
// 创建预约记录
Reservation reservation = new Reservation();
BeanUtils.copyProperties(dto, reservation);
reservation.setUserId(getCurrentUserId());
reservation.setStatus(0);
save(reservation);
// 发送审核通知
notifyAdmin(reservation);
return Result.success();
} finally {
redisTemplate.delete(lockKey);
}
}
private boolean checkTimeConflict(ReservationDTO dto) {
return baseMapper.selectCount(new QueryWrapper<Reservation>()
.eq("facility_id", dto.getFacilityId())
.eq("date", dto.getDate())
.eq("status", 1)
.and(w -> w
.between("start_time", dto.getStartTime(), dto.getEndTime())
.or()
.between("end_time", dto.getStartTime(), dto.getEndTime())
)) > 0;
}
}
3. 预约记录查询
java:src/main/java/com/example/controller/ReservationController.java">@RestController
@RequestMapping("/api/reservation")
public class ReservationController {
@GetMapping("/list")
public Result list(ReservationQueryDTO query) {
Page<Reservation> page = new Page<>(query.getPage(), query.getSize());
// 构建查询条件
QueryWrapper<Reservation> wrapper = new QueryWrapper<Reservation>()
.eq(query.getFacilityId() != null, "facility_id", query.getFacilityId())
.eq(query.getStatus() != null, "status", query.getStatus())
.ge(query.getStartDate() != null, "date", query.getStartDate())
.le(query.getEndDate() != null, "date", query.getEndDate())
.orderByDesc("create_time");
// 如果不是管理员,只能查看自己的预约
if (!isAdmin()) {
wrapper.eq("user_id", getCurrentUserId());
}
// 分页查询
Page<ReservationVO> result = reservationService.page(page, wrapper)
.convert(reservation -> {
ReservationVO vo = new ReservationVO();
BeanUtils.copyProperties(reservation, vo);
// 设置关联信息
vo.setFacilityName(facilityService.getById(reservation.getFacilityId()).getName());
vo.setUserName(userService.getById(reservation.getUserId()).getRealName());
return vo;
});
return Result.success(result);
}
@PostMapping("/cancel/{id}")
public Result cancel(@PathVariable Long id) {
Reservation reservation = reservationService.getById(id);
// 检查是否可以取消
if (reservation.getStatus() != 0 && reservation.getStatus() != 1) {
return Result.error("当前状态不可取消");
}
// 检查权限
if (!isAdmin() && !reservation.getUserId().equals(getCurrentUserId())) {
return Result.error("无权操作");
}
// 更新状态
reservation.setStatus(3);
reservationService.updateById(reservation);
return Result.success();
}
}
4. 预约统计分析
java:src/main/java/com/example/service/impl/ReservationStatServiceImpl.java">@Service
public class ReservationStatServiceImpl implements ReservationStatService {
@Override
public Map<String, Object> getStatistics(String startDate, String endDate) {
Map<String, Object> result = new HashMap<>();
// 按设施类型统计预约次数
List<Map<String, Object>> typeStats = baseMapper.selectMaps(
new QueryWrapper<Reservation>()
.select("f.type", "count(*) as count")
.leftJoin("facility f", "facility_id = f.id")
.between("date", startDate, endDate)
.eq("status", 1)
.groupBy("f.type")
);
result.put("typeStats", typeStats);
// 按时段统计使用率
List<Map<String, Object>> timeStats = baseMapper.selectMaps(
new QueryWrapper<Reservation>()
.select("DATE_FORMAT(start_time, '%H:00') as time_slot", "count(*) as count")
.between("date", startDate, endDate)
.eq("status", 1)
.groupBy("time_slot")
.orderByAsc("time_slot")
);
result.put("timeStats", timeStats);
return result;
}
}
WebSocket客服对话功能实现
一、数据库设计
1. 会话和消息表
-- 会话表
CREATE TABLE `chat_session` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '用户ID',
`customer_service_id` bigint COMMENT '客服ID',
`status` tinyint NOT NULL COMMENT '状态:0等待中 1进行中 2已结束',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`end_time` datetime COMMENT '结束时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_cs_id` (`customer_service_id`)
);
-- 消息表
CREATE TABLE `chat_message` (
`id` bigint NOT NULL AUTO_INCREMENT,
`session_id` bigint NOT NULL COMMENT '会话ID',
`sender_id` bigint NOT NULL COMMENT '发送者ID',
`sender_type` tinyint NOT NULL COMMENT '发送者类型:1用户 2客服',
`content` text NOT NULL COMMENT '消息内容',
`msg_type` tinyint NOT NULL COMMENT '消息类型:1文本 2图片 3文件',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_session_id` (`session_id`)
);
二、WebSocket配置
1. WebSocket配置类
java:src/main/java/com/example/config/WebSocketConfig.java">@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private ChatWebSocketHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler, "/ws/chat")
.setAllowedOrigins("*")
.addInterceptors(new ChatHandshakeInterceptor());
}
}
2. WebSocket拦截器
java:src/main/java/com/example/websocket/ChatHandshakeInterceptor.java">public class ChatHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 获取token,验证用户身份
String token = request.getHeaders().getFirst("Authorization");
String userId = JwtUtil.getUserId(token);
if (StringUtils.isEmpty(userId)) {
return false;
}
// 将用户信息存入attributes
attributes.put("userId", userId);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}
三、WebSocket处理器
1. 消息处理器
java:src/main/java/com/example/websocket/ChatWebSocketHandler.java">@Component
@Slf4j
public class ChatWebSocketHandler extends TextWebSocketHandler {
@Autowired
private ChatSessionService chatSessionService;
@Autowired
private ChatMessageService chatMessageService;
// 保存所有客户端连接
private static final Map<String, WebSocketSession> userSessions = new ConcurrentHashMap<>();
private static final Map<String, WebSocketSession> customerServiceSessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = getUserId(session);
String userType = getUserType(session);
if ("USER".equals(userType)) {
userSessions.put(userId, session);
// 为用户分配客服
assignCustomerService(userId);
} else if ("CUSTOMER_SERVICE".equals(userType)) {
customerServiceSessions.put(userId, session);
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
try {
// 解析消息
ChatMessageDTO messageDTO = JSON.parseObject(message.getPayload(), ChatMessageDTO.class);
// 保存消息到数据库
ChatMessage chatMessage = saveChatMessage(messageDTO);
// 转发消息给接收方
forwardMessage(chatMessage);
} catch (Exception e) {
log.error("处理消息失败", e);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String userId = getUserId(session);
String userType = getUserType(session);
if ("USER".equals(userType)) {
userSessions.remove(userId);
// 结束会话
endChatSession(userId);
} else if ("CUSTOMER_SERVICE".equals(userType)) {
customerServiceSessions.remove(userId);
// 重新分配该客服的会话
reassignSessions(userId);
}
}
private void assignCustomerService(String userId) {
// 查找空闲的客服
String csId = findAvailableCustomerService();
if (csId != null) {
// 创建会话
ChatSession session = chatSessionService.createSession(userId, csId);
// 通知双方会话建立
notifySessionCreated(session);
} else {
// 没有可用客服,将用户加入等待队列
addToWaitingQueue(userId);
}
}
private void forwardMessage(ChatMessage message) throws IOException {
String receiverId = message.getSenderType() == 1 ?
message.getSession().getCustomerServiceId().toString() :
message.getSession().getUserId().toString();
WebSocketSession receiverSession = message.getSenderType() == 1 ?
customerServiceSessions.get(receiverId) :
userSessions.get(receiverId);
if (receiverSession != null && receiverSession.isOpen()) {
receiverSession.sendMessage(new TextMessage(JSON.toJSONString(message)));
}
}
}
四、客服服务实现
1. 会话服务
java:src/main/java/com/example/service/impl/ChatSessionServiceImpl.java">@Service
public class ChatSessionServiceImpl implements ChatSessionService {
@Override
@Transactional
public ChatSession createSession(String userId, String csId) {
ChatSession session = new ChatSession();
session.setUserId(Long.valueOf(userId));
session.setCustomerServiceId(Long.valueOf(csId));
session.setStatus(1);
save(session);
// 创建欢迎消息
ChatMessage welcomeMsg = new ChatMessage();
welcomeMsg.setSessionId(session.getId());
welcomeMsg.setSenderId(Long.valueOf(csId));
welcomeMsg.setSenderType(2);
welcomeMsg.setContent("您好,很高兴为您服务!");
welcomeMsg.setMsgType(1);
chatMessageService.save(welcomeMsg);
return session;
}
@Override
public void endSession(Long sessionId) {
update()
.set("status", 2)
.set("end_time", new Date())
.eq("id", sessionId)
.update();
}
@Override
public List<ChatSessionVO> getCustomerServiceSessions(Long csId) {
return baseMapper.selectSessionList(new QueryWrapper<ChatSession>()
.eq("customer_service_id", csId)
.eq("status", 1)
.orderByDesc("create_time"));
}
}
2. 消息服务
java:src/main/java/com/example/service/impl/ChatMessageServiceImpl.java">@Service
public class ChatMessageServiceImpl implements ChatMessageService {
@Override
public List<ChatMessageVO> getSessionMessages(Long sessionId) {
return baseMapper.selectList(new QueryWrapper<ChatMessage>()
.eq("session_id", sessionId)
.orderByAsc("create_time"))
.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
}
@Override
public void saveMessage(ChatMessageDTO dto) {
ChatMessage message = new ChatMessage();
BeanUtils.copyProperties(dto, message);
// 处理特殊消息类型
if (message.getMsgType() == 2) {
// 图片消息,保存图片
String imageUrl = uploadImage(dto.getImageData());
message.setContent(imageUrl);
} else if (message.getMsgType() == 3) {
// 文件消息,保存文件
String fileUrl = uploadFile(dto.getFileData());
message.setContent(fileUrl);
}
save(message);
}
}
五、前端实现
1. WebSocket连接管理
javascript:src/utils/websocket.js">class ChatWebSocket {
constructor() {
this.ws = null;
this.messageCallbacks = [];
}
connect() {
const token = localStorage.getItem('token');
this.ws = new WebSocket(`ws://localhost:8080/ws/chat`);
this.ws.onopen = () => {
console.log('WebSocket连接成功');
// 发送认证信息
this.ws.send(JSON.stringify({
type: 'AUTH',
token: token
}));
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.messageCallbacks.forEach(callback => callback(message));
};
this.ws.onclose = () => {
console.log('WebSocket连接关闭');
// 尝试重连
setTimeout(() => this.connect(), 3000);
};
}
sendMessage(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
onMessage(callback) {
this.messageCallbacks.push(callback);
}
}
export default new ChatWebSocket();
2. 聊天组件
<template>
<div class="chat-window">
<!-- 消息列表 -->
<div class="message-list" ref="messageList">
<div v-for="msg in messages" :key="msg.id"
:class="['message', msg.senderType === 1 ? 'message-right' : 'message-left']">
<div class="avatar">
<img :src="msg.senderType === 1 ? userAvatar : csAvatar">
</div>
<div class="content">
<template v-if="msg.msgType === 1">
{{ msg.content }}
</template>
<template v-else-if="msg.msgType === 2">
<img :src="msg.content" class="message-image">
</template>
<template v-else-if="msg.msgType === 3">
<div class="file-message" @click="downloadFile(msg.content)">
<i class="el-icon-document"></i>
<span>{{ getFileName(msg.content) }}</span>
</div>
</template>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<div class="toolbar">
<el-upload
action="/api/chat/upload"
:show-file-list="false"
:before-upload="beforeImageUpload">
<i class="el-icon-picture"></i>
</el-upload>
<el-upload
action="/api/chat/upload"
:show-file-list="false"
:before-upload="beforeFileUpload">
<i class="el-icon-folder"></i>
</el-upload>
</div>
<el-input
v-model="inputContent"
type="textarea"
:rows="3"
placeholder="请输入消息"
@keyup.enter.native="sendMessage">
</el-input>
<el-button type="primary" @click="sendMessage">发送</el-button>
</div>
</div>
</template>
<script>
import chatWebSocket from '@/utils/websocket';
export default {
data() {
return {
messages: [],
inputContent: '',
sessionId: null
};
},
created() {
chatWebSocket.connect();
chatWebSocket.onMessage(this.handleMessage);
},
methods: {
handleMessage(message) {
this.messages.push(message);
this.$nextTick(() => {
this.scrollToBottom();
});
},
sendMessage() {
if (!this.inputContent.trim()) return;
const message = {
sessionId: this.sessionId,
content: this.inputContent,
msgType: 1,
senderType: 1
};
chatWebSocket.sendMessage(message);
this.inputContent = '';
},
beforeImageUpload(file) {
const isImage = file.type.startsWith('image/');
if (!isImage) {
this.$message.error('只能上传图片文件!');
return false;
}
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const message = {
sessionId: this.sessionId,
msgType: 2,
senderType: 1,
imageData: reader.result
};
chatWebSocket.sendMessage(message);
};
return false;
},
scrollToBottom() {
const messageList = this.$refs.messageList;
messageList.scrollTop = messageList.scrollHeight;
}
}
};
</script>
<style scoped>
.chat-window {
height: 600px;
display: flex;
flex-direction: column;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
display: flex;
margin-bottom: 20px;
}
.message-right {
flex-direction: row-reverse;
}
/* 其他样式省略 */
</style>