mirror of
https://gitee.com/czh-dev/upload-hub
synced 2026-09-05 23:47:46 +08:00
feat:使用SSE实现轻量消息推送
This commit is contained in:
@@ -3,10 +3,12 @@ package cn.czh;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import org.springframework.web.filter.CorsFilter;
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
|
||||||
|
@EnableAsync
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class Main {
|
public class Main {
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package cn.czh.controller;
|
package cn.czh.controller;
|
||||||
|
|
||||||
import cn.czh.base.Result;
|
import cn.czh.base.Result;
|
||||||
|
import cn.czh.dto.NotifyMessage;
|
||||||
import cn.czh.entity.StorageConfig;
|
import cn.czh.entity.StorageConfig;
|
||||||
import cn.czh.service.IAuthService;
|
import cn.czh.service.IAuthService;
|
||||||
import cn.czh.service.IFileService;
|
import cn.czh.service.IFileService;
|
||||||
|
import cn.czh.service.ISseService;
|
||||||
import cn.czh.service.IStorageConfigService;
|
import cn.czh.service.IStorageConfigService;
|
||||||
import cn.czh.utils.FileTypeUtil;
|
import cn.czh.utils.FileTypeUtil;
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
@@ -24,6 +27,7 @@ import java.net.NetworkInterface;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@@ -36,6 +40,8 @@ public class FileController {
|
|||||||
private IFileService fileService;
|
private IFileService fileService;
|
||||||
@Resource
|
@Resource
|
||||||
private IAuthService authService;
|
private IAuthService authService;
|
||||||
|
@Resource
|
||||||
|
private ISseService sseService;
|
||||||
|
|
||||||
@Value("${server.port}")
|
@Value("${server.port}")
|
||||||
private String serverPort;
|
private String serverPort;
|
||||||
@@ -147,6 +153,8 @@ public class FileController {
|
|||||||
return Result.error("非主用户不能修改分享状态");
|
return Result.error("非主用户不能修改分享状态");
|
||||||
}
|
}
|
||||||
enableShare = enable;
|
enableShare = enable;
|
||||||
|
Map<String, Object> data = MapUtil.of("enable", enable);
|
||||||
|
sseService.notifySystemEvent(new NotifyMessage("enableShare", data));
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package cn.czh.controller;
|
||||||
|
|
||||||
|
import cn.czh.service.ISseService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/sse")
|
||||||
|
public class SseController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ISseService sseService;
|
||||||
|
|
||||||
|
@GetMapping("/subscribeSharedFiles")
|
||||||
|
public SseEmitter subscribeSharedFiles(HttpServletRequest request) {
|
||||||
|
String remoteAddr = request.getRemoteAddr();
|
||||||
|
return sseService.subscribeSharedFiles(remoteAddr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/subscribeSystemEvent")
|
||||||
|
public SseEmitter subscribeSystemEvent(HttpServletRequest request) {
|
||||||
|
String remoteAddr = request.getRemoteAddr();
|
||||||
|
return sseService.subscribeSystemEvent(remoteAddr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package cn.czh.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Data
|
||||||
|
public class NotifyMessage {
|
||||||
|
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
private Map<String, Object> data;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cn.czh.service;
|
||||||
|
|
||||||
|
import cn.czh.dto.NotifyMessage;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
public interface ISseService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅共享文件更新
|
||||||
|
*/
|
||||||
|
SseEmitter subscribeSharedFiles(String clientId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅系统事件
|
||||||
|
*/
|
||||||
|
SseEmitter subscribeSystemEvent(String clientId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知共享文件更新
|
||||||
|
*/
|
||||||
|
void notifySharedFileUpdate(String message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知系统事件
|
||||||
|
*/
|
||||||
|
void notifySystemEvent(NotifyMessage message);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
package cn.czh.service.impl;
|
package cn.czh.service.impl;
|
||||||
|
|
||||||
|
import cn.czh.dto.NotifyMessage;
|
||||||
import cn.czh.entity.UserConfig;
|
import cn.czh.entity.UserConfig;
|
||||||
import cn.czh.mapper.UserConfigMapper;
|
import cn.czh.mapper.UserConfigMapper;
|
||||||
import cn.czh.service.IAuthService;
|
import cn.czh.service.IAuthService;
|
||||||
|
import cn.czh.service.ISseService;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AuthServiceImpl implements IAuthService {
|
public class AuthServiceImpl implements IAuthService {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private UserConfigMapper userConfigMapper;
|
private UserConfigMapper userConfigMapper;
|
||||||
|
@Resource
|
||||||
|
private ISseService sseService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMainUserPassword() {
|
public String getMainUserPassword() {
|
||||||
@@ -48,6 +54,11 @@ public class AuthServiceImpl implements IAuthService {
|
|||||||
} else {
|
} else {
|
||||||
userConfigMapper.updateById(user);
|
userConfigMapper.updateById(user);
|
||||||
}
|
}
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("password", password);
|
||||||
|
NotifyMessage message = new NotifyMessage("setPassword", data);
|
||||||
|
|
||||||
|
sseService.notifySystemEvent(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import cn.czh.entity.UploadFile;
|
|||||||
import cn.czh.mapper.ShareFileMapper;
|
import cn.czh.mapper.ShareFileMapper;
|
||||||
import cn.czh.mapper.UploadFileMapper;
|
import cn.czh.mapper.UploadFileMapper;
|
||||||
import cn.czh.service.IFileService;
|
import cn.czh.service.IFileService;
|
||||||
|
import cn.czh.service.ISseService;
|
||||||
|
import cn.hutool.json.JSON;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
@@ -24,6 +28,8 @@ public class FileServiceImpl implements IFileService {
|
|||||||
private UploadFileMapper uploadFileMapper;
|
private UploadFileMapper uploadFileMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private ShareFileMapper shareFileMapper;
|
private ShareFileMapper shareFileMapper;
|
||||||
|
@Resource
|
||||||
|
private ISseService sseService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<UploadFile> pageFiles(Integer page, Integer pageSize, String storageType, String fileName) {
|
public IPage<UploadFile> pageFiles(Integer page, Integer pageSize, String storageType, String fileName) {
|
||||||
@@ -70,12 +76,14 @@ public class FileServiceImpl implements IFileService {
|
|||||||
sharedFile.setFileIdentifier(identifier);
|
sharedFile.setFileIdentifier(identifier);
|
||||||
sharedFile.setCreateTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
sharedFile.setCreateTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||||
shareFileMapper.insert(sharedFile);
|
shareFileMapper.insert(sharedFile);
|
||||||
|
sseService.notifySharedFileUpdate("add");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeSharedFile(String fileIdentifier) {
|
public void removeSharedFile(String fileIdentifier) {
|
||||||
shareFileMapper.delete(Wrappers.lambdaQuery(SharedFile.class).eq(SharedFile::getFileIdentifier, fileIdentifier));
|
shareFileMapper.delete(Wrappers.lambdaQuery(SharedFile.class).eq(SharedFile::getFileIdentifier, fileIdentifier));
|
||||||
|
sseService.notifySharedFileUpdate("remove");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package cn.czh.service.impl;
|
||||||
|
|
||||||
|
import cn.czh.dto.NotifyMessage;
|
||||||
|
import cn.czh.service.ISseService;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class SseServiceImpl implements ISseService {
|
||||||
|
|
||||||
|
private final Map<String, SseEmitter> sharedFilesEmitters = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, SseEmitter> systemEventEmitters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SseEmitter subscribeSharedFiles(String clientId) {
|
||||||
|
SseEmitter emitter = new SseEmitter(0L); // 设置超时时间
|
||||||
|
sharedFilesEmitters.put(clientId, emitter);
|
||||||
|
|
||||||
|
emitter.onCompletion(() -> sharedFilesEmitters.remove(clientId));
|
||||||
|
emitter.onTimeout(() -> sharedFilesEmitters.remove(clientId));
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SseEmitter subscribeSystemEvent(String clientId) {
|
||||||
|
SseEmitter emitter = new SseEmitter(0L); // 设置超时时间
|
||||||
|
systemEventEmitters.put(clientId, emitter);
|
||||||
|
|
||||||
|
emitter.onCompletion(() -> systemEventEmitters.remove(clientId));
|
||||||
|
emitter.onTimeout(() -> systemEventEmitters.remove(clientId));
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
@Override
|
||||||
|
public void notifySharedFileUpdate(String message) {
|
||||||
|
sharedFilesEmitters.forEach((clientId, emitter) ->
|
||||||
|
sendNotification(clientId, emitter, "sharedFileUpdate", message, sharedFilesEmitters));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
@Override
|
||||||
|
public void notifySystemEvent(NotifyMessage message) {
|
||||||
|
systemEventEmitters.forEach((clientId, emitter) ->
|
||||||
|
sendNotification(clientId, emitter, "systemEvent", JSONUtil.toJsonStr(message), systemEventEmitters));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendNotification(String clientId, SseEmitter emitter, String event, String message, Map<String, SseEmitter> emittersMap) {
|
||||||
|
try {
|
||||||
|
if (emitter != null) {
|
||||||
|
emitter.send(SseEmitter.event().name(event).data(message));
|
||||||
|
} else {
|
||||||
|
log.warn("Emitter for client {} is disposed or null, skipping notification", clientId);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error sending notification to client: {}", clientId, e);
|
||||||
|
emittersMap.remove(clientId); // 从对应的Map中移除
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Unexpected error sending notification to client: {}", clientId, e);
|
||||||
|
emittersMap.remove(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getSharedFiles, enableShare, getShareStatus, shareAddress, unShareFile } from '@/utils/api';
|
import { getSharedFiles, enableShare, getShareStatus, shareAddress, unShareFile, subscribeSharedFiles, subscribeSystemEvent } from '@/utils/api';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ShareFile',
|
name: 'ShareFile',
|
||||||
@@ -79,12 +79,15 @@ export default {
|
|||||||
protocol: 'IPv4',
|
protocol: 'IPv4',
|
||||||
fileList: [],
|
fileList: [],
|
||||||
enableShare: false,
|
enableShare: false,
|
||||||
|
isAdmin: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchFiles();
|
this.fetchFiles();
|
||||||
this.getShareStatus();
|
this.getShareStatus();
|
||||||
this.getShareAddress();
|
this.getShareAddress();
|
||||||
|
this.subscribeSharedFiles();
|
||||||
|
this.subscribeSystemEvent();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async getShareAddress() {
|
async getShareAddress() {
|
||||||
@@ -109,6 +112,20 @@ export default {
|
|||||||
this.fileList = res.data;
|
this.fileList = res.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
subscribeSharedFiles() {
|
||||||
|
subscribeSharedFiles(() => {
|
||||||
|
this.fetchFiles();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
subscribeSystemEvent() {
|
||||||
|
subscribeSystemEvent((event) => {
|
||||||
|
if (event.type === 'enableShare') {
|
||||||
|
this.fileList = [];
|
||||||
|
this.enableShare = event.data.enable;
|
||||||
|
this.fetchFiles();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async downloadFile(file) {
|
async downloadFile(file) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { http, httpExtra } from './http';
|
import { http, baseUrl, httpExtra } from './http';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取上传进度
|
* 获取上传进度
|
||||||
@@ -269,6 +269,59 @@ const setPassword = (password) => {
|
|||||||
formData.append('password', password);
|
formData.append('password', password);
|
||||||
return http.post("/config/setPassword", formData);
|
return http.post("/config/setPassword", formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅共享文件更新
|
||||||
|
* @param {Function} callback 文件更新的回调函数
|
||||||
|
* @returns {EventSource} 返回 EventSource 实例
|
||||||
|
*/
|
||||||
|
const subscribeSharedFiles = (callback) => {
|
||||||
|
const eventSource = new EventSource(`${baseUrl}/sse/subscribeSharedFiles`);
|
||||||
|
|
||||||
|
eventSource.addEventListener('sharedFileUpdate', (event) => {
|
||||||
|
callback(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error("SSE连接异常:", error);
|
||||||
|
if (eventSource.readyState === EventSource.CLOSED) {
|
||||||
|
console.log("连接已关闭");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return eventSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅系统事件更新
|
||||||
|
* @param {Function} callback 文件更新的回调函数
|
||||||
|
* @returns {EventSource} 返回 EventSource 实例
|
||||||
|
*/
|
||||||
|
const subscribeSystemEvent = (callback) => {
|
||||||
|
const eventSource = new EventSource(`${baseUrl}/sse/subscribeSystemEvent`);
|
||||||
|
|
||||||
|
eventSource.addEventListener('systemEvent', (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
callback(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('JSON解析失败:', e, '原始数据:', event.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
console.log('[SSE] Received default message:', event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error("SSE连接异常:", error);
|
||||||
|
if (eventSource.readyState === EventSource.CLOSED) {
|
||||||
|
console.log("连接已关闭");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getUploadProgress,
|
getUploadProgress,
|
||||||
createMultipartUpload,
|
createMultipartUpload,
|
||||||
@@ -289,5 +342,7 @@ export {
|
|||||||
setPassword,
|
setPassword,
|
||||||
addSharedFile,
|
addSharedFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
|
subscribeSharedFiles,
|
||||||
|
subscribeSystemEvent,
|
||||||
httpExtra
|
httpExtra
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ http.interceptors.response.use(
|
|||||||
response => {
|
response => {
|
||||||
try {
|
try {
|
||||||
if (response.data && response.data.code !== 200) {
|
if (response.data && response.data.code !== 200) {
|
||||||
console.log(response.data);
|
|
||||||
Message.error(response.data.msg || '请求失败');
|
Message.error(response.data.msg || '请求失败');
|
||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
Reference in New Issue
Block a user