temp jitsi
This commit is contained in:
@@ -268,6 +268,100 @@ public class WorkcaseChatContorller {
|
||||
return chatRoomService.assignCustomerService(roomId);
|
||||
}
|
||||
|
||||
// ========================= 视频会议管理(Jitsi Meet) =========================
|
||||
|
||||
@Autowired
|
||||
private org.xyzh.api.workcase.service.VideoMeetingService videoMeetingService;
|
||||
|
||||
@Operation(summary = "创建视频会议")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PostMapping("/meeting/create")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> createVideoMeeting(
|
||||
@RequestBody org.xyzh.api.workcase.dto.TbVideoMeetingDTO meetingDTO) {
|
||||
ValidationResult vr = ValidationUtils.validate(meetingDTO, Arrays.asList(
|
||||
ValidationUtils.requiredString("roomId", "聊天室ID")
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
|
||||
try {
|
||||
return videoMeetingService.createMeeting(meetingDTO);
|
||||
} catch (Exception e) {
|
||||
return ResultDomain.failure(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会议信息")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@GetMapping("/meeting/{meetingId}")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> getMeetingInfo(
|
||||
@PathVariable(value = "meetingId") String meetingId) {
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
|
||||
try {
|
||||
return videoMeetingService.getMeetingInfo(meetingId, userId);
|
||||
} catch (Exception e) {
|
||||
return ResultDomain.failure(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "加入会议(生成用户专属JWT)")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PostMapping("/meeting/{meetingId}/join")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> joinMeeting(
|
||||
@PathVariable(value = "meetingId") String meetingId) {
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
|
||||
// 验证加入权限
|
||||
ResultDomain<Boolean> accessCheck = videoMeetingService.validateMeetingAccess(meetingId, userId);
|
||||
if (!accessCheck.getSuccess() || !accessCheck.getData()) {
|
||||
return ResultDomain.failure("您无权加入此会议");
|
||||
}
|
||||
|
||||
// 生成用户专属的iframe URL(包含JWT Token)
|
||||
try {
|
||||
return videoMeetingService.generateUserMeetingUrl(meetingId, userId);
|
||||
} catch (Exception e) {
|
||||
return ResultDomain.failure(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "开始会议")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PostMapping("/meeting/{meetingId}/start")
|
||||
public ResultDomain<Boolean> startMeeting(@PathVariable(value = "meetingId") String meetingId) {
|
||||
try {
|
||||
return videoMeetingService.startMeeting(meetingId);
|
||||
} catch (Exception e) {
|
||||
return ResultDomain.failure(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "结束会议")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@PostMapping("/meeting/{meetingId}/end")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> endMeeting(
|
||||
@PathVariable(value = "meetingId") String meetingId) {
|
||||
try {
|
||||
return videoMeetingService.endMeeting(meetingId);
|
||||
} catch (Exception e) {
|
||||
return ResultDomain.failure(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取聊天室当前活跃会议")
|
||||
@PreAuthorize("hasAuthority('workcase:room:meeting')")
|
||||
@GetMapping("/meeting/room/{roomId}/active")
|
||||
public ResultDomain<org.xyzh.api.workcase.vo.VideoMeetingVO> getActiveMeetingByRoom(
|
||||
@PathVariable(value = "roomId") String roomId) {
|
||||
try {
|
||||
return videoMeetingService.getActiveMeetingByRoom(roomId);
|
||||
} catch (Exception e) {
|
||||
return ResultDomain.failure(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= 微信客服消息回调 =========================
|
||||
|
||||
// @Operation(summary = "微信客服消息回调验证(GET)")
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package org.xyzh.workcase.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.xyzh.api.workcase.service.JitsiTokenService;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description Jitsi Meet JWT Token服务实现类
|
||||
* @filename JitsiTokenServiceImpl.java
|
||||
* @author claude
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-25
|
||||
*/
|
||||
@DubboService(version = "1.0.0", group = "workcase", timeout = 30000, retries = 0)
|
||||
public class JitsiTokenServiceImpl implements JitsiTokenService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(JitsiTokenServiceImpl.class);
|
||||
|
||||
@Value("${jitsi.app.id:urbanLifeline}")
|
||||
private String jitsiAppId;
|
||||
|
||||
@Value("${jitsi.app.secret:your-secret-key-change-in-production}")
|
||||
private String jitsiAppSecret;
|
||||
|
||||
@Value("${jitsi.server.url:https://meet.jit.si}")
|
||||
private String jitsiServerUrl;
|
||||
|
||||
@Value("${jitsi.token.expiration:7200000}")
|
||||
private Long tokenExpiration; // 默认2小时
|
||||
|
||||
@Override
|
||||
public String generateJwtToken(String roomName, String userId, String userName, boolean isModerator) {
|
||||
logger.info("生成Jitsi JWT Token: roomName={}, userId={}, userName={}, isModerator={}",
|
||||
roomName, userId, userName, isModerator);
|
||||
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
long exp = now + tokenExpiration;
|
||||
|
||||
// 构建用户上下文
|
||||
Map<String, Object> userContext = new HashMap<>();
|
||||
userContext.put("id", userId);
|
||||
userContext.put("name", userName);
|
||||
userContext.put("moderator", isModerator);
|
||||
|
||||
// 构建JWT claims
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("context", Map.of("user", userContext));
|
||||
claims.put("room", roomName);
|
||||
claims.put("iss", jitsiAppId);
|
||||
claims.put("aud", "jitsi");
|
||||
claims.put("sub", jitsiServerUrl);
|
||||
claims.put("exp", exp / 1000); // 秒级时间戳
|
||||
claims.put("nbf", now / 1000);
|
||||
|
||||
// 生成JWT Token
|
||||
String token = Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setIssuedAt(new Date(now))
|
||||
.setExpiration(new Date(exp))
|
||||
.signWith(SignatureAlgorithm.HS256, jitsiAppSecret.getBytes())
|
||||
.compact();
|
||||
|
||||
logger.info("JWT Token生成成功: roomName={}", roomName);
|
||||
return token;
|
||||
} catch (Exception e) {
|
||||
logger.error("生成JWT Token失败: roomName={}, error={}", roomName, e.getMessage(), e);
|
||||
throw new RuntimeException("生成JWT Token失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateJwtToken(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(jitsiAppSecret.getBytes())
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
// 检查过期时间
|
||||
Date expiration = claims.getExpiration();
|
||||
return expiration.after(new Date());
|
||||
} catch (Exception e) {
|
||||
logger.warn("JWT Token验证失败: error={}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildIframeUrl(String roomName, String jwtToken, JSONObject config) {
|
||||
logger.info("构建Jitsi iframe URL: roomName={}", roomName);
|
||||
|
||||
StringBuilder url = new StringBuilder();
|
||||
url.append(jitsiServerUrl).append("/").append(roomName);
|
||||
|
||||
// 添加JWT Token
|
||||
url.append("?jwt=").append(jwtToken);
|
||||
|
||||
// 添加默认配置
|
||||
url.append("&config.startWithAudioMuted=false");
|
||||
url.append("&config.startWithVideoMuted=false");
|
||||
url.append("&config.enableWelcomePage=false");
|
||||
url.append("&config.prejoinPageEnabled=false");
|
||||
url.append("&config.disableDeepLinking=true");
|
||||
url.append("&config.enableChat=true");
|
||||
url.append("&config.enableScreenSharing=true");
|
||||
|
||||
// 界面配置
|
||||
url.append("&interfaceConfig.SHOW_JITSI_WATERMARK=false");
|
||||
url.append("&interfaceConfig.SHOW_WATERMARK_FOR_GUESTS=false");
|
||||
url.append("&interfaceConfig.DISABLE_JOIN_LEAVE_NOTIFICATIONS=false");
|
||||
url.append("&interfaceConfig.DEFAULT_BACKGROUND=#474747");
|
||||
|
||||
// 添加自定义配置
|
||||
if (config != null && !config.isEmpty()) {
|
||||
config.forEach((key, value) -> {
|
||||
url.append("&config.").append(key).append("=").append(value);
|
||||
});
|
||||
}
|
||||
|
||||
String iframeUrl = url.toString();
|
||||
logger.info("iframe URL构建成功: url={}", iframeUrl);
|
||||
return iframeUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateRoomName(String workcaseId) {
|
||||
// 格式: workcase_{workcaseId}_{timestamp}
|
||||
String roomName = String.format("workcase_%s_%d",
|
||||
workcaseId != null ? workcaseId : "default",
|
||||
System.currentTimeMillis());
|
||||
|
||||
logger.info("生成Jitsi房间名: roomName={}", roomName);
|
||||
return roomName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package org.xyzh.workcase.service;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO;
|
||||
import org.xyzh.api.workcase.dto.TbVideoMeetingDTO;
|
||||
import org.xyzh.api.workcase.service.JitsiTokenService;
|
||||
import org.xyzh.api.workcase.service.VideoMeetingService;
|
||||
import org.xyzh.api.workcase.vo.ChatMemberVO;
|
||||
import org.xyzh.api.workcase.vo.VideoMeetingVO;
|
||||
import org.xyzh.common.auth.utils.LoginUtil;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.utils.id.IdUtil;
|
||||
import org.xyzh.workcase.mapper.TbChatRoomMemberMapper;
|
||||
import org.xyzh.workcase.mapper.TbVideoMeetingMapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @description 视频会议服务实现类
|
||||
* @filename VideoMeetingServiceImpl.java
|
||||
* @author claude
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-25
|
||||
*/
|
||||
@DubboService(version = "1.0.0", group = "workcase", timeout = 30000, retries = 0)
|
||||
public class VideoMeetingServiceImpl implements VideoMeetingService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(VideoMeetingServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private TbVideoMeetingMapper videoMeetingMapper;
|
||||
|
||||
@Autowired
|
||||
private TbChatRoomMemberMapper chatRoomMemberMapper;
|
||||
|
||||
@Autowired
|
||||
private JitsiTokenService jitsiTokenService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<VideoMeetingVO> createMeeting(TbVideoMeetingDTO meetingDTO) {
|
||||
// 获取当前用户ID
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
logger.info("创建视频会议: roomId={}, workcaseId={}, userId={}",
|
||||
meetingDTO.getRoomId(), meetingDTO.getWorkcaseId(), userId);
|
||||
|
||||
try {
|
||||
// 1. 验证用户是否为聊天室成员
|
||||
if (!isMemberOfRoom(meetingDTO.getRoomId(), userId)) {
|
||||
logger.warn("用户不是聊天室成员,无法创建会议: roomId={}, userId={}",
|
||||
meetingDTO.getRoomId(), userId);
|
||||
return ResultDomain.failure("您不是聊天室成员,无法创建会议");
|
||||
}
|
||||
|
||||
// 2. 检查聊天室是否已有进行中的会议
|
||||
TbVideoMeetingDTO existingMeetingFilter = new TbVideoMeetingDTO();
|
||||
existingMeetingFilter.setRoomId(meetingDTO.getRoomId());
|
||||
existingMeetingFilter.setStatus("ongoing");
|
||||
List<VideoMeetingVO> existingMeetings = videoMeetingMapper.selectVideoMeetingList(existingMeetingFilter);
|
||||
|
||||
if (existingMeetings != null && !existingMeetings.isEmpty()) {
|
||||
logger.warn("聊天室已有进行中的会议: roomId={}", meetingDTO.getRoomId());
|
||||
return ResultDomain.failure("聊天室已有进行中的会议,请稍后再试");
|
||||
}
|
||||
|
||||
// 3. 生成会议ID和房间名
|
||||
String meetingId = IdUtil.generateUUID();
|
||||
String jitsiRoomName = jitsiTokenService.generateRoomName(meetingDTO.getWorkcaseId());
|
||||
|
||||
// 4. 获取用户信息(从聊天室成员表)
|
||||
TbChatRoomMemberDTO memberFilter = new TbChatRoomMemberDTO();
|
||||
memberFilter.setRoomId(meetingDTO.getRoomId());
|
||||
memberFilter.setUserId(userId);
|
||||
List<ChatMemberVO> members = chatRoomMemberMapper.selectChatRoomMemberList(memberFilter);
|
||||
|
||||
String userName = "用户";
|
||||
String userType = "guest";
|
||||
if (members != null && !members.isEmpty()) {
|
||||
ChatMemberVO member = members.get(0);
|
||||
userName = member.getUserName();
|
||||
userType = member.getUserType();
|
||||
}
|
||||
|
||||
// 5. 生成创建者的JWT Token(创建者默认为主持人)
|
||||
String jwtToken = jitsiTokenService.generateJwtToken(
|
||||
jitsiRoomName,
|
||||
userId,
|
||||
userName,
|
||||
true // 创建者为主持人
|
||||
);
|
||||
|
||||
// 6. 构建iframe URL
|
||||
String iframeUrl = jitsiTokenService.buildIframeUrl(jitsiRoomName, jwtToken, meetingDTO.getConfig());
|
||||
|
||||
// 7. 填充会议信息
|
||||
meetingDTO.setMeetingId(meetingId);
|
||||
meetingDTO.setJitsiRoomName(jitsiRoomName);
|
||||
meetingDTO.setJwtToken(jwtToken); // 存储创建者的token(可选)
|
||||
meetingDTO.setIframeUrl(iframeUrl);
|
||||
meetingDTO.setStatus("scheduled");
|
||||
meetingDTO.setCreatorId(userId);
|
||||
meetingDTO.setCreatorType(userType);
|
||||
meetingDTO.setCreatorName(userName);
|
||||
meetingDTO.setParticipantCount(0);
|
||||
meetingDTO.setOptsn(IdUtil.getOptsn());
|
||||
|
||||
if (meetingDTO.getMaxParticipants() == null) {
|
||||
meetingDTO.setMaxParticipants(10);
|
||||
}
|
||||
|
||||
// 8. 插入数据库
|
||||
int rows = videoMeetingMapper.insertVideoMeeting(meetingDTO);
|
||||
if (rows > 0) {
|
||||
logger.info("视频会议创建成功: meetingId={}, jitsiRoomName={}",
|
||||
meetingId, jitsiRoomName);
|
||||
|
||||
// 9. 返回VO
|
||||
VideoMeetingVO meetingVO = new VideoMeetingVO();
|
||||
BeanUtils.copyProperties(meetingDTO, meetingVO);
|
||||
return ResultDomain.success("创建会议成功", meetingVO);
|
||||
} else {
|
||||
logger.error("插入会议记录失败: meetingId={}", meetingId);
|
||||
return ResultDomain.failure("创建会议失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("创建视频会议异常: roomId={}, error={}",
|
||||
meetingDTO.getRoomId(), e.getMessage(), e);
|
||||
return ResultDomain.failure("创建会议失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<VideoMeetingVO> getMeetingInfo(String meetingId, String userId) {
|
||||
logger.info("获取会议信息: meetingId={}, userId={}", meetingId, userId);
|
||||
|
||||
try {
|
||||
TbVideoMeetingDTO filter = new TbVideoMeetingDTO();
|
||||
filter.setMeetingId(meetingId);
|
||||
List<VideoMeetingVO> meetings = videoMeetingMapper.selectVideoMeetingList(filter);
|
||||
|
||||
if (meetings == null || meetings.isEmpty()) {
|
||||
logger.warn("会议不存在: meetingId={}", meetingId);
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
VideoMeetingVO meeting = meetings.get(0);
|
||||
|
||||
// 验证访问权限
|
||||
ResultDomain<Boolean> accessCheck = validateMeetingAccess(meetingId, userId);
|
||||
if (!accessCheck.getSuccess() || !accessCheck.getData()) {
|
||||
logger.warn("用户无权访问会议: meetingId={}, userId={}", meetingId, userId);
|
||||
return ResultDomain.failure("您无权访问此会议");
|
||||
}
|
||||
|
||||
logger.info("获取会议信息成功: meetingId={}", meetingId);
|
||||
return ResultDomain.success("获取会议信息成功", meeting);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取会议信息异常: meetingId={}, error={}", meetingId, e.getMessage(), e);
|
||||
return ResultDomain.failure("获取会议信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Boolean> validateMeetingAccess(String meetingId, String userId) {
|
||||
logger.info("验证会议访问权限: meetingId={}, userId={}", meetingId, userId);
|
||||
|
||||
try {
|
||||
// 1. 获取会议信息
|
||||
TbVideoMeetingDTO filter = new TbVideoMeetingDTO();
|
||||
filter.setMeetingId(meetingId);
|
||||
List<VideoMeetingVO> meetings = videoMeetingMapper.selectVideoMeetingList(filter);
|
||||
|
||||
if (meetings == null || meetings.isEmpty()) {
|
||||
logger.warn("会议不存在: meetingId={}", meetingId);
|
||||
return ResultDomain.success("会议不存在", false);
|
||||
}
|
||||
|
||||
VideoMeetingVO meeting = meetings.get(0);
|
||||
|
||||
// 2. 检查用户是否为聊天室成员
|
||||
boolean isMember = isMemberOfRoom(meeting.getRoomId(), userId);
|
||||
|
||||
logger.info("会议访问权限验证结果: meetingId={}, userId={}, hasAccess={}",
|
||||
meetingId, userId, isMember);
|
||||
return ResultDomain.success("会议访问权限验证成功", isMember);
|
||||
} catch (Exception e) {
|
||||
logger.error("验证会议访问权限异常: meetingId={}, error={}", meetingId, e.getMessage(), e);
|
||||
return ResultDomain.failure("验证访问权限失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<VideoMeetingVO> generateUserMeetingUrl(String meetingId, String userId) {
|
||||
logger.info("生成用户专属会议URL: meetingId={}, userId={}", meetingId, userId);
|
||||
|
||||
try {
|
||||
// 1. 获取会议信息
|
||||
TbVideoMeetingDTO filter = new TbVideoMeetingDTO();
|
||||
filter.setMeetingId(meetingId);
|
||||
List<VideoMeetingVO> meetings = videoMeetingMapper.selectVideoMeetingList(filter);
|
||||
|
||||
if (meetings == null || meetings.isEmpty()) {
|
||||
logger.warn("会议不存在: meetingId={}", meetingId);
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
VideoMeetingVO meeting = meetings.get(0);
|
||||
|
||||
// 2. 验证访问权限
|
||||
ResultDomain<Boolean> accessCheck = validateMeetingAccess(meetingId, userId);
|
||||
if (!accessCheck.getSuccess() || !accessCheck.getData()) {
|
||||
logger.warn("用户无权访问会议: meetingId={}, userId={}", meetingId, userId);
|
||||
return ResultDomain.failure("您无权访问此会议");
|
||||
}
|
||||
|
||||
// 3. 获取用户信息
|
||||
TbChatRoomMemberDTO memberFilter = new TbChatRoomMemberDTO();
|
||||
memberFilter.setRoomId(meeting.getRoomId());
|
||||
memberFilter.setUserId(userId);
|
||||
List<ChatMemberVO> members = chatRoomMemberMapper.selectChatRoomMemberList(memberFilter);
|
||||
|
||||
String userName = "用户";
|
||||
boolean isModerator = false;
|
||||
|
||||
if (members != null && !members.isEmpty()) {
|
||||
ChatMemberVO member = members.get(0);
|
||||
userName = member.getUserName();
|
||||
// 客服人员设为主持人
|
||||
isModerator = "agent".equals(member.getUserType());
|
||||
}
|
||||
|
||||
// 4. 生成用户专属JWT Token
|
||||
String userJwtToken = jitsiTokenService.generateJwtToken(
|
||||
meeting.getJitsiRoomName(),
|
||||
userId,
|
||||
userName,
|
||||
isModerator
|
||||
);
|
||||
|
||||
// 5. 构建用户专属iframe URL
|
||||
String userIframeUrl = jitsiTokenService.buildIframeUrl(
|
||||
meeting.getJitsiRoomName(),
|
||||
userJwtToken,
|
||||
meeting.getConfig()
|
||||
);
|
||||
|
||||
// 6. 更新VO
|
||||
meeting.setJwtToken(userJwtToken);
|
||||
meeting.setIframeUrl(userIframeUrl);
|
||||
|
||||
logger.info("生成用户专属会议URL成功: meetingId={}, userId={}", meetingId, userId);
|
||||
return ResultDomain.success("生成用户专属会议URL成功", meeting);
|
||||
} catch (Exception e) {
|
||||
logger.error("生成用户专属会议URL异常: meetingId={}, error={}", meetingId, e.getMessage(), e);
|
||||
return ResultDomain.failure("生成会议URL失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<Boolean> startMeeting(String meetingId) {
|
||||
logger.info("开始会议: meetingId={}", meetingId);
|
||||
|
||||
try {
|
||||
TbVideoMeetingDTO updateDTO = new TbVideoMeetingDTO();
|
||||
updateDTO.setMeetingId(meetingId);
|
||||
updateDTO.setStatus("ongoing");
|
||||
updateDTO.setActualStartTime(new Date());
|
||||
|
||||
int rows = videoMeetingMapper.updateVideoMeeting(updateDTO);
|
||||
if (rows > 0) {
|
||||
logger.info("会议开始成功: meetingId={}", meetingId);
|
||||
return ResultDomain.success("会议开始成功", true);
|
||||
} else {
|
||||
logger.warn("会议开始失败(可能不存在): meetingId={}", meetingId);
|
||||
return ResultDomain.failure("会议不存在或已开始");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("开始会议异常: meetingId={}, error={}", meetingId, e.getMessage(), e);
|
||||
return ResultDomain.failure("开始会议失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<VideoMeetingVO> endMeeting(String meetingId) {
|
||||
logger.info("结束会议: meetingId={}", meetingId);
|
||||
|
||||
try {
|
||||
// 1. 获取会议信息
|
||||
TbVideoMeetingDTO filter = new TbVideoMeetingDTO();
|
||||
filter.setMeetingId(meetingId);
|
||||
List<VideoMeetingVO> meetings = videoMeetingMapper.selectVideoMeetingList(filter);
|
||||
|
||||
if (meetings == null || meetings.isEmpty()) {
|
||||
logger.warn("会议不存在: meetingId={}", meetingId);
|
||||
return ResultDomain.failure("会议不存在");
|
||||
}
|
||||
|
||||
VideoMeetingVO meeting = meetings.get(0);
|
||||
|
||||
// 2. 计算会议时长
|
||||
Integer durationSeconds = null;
|
||||
if (meeting.getActualStartTime() != null) {
|
||||
long duration = (System.currentTimeMillis() - meeting.getActualStartTime().getTime()) / 1000;
|
||||
durationSeconds = (int) duration;
|
||||
}
|
||||
|
||||
// 3. 更新会议状态
|
||||
TbVideoMeetingDTO updateDTO = new TbVideoMeetingDTO();
|
||||
updateDTO.setMeetingId(meetingId);
|
||||
updateDTO.setStatus("ended");
|
||||
updateDTO.setActualEndTime(new Date());
|
||||
updateDTO.setDurationSeconds(durationSeconds);
|
||||
|
||||
int rows = videoMeetingMapper.updateVideoMeeting(updateDTO);
|
||||
if (rows > 0) {
|
||||
// 4. 更新VO
|
||||
meeting.setStatus("ended");
|
||||
meeting.setActualEndTime(new Date());
|
||||
meeting.setDurationSeconds(durationSeconds);
|
||||
|
||||
logger.info("会议结束成功: meetingId={}, duration={}秒", meetingId, durationSeconds);
|
||||
return ResultDomain.success("会议结束成功", meeting);
|
||||
} else {
|
||||
logger.warn("会议结束失败: meetingId={}", meetingId);
|
||||
return ResultDomain.failure("结束会议失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("结束会议异常: meetingId={}, error={}", meetingId, e.getMessage(), e);
|
||||
return ResultDomain.failure("结束会议失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<VideoMeetingVO> getActiveMeetingByRoom(String roomId) {
|
||||
logger.info("获取聊天室活跃会议: roomId={}", roomId);
|
||||
|
||||
try {
|
||||
TbVideoMeetingDTO filter = new TbVideoMeetingDTO();
|
||||
filter.setRoomId(roomId);
|
||||
filter.setStatus("ongoing");
|
||||
List<VideoMeetingVO> meetings = videoMeetingMapper.selectVideoMeetingList(filter);
|
||||
|
||||
if (meetings == null || meetings.isEmpty()) {
|
||||
logger.info("聊天室无活跃会议: roomId={}", roomId);
|
||||
return ResultDomain.failure("无活跃会议");
|
||||
}
|
||||
|
||||
VideoMeetingVO meeting = meetings.get(0);
|
||||
logger.info("找到活跃会议: roomId={}, meetingId={}", roomId, meeting.getMeetingId());
|
||||
return ResultDomain.success("找到活跃会议", meeting);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取活跃会议异常: roomId={}, error={}", roomId, e.getMessage(), e);
|
||||
return ResultDomain.failure("获取活跃会议失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMemberOfRoom(String roomId, String userId) {
|
||||
logger.debug("检查用户是否为聊天室成员: roomId={}, userId={}", roomId, userId);
|
||||
|
||||
try {
|
||||
TbChatRoomMemberDTO filter = new TbChatRoomMemberDTO();
|
||||
filter.setRoomId(roomId);
|
||||
filter.setUserId(userId);
|
||||
filter.setStatus("active");
|
||||
|
||||
List<ChatMemberVO> members = chatRoomMemberMapper.selectChatRoomMemberList(filter);
|
||||
boolean isMember = members != null && !members.isEmpty();
|
||||
|
||||
logger.debug("用户成员检查结果: roomId={}, userId={}, isMember={}",
|
||||
roomId, userId, isMember);
|
||||
return isMember;
|
||||
} catch (Exception e) {
|
||||
logger.error("检查用户成员身份异常: roomId={}, userId={}, error={}",
|
||||
roomId, userId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,26 @@
|
||||
package org.xyzh.workcase.service;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboReference;
|
||||
import org.apache.dubbo.config.annotation.DubboService;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.xyzh.api.file.dto.TbSysFileDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
|
||||
@@ -20,10 +35,23 @@ import org.xyzh.workcase.enums.WorkcaseProcessAction;
|
||||
import org.xyzh.workcase.mapper.TbWorkcaseDeviceMapper;
|
||||
import org.xyzh.workcase.mapper.TbWorkcaseMapper;
|
||||
import org.xyzh.workcase.mapper.TbWorkcaseProcessMapper;
|
||||
import org.xyzh.api.file.service.FileService;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomDTO;
|
||||
import org.xyzh.api.workcase.service.ChatRoomService;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.DecodeHintType;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
@DubboService(version = "1.0.0",group = "workcase",timeout = 30000,retries = 0)
|
||||
public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
@@ -41,6 +69,9 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
@Autowired
|
||||
private ChatRoomService chatRoomService;
|
||||
|
||||
@DubboReference(version = "1.0.0",group = "file",timeout = 30000,retries = 0)
|
||||
private FileService fileService;
|
||||
|
||||
// ====================== 工单管理 ======================
|
||||
|
||||
@Override
|
||||
@@ -68,7 +99,10 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
}
|
||||
// 统一由后端从登录态设置 creator,避免前端传入不可信
|
||||
workcase.setCreator(LoginUtil.getCurrentUserId());
|
||||
|
||||
|
||||
// 解析设备铭牌二维码
|
||||
// ResultDomain<TbWorkcaseDeviceDTO> deviceResult = anylizeQrCode(workcase.getDeviceNamePlateImg());
|
||||
|
||||
int rows = workcaseMapper.insertWorkcase(workcase);
|
||||
if (rows > 0) {
|
||||
// 创建工单处理记录
|
||||
@@ -80,7 +114,7 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
process.setMessage("工单创建");
|
||||
process.setCreator(workcase.getCreator());
|
||||
workcaseProcessMapper.insertWorkcaseProcess(process);
|
||||
|
||||
|
||||
// 如果是新创建的聊天室,更新聊天室的 workcaseId
|
||||
|
||||
logger.info("更新聊天室的工单ID: roomId={}, workcaseId={}", workcase.getRoomId(), workcase.getWorkcaseId());
|
||||
@@ -89,9 +123,24 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
updateRoom.setWorkcaseId(workcase.getWorkcaseId());
|
||||
chatRoomService.updateChatRoom(updateRoom);
|
||||
|
||||
|
||||
syncWorkcaseToCrm(workcase);
|
||||
// 插入设备文件记录到数据库
|
||||
// if (deviceResult.getSuccess() && deviceResult.getData() != null) {
|
||||
// List<TbWorkcaseDeviceDTO> deviceList = (List<TbWorkcaseDeviceDTO>) deviceResult.getData();
|
||||
// for (TbWorkcaseDeviceDTO deviceDTO : deviceList) {
|
||||
// deviceDTO.setWorkcaseId(workcase.getWorkcaseId());
|
||||
// try {
|
||||
// workcaseDeviceMapper.insertWorkcaseDevice(deviceDTO);
|
||||
// logger.info("设备文件记录插入成功: workcaseId={}, fileName={}",
|
||||
// workcase.getWorkcaseId(), deviceDTO.getFileName());
|
||||
// } catch (Exception e) {
|
||||
// logger.error("设备文件记录插入失败: " + deviceDTO.getFileName(), e);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
syncWorkcaseToCrm(workcase);
|
||||
return ResultDomain.success("创建成功", workcase);
|
||||
// return ResultDomain.success(deviceResult.getSuccess() ? "创建成功" : "设备铭牌二维码解析失败", workcase);
|
||||
}
|
||||
return ResultDomain.failure("创建失败");
|
||||
}
|
||||
@@ -445,4 +494,349 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
return ResultDomain.success("查询成功", pageDomain);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 扫描设备铭牌二维码,并下载其中的文件,构建WorkDeviceDTO列表
|
||||
* @param qrcodeFileId 二维码文件id
|
||||
* @return WorkDeviceDTO列表
|
||||
* @author yslg
|
||||
* @since 2025-12-25
|
||||
*/
|
||||
private ResultDomain<TbWorkcaseDeviceDTO> anylizeQrCode(String qrcodeFileId){
|
||||
List<TbWorkcaseDeviceDTO> workDeviceList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
logger.info("开始解析设备铭牌二维码: qrcodeFileId={}", qrcodeFileId);
|
||||
|
||||
// 1. 从 FileService 获取二维码图片
|
||||
ResultDomain<byte[]> downloadResult = fileService.downloadFile(qrcodeFileId);
|
||||
if (!downloadResult.getSuccess()) {
|
||||
logger.error("下载二维码图片失败: {}", downloadResult.getMessage());
|
||||
return ResultDomain.failure("下载二维码图片失败: " + downloadResult.getMessage());
|
||||
}
|
||||
|
||||
byte[] qrcodeImageBytes = downloadResult.getData();
|
||||
logger.info("二维码图片下载成功,大小: {} bytes", qrcodeImageBytes.length);
|
||||
|
||||
// 2. 解析二维码内容
|
||||
String qrcodeContent = decodeQRCode(qrcodeImageBytes);
|
||||
if (qrcodeContent == null || qrcodeContent.isEmpty()) {
|
||||
logger.error("二维码解析失败或内容为空");
|
||||
return ResultDomain.failure("二维码解析失败或内容为空");
|
||||
}
|
||||
|
||||
logger.info("二维码解析成功,内容: {}", qrcodeContent);
|
||||
|
||||
// 3. 判断二维码内容类型并解析
|
||||
String deviceCode = null;
|
||||
String device = null;
|
||||
JSONArray files = null;
|
||||
|
||||
// 尝试判断是 URL 还是 JSON
|
||||
if (qrcodeContent.startsWith("http://") || qrcodeContent.startsWith("https://")) {
|
||||
// 二维码包含 URL,需要访问 H5 页面解析
|
||||
logger.info("检测到二维码包含URL,开始访问页面: {}", qrcodeContent);
|
||||
JSONObject pageData = parseDevicePageFromUrl(qrcodeContent);
|
||||
|
||||
if (pageData == null) {
|
||||
logger.error("访问URL并解析页面失败");
|
||||
return ResultDomain.failure("访问设备信息页面失败");
|
||||
}
|
||||
|
||||
deviceCode = pageData.getString("deviceCode");
|
||||
device = pageData.getString("device");
|
||||
files = pageData.getJSONArray("files");
|
||||
} else {
|
||||
// 二维码直接包含 JSON 数据
|
||||
logger.info("检测到二维码包含JSON数据,直接解析");
|
||||
JSONObject qrcodeData = JSON.parseObject(qrcodeContent);
|
||||
deviceCode = qrcodeData.getString("deviceCode");
|
||||
device = qrcodeData.getString("device");
|
||||
files = qrcodeData.getJSONArray("files");
|
||||
}
|
||||
|
||||
if (files == null || files.isEmpty()) {
|
||||
logger.warn("二维码中没有文件信息");
|
||||
return ResultDomain.success("二维码中没有文件信息", workDeviceList);
|
||||
}
|
||||
|
||||
logger.info("设备编号: {}, 设备名称: {}, 文件数量: {}", deviceCode, device, files.size());
|
||||
|
||||
// 4. 下载并存储每个文件
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
JSONObject fileInfo = files.getJSONObject(i);
|
||||
String fileName = fileInfo.getString("fileName");
|
||||
String fileUrl = fileInfo.getString("url");
|
||||
|
||||
logger.info("开始下载文件 [{}/{}]: {}", i + 1, files.size(), fileName);
|
||||
|
||||
try {
|
||||
// 下载文件
|
||||
byte[] fileBytes = downloadFileFromUrl(fileUrl);
|
||||
if (fileBytes == null || fileBytes.length == 0) {
|
||||
logger.error("文件下载失败: {}", fileName);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info("文件下载成功: {}, 大小: {} bytes", fileName, fileBytes.length);
|
||||
|
||||
// 推断文件类型
|
||||
String contentType = getContentTypeFromFileName(fileName);
|
||||
|
||||
// 通过 FileService 存储文件
|
||||
ResultDomain<TbSysFileDTO> uploadResult = fileService.uploadFileBytes(
|
||||
fileBytes,
|
||||
fileName,
|
||||
contentType,
|
||||
"workcase",
|
||||
deviceCode
|
||||
);
|
||||
|
||||
if (uploadResult.getSuccess()) {
|
||||
TbSysFileDTO uploadedFile = uploadResult.getData();
|
||||
logger.info("文件上传成功: fileId={}, fileName={}", uploadedFile.getFileId(), fileName);
|
||||
|
||||
// 创建 TbWorkcaseDeviceDTO 对象
|
||||
TbWorkcaseDeviceDTO deviceDTO = new TbWorkcaseDeviceDTO();
|
||||
deviceDTO.setDevice(device);
|
||||
deviceDTO.setDeviceCode(deviceCode);
|
||||
deviceDTO.setFileId(uploadedFile.getFileId());
|
||||
deviceDTO.setFileName(fileName);
|
||||
deviceDTO.setFileRootId(uploadedFile.getFileRootId());
|
||||
deviceDTO.setOptsn(IdUtil.getOptsn());
|
||||
|
||||
workDeviceList.add(deviceDTO);
|
||||
} else {
|
||||
logger.error("文件上传失败: {}, 原因: {}", fileName, uploadResult.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理文件失败: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("二维码解析完成,成功处理 {} 个文件", workDeviceList.size());
|
||||
return ResultDomain.success("解析成功,处理了 " + workDeviceList.size() + " 个文件", workDeviceList);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("解析设备铭牌二维码失败", e);
|
||||
return ResultDomain.failure("解析设备铭牌二维码失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 解析设备页面信息
|
||||
*/
|
||||
private JSONObject parseDevicePageFromUrl(String url) {
|
||||
try {
|
||||
logger.info("开始访问设备信息页面: {}", url);
|
||||
|
||||
// 访问 URL 并获取 HTML 内容
|
||||
Document doc = Jsoup.connect(url)
|
||||
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
.timeout(10000)
|
||||
.followRedirects(true)
|
||||
.get();
|
||||
|
||||
logger.info("页面访问成功,开始解析内容");
|
||||
|
||||
// 解析设备编号和型号(从页面标题或特定元素中)
|
||||
String deviceInfo = doc.select("body").text();
|
||||
|
||||
// 尝试匹配设备编号模式:数字格式如 202508012
|
||||
String deviceCode = null;
|
||||
String device = null;
|
||||
|
||||
// 从页面内容中提取设备编号和型号
|
||||
if (deviceInfo.contains("(") && deviceInfo.contains(")")) {
|
||||
// 例如: "202508012 (THHM1800PL)"
|
||||
int start = deviceInfo.indexOf("(");
|
||||
int end = deviceInfo.indexOf(")", start);
|
||||
if (start > 0 && end > start) {
|
||||
// 提取括号前的设备编号
|
||||
String beforeBracket = deviceInfo.substring(Math.max(0, start - 20), start).trim();
|
||||
String[] parts = beforeBracket.split("\\s+");
|
||||
if (parts.length > 0) {
|
||||
deviceCode = parts[parts.length - 1].trim();
|
||||
}
|
||||
|
||||
// 提取括号内的型号
|
||||
device = deviceInfo.substring(start + 1, end).trim();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("提取的设备信息 - 编号: {}, 型号: {}", deviceCode, device);
|
||||
|
||||
// 解析文件列表
|
||||
JSONArray files = new JSONArray();
|
||||
|
||||
// 查找所有下载链接(通常是 PDF、图片等文件)
|
||||
Elements links = doc.select("a[href]");
|
||||
for (Element link : links) {
|
||||
String href = link.attr("abs:href"); // 获取绝对 URL
|
||||
String text = link.text().trim();
|
||||
|
||||
// 过滤出文件下载链接(包含文件扩展名)
|
||||
if (isFileLink(href) && !text.isEmpty()) {
|
||||
JSONObject fileInfo = new JSONObject();
|
||||
fileInfo.put("fileName", text);
|
||||
fileInfo.put("url", href);
|
||||
files.add(fileInfo);
|
||||
logger.info("发现文件: {} -> {}", text, href);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到链接,尝试从 script 标签中查找 JSON 数据
|
||||
if (files.isEmpty()) {
|
||||
Elements scripts = doc.select("script");
|
||||
for (Element script : scripts) {
|
||||
String scriptContent = script.html();
|
||||
// 尝试查找 JSON 数据
|
||||
if (scriptContent.contains("files") || scriptContent.contains("fileName")) {
|
||||
try {
|
||||
// 尝试解析嵌入的 JSON
|
||||
int jsonStart = scriptContent.indexOf("{");
|
||||
int jsonEnd = scriptContent.lastIndexOf("}") + 1;
|
||||
if (jsonStart >= 0 && jsonEnd > jsonStart) {
|
||||
String jsonStr = scriptContent.substring(jsonStart, jsonEnd);
|
||||
JSONObject embedded = JSON.parseObject(jsonStr);
|
||||
if (embedded.containsKey("files")) {
|
||||
files = embedded.getJSONArray("files");
|
||||
if (deviceCode == null) {
|
||||
deviceCode = embedded.getString("deviceCode");
|
||||
}
|
||||
if (device == null) {
|
||||
device = embedded.getString("device");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略解析错误,继续下一个 script
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceCode == null || files.isEmpty()) {
|
||||
logger.error("无法从页面中提取完整的设备信息");
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("deviceCode", deviceCode);
|
||||
result.put("device", device != null ? device : "Unknown");
|
||||
result.put("files", files);
|
||||
|
||||
logger.info("页面解析成功,设备编号: {}, 文件数量: {}", deviceCode, files.size());
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("解析设备页面失败: " + url, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文件下载链接
|
||||
*/
|
||||
private boolean isFileLink(String url) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lowerUrl = url.toLowerCase();
|
||||
return lowerUrl.endsWith(".pdf") ||
|
||||
lowerUrl.endsWith(".jpg") ||
|
||||
lowerUrl.endsWith(".jpeg") ||
|
||||
lowerUrl.endsWith(".png") ||
|
||||
lowerUrl.endsWith(".doc") ||
|
||||
lowerUrl.endsWith(".docx") ||
|
||||
lowerUrl.endsWith(".xls") ||
|
||||
lowerUrl.endsWith(".xlsx") ||
|
||||
lowerUrl.contains("/download") ||
|
||||
lowerUrl.contains("/file/") ||
|
||||
lowerUrl.contains(".pdf?") ||
|
||||
lowerUrl.contains(".jpg?");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码二维码图片
|
||||
*/
|
||||
private String decodeQRCode(byte[] imageBytes) {
|
||||
try {
|
||||
InputStream inputStream = new ByteArrayInputStream(imageBytes);
|
||||
BufferedImage bufferedImage = ImageIO.read(inputStream);
|
||||
|
||||
if (bufferedImage == null) {
|
||||
logger.error("无法读取图片");
|
||||
return null;
|
||||
}
|
||||
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(
|
||||
new HybridBinarizer(
|
||||
new BufferedImageLuminanceSource(bufferedImage)
|
||||
)
|
||||
);
|
||||
|
||||
MultiFormatReader reader = new MultiFormatReader();
|
||||
java.util.Map<DecodeHintType, Object> hints = new java.util.HashMap<>();
|
||||
hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
|
||||
|
||||
Result result = reader.decode(binaryBitmap, hints);
|
||||
return result.getText();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("解码二维码失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL下载文件
|
||||
*/
|
||||
private byte[] downloadFileFromUrl(String fileUrl) {
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
HttpGet httpGet = new HttpGet(URI.create(fileUrl));
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
|
||||
if (response.getCode() == 200) {
|
||||
return EntityUtils.toByteArray(response.getEntity());
|
||||
} else {
|
||||
logger.error("下载文件失败,HTTP状态码: {}", response.getCode());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("下载文件失败: " + fileUrl, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名推断 Content-Type
|
||||
*/
|
||||
private String getContentTypeFromFileName(String fileName) {
|
||||
if (fileName == null) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
String lowerFileName = fileName.toLowerCase();
|
||||
if (lowerFileName.endsWith(".pdf")) {
|
||||
return "application/pdf";
|
||||
} else if (lowerFileName.endsWith(".jpg") || lowerFileName.endsWith(".jpeg")) {
|
||||
return "image/jpeg";
|
||||
} else if (lowerFileName.endsWith(".png")) {
|
||||
return "image/png";
|
||||
} else if (lowerFileName.endsWith(".doc")) {
|
||||
return "application/msword";
|
||||
} else if (lowerFileName.endsWith(".docx")) {
|
||||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
} else if (lowerFileName.endsWith(".xls")) {
|
||||
return "application/vnd.ms-excel";
|
||||
} else if (lowerFileName.endsWith(".xlsx")) {
|
||||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
} else {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user