Commit 655980ca by zhangxingmin

push

parent 95df52a3
package com.yd.communication.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig {
@Bean(name = "communicationExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setThreadNamePrefix("communication-"); // 线程名前缀
executor.initialize();
return executor;
}
}
\ No newline at end of file
package com.yd.communication.api.controller;
import com.yd.common.result.Result;
import com.yd.communication.api.service.ApiCoDesensitizationRuleService;
import com.yd.communication.feign.client.ApiCoDesensitizationRuleFeignClient;
import com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 脱敏信息
*
* @author zxm
* @since 2026-07-28
*/
@Slf4j
@RestController
@RequestMapping("/desensitization")
@Validated
public class ApiCoDesensitizationRuleController implements ApiCoDesensitizationRuleFeignClient {
@Resource
private ApiCoDesensitizationRuleService apiCoDesensitizationRuleService;
/**
* 根据 ID 查询单条规则
* @param bizId
* @return
*/
@Override
public Result<ApiCoDesensitizationRuleResponse> getByBizId(String bizId) {
return apiCoDesensitizationRuleService.getByBizId(bizId);
}
/**
* 根据资源类型和资源ID获取生效的脱敏规则列表(供脱敏引擎调用)
* @param resourceType
* @param resourceId
* @return
*/
@Override
public Result<List<ApiCoDesensitizationRuleResponse>> getEnabledRules(String resourceType, String resourceId) {
return apiCoDesensitizationRuleService.getEnabledRules(resourceType,resourceType);
}
}
\ No newline at end of file
package com.yd.communication.api.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 协同-脱敏设置表(通用) 前端控制器
* </p>
*
* @author zxm
* @since 2026-07-28
*/
@RestController
@RequestMapping("/coDesensitizationRule")
public class CoDesensitizationRuleController {
}
package com.yd.communication.api.service;
import com.yd.common.result.Result;
import com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse;
import java.util.List;
public interface ApiCoDesensitizationRuleService {
Result<ApiCoDesensitizationRuleResponse> getByBizId(String bizId);
Result<List<ApiCoDesensitizationRuleResponse>> getEnabledRules(String resourceType, String resourceId);
}
package com.yd.communication.api.service.impl;
import com.yd.common.exception.BusinessException;
import com.yd.common.result.Result;
import com.yd.communication.api.service.ApiCoDesensitizationRuleService;
import com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse;
import com.yd.communication.service.model.CoDesensitizationRule;
import com.yd.communication.service.service.ICoDesensitizationRuleService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ApiCoDesensitizationRuleServiceImpl implements ApiCoDesensitizationRuleService {
@Resource
private ICoDesensitizationRuleService ruleService;
/**
* 根据 ID 查询单条规则
* @param bizId
* @return
*/
@Override
public Result<ApiCoDesensitizationRuleResponse> getByBizId(String bizId) {
CoDesensitizationRule rule = ruleService.lambdaQuery()
.eq(CoDesensitizationRule::getRuleBizId,bizId)
.last(" limit 1 ")
.one();
if (rule == null) {
throw new BusinessException("脱敏信息不存在");
}
ApiCoDesensitizationRuleResponse response = new ApiCoDesensitizationRuleResponse();
BeanUtils.copyProperties(rule,response);
return Result.success(response);
}
/**
* 根据资源类型和资源ID获取生效的脱敏规则列表(供脱敏引擎调用)
* @param resourceType
* @param resourceId
* @return
*/
@Override
public Result<List<ApiCoDesensitizationRuleResponse>> getEnabledRules(String resourceType, String resourceId) {
List<CoDesensitizationRule> rules = ruleService.getEnabledRulesByResource(resourceType, resourceId);
if (CollectionUtils.isEmpty(rules)) {
return Result.success();
}
List<ApiCoDesensitizationRuleResponse> responses = rules.stream().map(dto -> {
ApiCoDesensitizationRuleResponse response = new ApiCoDesensitizationRuleResponse();
BeanUtils.copyProperties(dto,response);
return response;
}).collect(Collectors.toList());
return Result.success(responses);
}
}
...@@ -7,6 +7,7 @@ import com.yd.common.result.Result; ...@@ -7,6 +7,7 @@ import com.yd.common.result.Result;
import com.yd.common.utils.RandomStringGenerator; import com.yd.common.utils.RandomStringGenerator;
import com.yd.common.utils.RedisUtil; import com.yd.common.utils.RedisUtil;
import com.yd.communication.api.service.ApiCoSessionService; import com.yd.communication.api.service.ApiCoSessionService;
import com.yd.communication.api.utils.ClientInfoUtils;
import com.yd.communication.feign.dto.RoomRedisInfoDTO; import com.yd.communication.feign.dto.RoomRedisInfoDTO;
import com.yd.communication.feign.enums.CoSessionStatusEnum; import com.yd.communication.feign.enums.CoSessionStatusEnum;
import com.yd.communication.feign.enums.ControlHolderTypeEnum; import com.yd.communication.feign.enums.ControlHolderTypeEnum;
...@@ -20,6 +21,7 @@ import com.yd.communication.feign.response.CreateResponse; ...@@ -20,6 +21,7 @@ import com.yd.communication.feign.response.CreateResponse;
import com.yd.communication.feign.response.JoinResponse; import com.yd.communication.feign.response.JoinResponse;
import com.yd.communication.feign.response.SessionDetailResponse; import com.yd.communication.feign.response.SessionDetailResponse;
import com.yd.communication.service.model.CoSession; import com.yd.communication.service.model.CoSession;
import com.yd.communication.service.service.ICoOperationLogService;
import com.yd.communication.service.service.ICoSessionService; import com.yd.communication.service.service.ICoSessionService;
import com.yd.communication.service.service.IRecordingTaskService; import com.yd.communication.service.service.IRecordingTaskService;
import com.yd.communication.service.utils.RandomUtil; import com.yd.communication.service.utils.RandomUtil;
...@@ -28,7 +30,6 @@ import org.apache.commons.lang3.StringUtils; ...@@ -28,7 +30,6 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
...@@ -44,6 +45,9 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService { ...@@ -44,6 +45,9 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService {
private IRecordingTaskService recordingService; private IRecordingTaskService recordingService;
@Resource @Resource
private ICoOperationLogService operationLogService;
@Resource
private RedisUtil redisUtil; private RedisUtil redisUtil;
/** /**
...@@ -138,8 +142,19 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService { ...@@ -138,8 +142,19 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService {
// recordingService.startRecording(session.getCoSessionBizId(), roomId); // recordingService.startRecording(session.getCoSessionBizId(), roomId);
log.info("【创建会话-内部】自动录制已触发(未实际启动)"); log.info("【创建会话-内部】自动录制已触发(未实际启动)");
} }
log.info("【创建会话-内部】创建成功, roomId={}, roomPwd={}", roomId, roomPwd); log.info("【创建会话-内部】创建成功, roomId={}, roomPwd={}", roomId, roomPwd);
//生成协同操作日志
operationLogService.log(
session.getCoSessionBizId(),
ownerId,
ownerType,
ownerId,
"create_session",
String.format("创建会话 roomId=%s, roomPwd=%s", session.getRoomId(), session.getRoomPwd()),
ClientInfoUtils.getDevice(),
ClientInfoUtils.getClientIp()
);
return session; return session;
} catch (Exception e) { } catch (Exception e) {
log.error("【创建会话-内部】异常", e); log.error("【创建会话-内部】异常", e);
...@@ -257,6 +272,17 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService { ...@@ -257,6 +272,17 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService {
log.warn("【加入会话-内部】Redis缓存不存在,可能已过期,roomId={}", session.getRoomId()); log.warn("【加入会话-内部】Redis缓存不存在,可能已过期,roomId={}", session.getRoomId());
} }
//生成协同操作日志
operationLogService.log(
session.getCoSessionBizId(),
participantId,
participantType,
participantId,
"join_session",
String.format("加入会话 roomId=%s, 控制权转移至%s", session.getRoomId(), session.getControlHolderType()),
ClientInfoUtils.getDevice(),
ClientInfoUtils.getClientIp()
);
return session; return session;
} catch (Exception e) { } catch (Exception e) {
log.error("【加入会话-内部】异常", e); log.error("【加入会话-内部】异常", e);
...@@ -314,9 +340,17 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService { ...@@ -314,9 +340,17 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService {
redisUtil.deleteObject(RedisEnum.ROOM.getPrefix() + coSession.getRoomId()); redisUtil.deleteObject(RedisEnum.ROOM.getPrefix() + coSession.getRoomId());
log.info("【结束会话】Redis缓存已删除"); log.info("【结束会话】Redis缓存已删除");
// 结束录制视频,并且更新录制任务表信息存档 TODO // 添加操作日志,协同-操作日志表
operationLogService.log(
// 添加操作日志,协同-操作日志表 TODO coSession.getCoSessionBizId(),
null,
null,
null,
"end_session",
String.format("结束会话 roomId=%s", coSession.getRoomId()),
ClientInfoUtils.getDevice(),
ClientInfoUtils.getClientIp()
);
CommonResponse response = new CommonResponse(); CommonResponse response = new CommonResponse();
response.setMessage("会话已结束"); response.setMessage("会话已结束");
...@@ -395,7 +429,17 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService { ...@@ -395,7 +429,17 @@ public class ApiCoSessionServiceImpl implements ApiCoSessionService {
} else { } else {
log.warn("【切换控制权-内部】Redis缓存不存在,可能已过期"); log.warn("【切换控制权-内部】Redis缓存不存在,可能已过期");
} }
// 添加操作日志,协同-操作日志表 TODO // 添加操作日志,协同-操作日志表
operationLogService.log(
session.getCoSessionBizId(),
session.getParticipantId(),
ControlHolderTypeEnum.PARTICIPANT.getItemValue(),
session.getParticipantId(),
"transfer_control",
String.format("切换控制权至 %s:%s", session.getControlHolderType(), session.getControlHolderId()),
ClientInfoUtils.getDevice(),
ClientInfoUtils.getClientIp()
);
} catch (Exception e) { } catch (Exception e) {
log.error("【切换控制权-内部】异常", e); log.error("【切换控制权-内部】异常", e);
throw e; throw e;
......
...@@ -3,15 +3,15 @@ package com.yd.communication.api.service.impl; ...@@ -3,15 +3,15 @@ package com.yd.communication.api.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.yd.common.enums.CommonEnum; import com.yd.common.enums.CommonEnum;
import com.yd.common.enums.ProjectEnum;
import com.yd.common.enums.TenantEnum;
import com.yd.common.exception.BusinessException; import com.yd.common.exception.BusinessException;
import com.yd.common.result.Result; import com.yd.common.result.Result;
import com.yd.common.utils.RandomStringGenerator; import com.yd.common.utils.RandomStringGenerator;
import com.yd.communication.api.service.ApiRecordingTaskService; import com.yd.communication.api.service.ApiRecordingTaskService;
import com.yd.communication.api.utils.ClientInfoUtils;
import com.yd.communication.feign.request.recording.ApiStartRecordingRequest; import com.yd.communication.feign.request.recording.ApiStartRecordingRequest;
import com.yd.communication.feign.response.recording.ApiQueryRecordingResponse; import com.yd.communication.feign.response.recording.ApiQueryRecordingResponse;
import com.yd.communication.service.model.RecordingTask; import com.yd.communication.service.model.RecordingTask;
import com.yd.communication.service.service.ICoOperationLogService;
import com.yd.communication.service.service.IRecordingTaskService; import com.yd.communication.service.service.IRecordingTaskService;
import com.yd.oss.feign.client.ApiOssFeignClient; import com.yd.oss.feign.client.ApiOssFeignClient;
import com.yd.oss.feign.request.ApiUploadFileRequest; import com.yd.oss.feign.request.ApiUploadFileRequest;
...@@ -39,6 +39,9 @@ public class ApiRecordingTaskServiceImpl implements ApiRecordingTaskService { ...@@ -39,6 +39,9 @@ public class ApiRecordingTaskServiceImpl implements ApiRecordingTaskService {
private ApiOssFeignClient apiOssFeignClient; private ApiOssFeignClient apiOssFeignClient;
@Resource @Resource
private ICoOperationLogService operationLogService;
@Resource
private ObjectMapper objectMapper; // 注入 Jackson ObjectMapper private ObjectMapper objectMapper; // 注入 Jackson ObjectMapper
/** /**
...@@ -74,6 +77,18 @@ public class ApiRecordingTaskServiceImpl implements ApiRecordingTaskService { ...@@ -74,6 +77,18 @@ public class ApiRecordingTaskServiceImpl implements ApiRecordingTaskService {
iRecordingTaskService.save(task); iRecordingTaskService.save(task);
log.info("录制任务创建成功,taskId={}", task.getTaskId()); log.info("录制任务创建成功,taskId={}", task.getTaskId());
//生成协同操作日志
operationLogService.log(
request.getBizId(),
"system",
"system",
"system",
"start_recording",
String.format("开始录制 taskId=%s, bizType=%s", task.getTaskId(), request.getBizType()),
ClientInfoUtils.getDevice(),
ClientInfoUtils.getClientIp()
);
return Result.success(task.getTaskId()); return Result.success(task.getTaskId());
} }
...@@ -142,6 +157,18 @@ public class ApiRecordingTaskServiceImpl implements ApiRecordingTaskService { ...@@ -142,6 +157,18 @@ public class ApiRecordingTaskServiceImpl implements ApiRecordingTaskService {
result.put("fileKey", uploadResponse.getFileKey()); result.put("fileKey", uploadResponse.getFileKey());
log.info("录制文件上传成功,fileUrl={}", task.getFileUrl()); log.info("录制文件上传成功,fileUrl={}", task.getFileUrl());
//生成协同操作日志
operationLogService.log(
task.getBizId(),
"system",
"system",
"system",
"stop_recording",
String.format("停止录制 taskId=%s, fileUrl=%s", task.getTaskId(), task.getFileUrl()),
ClientInfoUtils.getDevice(),
ClientInfoUtils.getClientIp()
);
return Result.success(result); return Result.success(result);
} }
......
package com.yd.communication.api.utils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 客户端信息工具类(IP、设备号等)
* 优先从 HTTP 请求获取,若不存在则从 IpContextHolder 获取(WebSocket 场景)
*
* @author zxm
* @date 2026-08-03
*/
public class ClientInfoUtils {
/**
* 获取客户端真实 IP
* 优先级:X-Forwarded-For > X-Real-IP > RemoteAddr > IpContextHolder
*
* @return 客户端 IP
*/
public static String getClientIp() {
// 1. 从 HTTP 请求获取
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
if (request != null) {
// 获取 X-Forwarded-For(经过代理时)
String ip = request.getHeader("X-Forwarded-For");
if (StringUtils.isBlank(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (StringUtils.isBlank(ip)) {
ip = request.getRemoteAddr();
}
// X-Forwarded-For 可能包含多个 IP,取第一个
if (StringUtils.isNotBlank(ip) && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
if (StringUtils.isNotBlank(ip)) {
return ip;
}
}
}
} catch (Exception e) {
// 忽略异常,继续回退
}
// 2. 回退到 IpContextHolder(WebSocket 场景由 CoWebSocketServer 设置)
return IpContextHolder.getIp();
}
/**
* 获取设备号(User-Agent)
* 优先级:HTTP 请求头 > IpContextHolder
*
* @return 设备号(User-Agent)
*/
public static String getDevice() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
if (request != null) {
return request.getHeader("User-Agent");
}
}
} catch (Exception e) {
// 忽略异常
}
return IpContextHolder.getDevice();
}
/**
* 获取完整客户端信息(IP + 设备)
* @return 格式:IP | User-Agent
*/
public static String getClientInfo() {
return getClientIp() + " | " + getDevice();
}
}
\ No newline at end of file
package com.yd.communication.api.utils;
/**
* 用于在 WebSocket 等非 HTTP 环境下传递客户端 IP 和设备信息
* <p>
* 使用 ThreadLocal 存储,避免参数透传,确保线程安全。
* 通常在 WebSocket 的 @OnMessage 方法中设置,在业务 Service 中获取,
* 并在 finally 块中清除,防止内存泄漏。
* </p>
*
* @author zxm
* @date 2026-08-03
*/
public class IpContextHolder {
private static final ThreadLocal<String> IP_HOLDER = new ThreadLocal<>();
private static final ThreadLocal<String> DEVICE_HOLDER = new ThreadLocal<>();
/**
* 设置当前线程的客户端 IP
*/
public static void setIp(String ip) {
IP_HOLDER.set(ip);
}
/**
* 获取当前线程的客户端 IP
*/
public static String getIp() {
return IP_HOLDER.get();
}
/**
* 设置当前线程的设备号(User-Agent 等)
*/
public static void setDevice(String device) {
DEVICE_HOLDER.set(device);
}
/**
* 获取当前线程的设备号
*/
public static String getDevice() {
return DEVICE_HOLDER.get();
}
/**
* 清除当前线程的上下文(防止内存泄漏)
*/
public static void clear() {
IP_HOLDER.remove();
DEVICE_HOLDER.remove();
}
}
\ No newline at end of file
...@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSON; ...@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.yd.communication.api.service.ApiCoSessionService; import com.yd.communication.api.service.ApiCoSessionService;
import com.yd.communication.api.utils.IpContextHolder;
import com.yd.communication.feign.dto.RoomRedisInfoDTO; import com.yd.communication.feign.dto.RoomRedisInfoDTO;
import com.yd.communication.feign.enums.ControlHolderTypeEnum; import com.yd.communication.feign.enums.ControlHolderTypeEnum;
import com.yd.communication.feign.request.EndSessionRequest; import com.yd.communication.feign.request.EndSessionRequest;
...@@ -23,13 +24,14 @@ import javax.websocket.*; ...@@ -23,13 +24,14 @@ import javax.websocket.*;
import javax.websocket.server.PathParam; import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpoint;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.connection.RedisConnection;
/** /**
* 协同 WebSocket 服务端(支持多节点部署) * 协同 WebSocket 服务端(支持多节点部署)
...@@ -513,6 +515,11 @@ public class CoWebSocketServer { ...@@ -513,6 +515,11 @@ public class CoWebSocketServer {
*/ */
@OnMessage @OnMessage
public void onMessage(Session session, String message) { public void onMessage(Session session, String message) {
// 获取 IP
String remoteIp = getClientIp(session);
IpContextHolder.setIp(remoteIp);
IpContextHolder.setDevice(null);
// 根据会话查询对应的房间号 // 根据会话查询对应的房间号
String roomId = SESSION_ROOM.get(session); String roomId = SESSION_ROOM.get(session);
if (roomId == null) { if (roomId == null) {
...@@ -592,7 +599,7 @@ public class CoWebSocketServer { ...@@ -592,7 +599,7 @@ public class CoWebSocketServer {
// 记录操作日志(用于合规审计) // 记录操作日志(用于合规审计)
if (bizId != null) { if (bizId != null) {
operationLogService.log(bizId, userId, userType, userId, action, correctMsg, null, null); operationLogService.log(bizId, userId, userType, userId, action, correctMsg, IpContextHolder.getDevice(), IpContextHolder.getIp());
} }
log.info("控制权切换成功: roomId={}, holder={}:{}", roomId, correctHolderType, correctHolderId); log.info("控制权切换成功: roomId={}, holder={}:{}", roomId, correctHolderType, correctHolderId);
...@@ -627,6 +634,19 @@ public class CoWebSocketServer { ...@@ -627,6 +634,19 @@ public class CoWebSocketServer {
// 关闭房间所有连接并清理资源 // 关闭房间所有连接并清理资源
closeRoom(roomId); closeRoom(roomId);
log.info("共享已结束: roomId={}", roomId); log.info("共享已结束: roomId={}", roomId);
if (bizId != null) {
operationLogService.log(
bizId,
userId,
userType,
userId,
"end_sharing",
"结束共享",
IpContextHolder.getDevice(),
IpContextHolder.getIp()
);
}
return; return;
} }
...@@ -639,7 +659,7 @@ public class CoWebSocketServer { ...@@ -639,7 +659,7 @@ public class CoWebSocketServer {
// 记录操作日志 // 记录操作日志
if (bizId != null) { if (bizId != null) {
operationLogService.log(bizId, userId, userType, userId, action, message, null, null); operationLogService.log(bizId, userId, userType, userId, action, message, IpContextHolder.getDevice(), IpContextHolder.getIp());
} }
return; return;
} }
...@@ -686,7 +706,7 @@ public class CoWebSocketServer { ...@@ -686,7 +706,7 @@ public class CoWebSocketServer {
// 4.5 记录操作日志(合规审计) // 4.5 记录操作日志(合规审计)
if (bizId != null) { if (bizId != null) {
operationLogService.log(bizId, userId, userType, userId, action, message, null, null); operationLogService.log(bizId, userId, userType, userId, action, message, IpContextHolder.getDevice(), IpContextHolder.getIp());
} }
return; return;
} }
...@@ -698,6 +718,9 @@ public class CoWebSocketServer { ...@@ -698,6 +718,9 @@ public class CoWebSocketServer {
// 处理消息异常,向客户端返回错误信息 // 处理消息异常,向客户端返回错误信息
log.error("处理WebSocket消息异常,消息内容: {}", message, e); log.error("处理WebSocket消息异常,消息内容: {}", message, e);
sendError(session, "服务器处理异常"); sendError(session, "服务器处理异常");
} finally {
//清除上下文,防止内存泄漏
IpContextHolder.clear();
} }
} }
...@@ -877,4 +900,40 @@ public class CoWebSocketServer { ...@@ -877,4 +900,40 @@ public class CoWebSocketServer {
return value; return value;
} }
} }
// ==================== 获取客户端 IP 的工具方法 ====================
/**
* 获取客户端 IP(由于 javax.websocket.Session 接口没有 getRemoteAddress,通过反射调用)
*/
private String getClientIp(Session session) {
try {
// 尝试直接通过反射调用 getRemoteAddress(Tomcat 等实现类有该方法)
Method method = session.getClass().getMethod("getRemoteAddress");
Object result = method.invoke(session);
if (result instanceof InetSocketAddress) {
InetSocketAddress address = (InetSocketAddress) result;
return address.getAddress().getHostAddress();
}
} catch (Exception e) {
log.debug("通过反射获取 getRemoteAddress 失败,尝试其他方式");
}
// 备选:从请求参数中获取(前端可传 ?ip=xxx)
Map<String, List<String>> params = session.getRequestParameterMap();
if (params != null && params.containsKey("ip")) {
List<String> ips = params.get("ip");
if (ips != null && !ips.isEmpty()) {
return ips.get(0);
}
}
// 再备选:从 getUserProperties 中获取(如由 configurator 存储)
Object storedIp = session.getUserProperties().get("remoteIp");
if (storedIp != null) {
return storedIp.toString();
}
return "unknown";
}
} }
\ No newline at end of file
package com.yd.communication.feign.client;
import com.yd.common.result.Result;
import com.yd.communication.feign.fallback.ApiCoDesensitizationRuleFeignFallbackFactory;
import com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* 通信服务-脱敏信息Feign 客户端
*
* @author zxm
* @date 2026-07-28
*/
@FeignClient(name = "yd-communication-api", path = "/communication/api/desensitization", fallbackFactory = ApiCoDesensitizationRuleFeignFallbackFactory.class)
public interface ApiCoDesensitizationRuleFeignClient {
/**
* 根据业务ID查询单条规则
*/
@GetMapping("/{bizId}")
Result<ApiCoDesensitizationRuleResponse> getByBizId(@PathVariable @NotBlank(message = "业务ID不能为空") String bizId);
/**
* 根据资源类型和资源ID获取生效的脱敏规则列表(供脱敏引擎调用)
*/
@GetMapping("/enabled")
Result<List<ApiCoDesensitizationRuleResponse>> getEnabledRules(
@RequestParam(value = "resourceType",required = false) String resourceType,
@RequestParam(value = "resourceId",required = false) String resourceId);
}
package com.yd.communication.feign.fallback;
import com.yd.common.result.Result;
import com.yd.communication.feign.client.ApiCoDesensitizationRuleFeignClient;
import com.yd.communication.feign.response.desensitization.ApiCoDesensitizationRuleResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 通信服务-脱敏信息Feign降级处理
*/
@Slf4j
@Component
public class ApiCoDesensitizationRuleFeignFallbackFactory implements FallbackFactory<ApiCoDesensitizationRuleFeignClient> {
@Override
public ApiCoDesensitizationRuleFeignClient create(Throwable cause) {
return new ApiCoDesensitizationRuleFeignClient() {
@Override
public Result<ApiCoDesensitizationRuleResponse> getByBizId(String bizId) {
return null;
}
@Override
public Result<List<ApiCoDesensitizationRuleResponse>> getEnabledRules(String resourceType, String resourceId) {
return null;
}
};
}
}
package com.yd.communication.feign.response.desensitization;
import lombok.Data;
@Data
public class ApiCoDesensitizationRuleResponse {
/**
* 主键
*/
private Long id;
/**
* 脱敏规则唯一业务ID
*/
private String ruleBizId;
/**
* 规则名称(如:报告-客户姓名脱敏)
*/
private String ruleName;
/**
* 资源类型:report/news/mini_program
*/
private String resourceType;
/**
* 资源业务ID(NULL表示全局规则,适用于该类型下所有资源)
*/
private String resourceId;
/**
* 脱敏字段路径(JSON Path,如:$.customer.name)
*/
private String fieldPath;
/**
* 字段显示名称(冗余,便于运营配置)
*/
private String fieldName;
/**
* 脱敏方式:mask-掩码(如:张**)、hide-完全隐藏(****)、replace-替换(***)、partial-部分显示(如:138****8000)
*/
private String maskType;
/**
* 脱敏配置JSON(如:{"prefix":1,"suffix":1,"replace_char":"*"})
*/
private String maskConfig;
/**
* 规则是否启用:0-停用,1-启用
*/
private Integer enabled;
/**
* 是否为默认规则:0-否,1-是(资源未配置时使用默认)
*/
private Integer isDefault;
/**
* 排序顺序
*/
private Integer sortOrder;
}
package com.yd.communication.service.service; package com.yd.communication.service.service;
import com.yd.communication.service.model.CoDesensitizationRule; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.yd.communication.service.model.CoDesensitizationRule;
import java.util.List;
/** /**
* <p> * 协同-脱敏设置表 服务类
* 协同-脱敏设置表(通用) 服务类
* </p>
*
* @author zxm
* @since 2026-07-28
*/ */
public interface ICoDesensitizationRuleService extends IService<CoDesensitizationRule> { public interface ICoDesensitizationRuleService extends IService<CoDesensitizationRule> {
} /**
* 分页查询脱敏规则列表
*/
IPage<CoDesensitizationRule> pageList(Page<CoDesensitizationRule> page, String resourceType, String resourceId, Integer enabled);
/**
* 根据资源类型和资源ID获取生效的脱敏规则列表
*/
List<CoDesensitizationRule> getEnabledRulesByResource(String resourceType, String resourceId);
/**
* 启用/停用规则
*/
void toggleEnabled(Long id, Integer enabled);
/**
* 逻辑删除规则
*/
void deleteById(Long id);
}
\ No newline at end of file
package com.yd.communication.service.service.impl; package com.yd.communication.service.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.JsonNode; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.databind.ObjectMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yd.communication.service.model.CoDesensitizationRule; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yd.common.enums.CommonEnum;
import com.yd.common.exception.BusinessException;
import com.yd.common.utils.RandomStringGenerator;
import com.yd.communication.service.dao.CoDesensitizationRuleMapper; import com.yd.communication.service.dao.CoDesensitizationRuleMapper;
import com.yd.communication.service.model.CoDesensitizationRule;
import com.yd.communication.service.service.ICoDesensitizationRuleService; import com.yd.communication.service.service.ICoDesensitizationRuleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/**
* <p>
* 协同-脱敏设置表(通用) 服务实现类
* </p>
*
* @author zxm
* @since 2026-07-28
*/
@Slf4j @Slf4j
@Service @Service
public class CoDesensitizationRuleServiceImpl extends ServiceImpl<CoDesensitizationRuleMapper, CoDesensitizationRule> implements ICoDesensitizationRuleService { public class CoDesensitizationRuleServiceImpl
extends ServiceImpl<CoDesensitizationRuleMapper, CoDesensitizationRule>
@Resource implements ICoDesensitizationRuleService {
private CoDesensitizationRuleMapper ruleMapper;
private final ObjectMapper objectMapper = new ObjectMapper(); @Override
public IPage<CoDesensitizationRule> pageList(Page<CoDesensitizationRule> page,
String resourceType,
String resourceId,
Integer enabled) {
LambdaQueryWrapper<CoDesensitizationRule> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CoDesensitizationRule::getIsDeleted, 0);
if (StringUtils.isNotBlank(resourceType)) {
wrapper.eq(CoDesensitizationRule::getResourceType, resourceType);
}
if (StringUtils.isNotBlank(resourceId)) {
wrapper.eq(CoDesensitizationRule::getResourceId, resourceId);
}
if (enabled != null) {
wrapper.eq(CoDesensitizationRule::getEnabled, enabled);
}
wrapper.orderByAsc(CoDesensitizationRule::getSortOrder)
.orderByDesc(CoDesensitizationRule::getCreateTime);
return this.page(page, wrapper);
}
public String applyDesensitization(String rawData, String resourceType, String resourceId) { @Override
// 获取规则(先特定资源,再默认) public List<CoDesensitizationRule> getEnabledRulesByResource(String resourceType, String resourceId) {
LambdaQueryWrapper<CoDesensitizationRule> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CoDesensitizationRule> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CoDesensitizationRule::getResourceType, resourceType) wrapper.eq(CoDesensitizationRule::getIsDeleted, 0)
.eq(CoDesensitizationRule::getEnabled, 1) .eq(CoDesensitizationRule::getEnabled, 1)
.eq(StringUtils.isNotBlank(resourceType),CoDesensitizationRule::getResourceType, resourceType)
.eq(StringUtils.isNotBlank(resourceId),CoDesensitizationRule::getResourceId,resourceId)
.orderByAsc(CoDesensitizationRule::getSortOrder); .orderByAsc(CoDesensitizationRule::getSortOrder);
return this.list(wrapper);
}
List<CoDesensitizationRule> rules; @Override
// 先查特定资源 @Transactional(rollbackFor = Exception.class)
wrapper.eq(CoDesensitizationRule::getResourceId, resourceId); public void toggleEnabled(Long id, Integer enabled) {
rules = ruleMapper.selectList(wrapper); CoDesensitizationRule rule = this.getById(id);
if (rules.isEmpty()) { if (rule == null || rule.getIsDeleted() == 1) {
// 查默认规则 throw new BusinessException("规则不存在或已删除");
wrapper.clear();
wrapper.eq(CoDesensitizationRule::getResourceType, resourceType)
.eq(CoDesensitizationRule::getIsDefault, 1)
.eq(CoDesensitizationRule::getEnabled, 1)
.orderByAsc(CoDesensitizationRule::getSortOrder);
rules = ruleMapper.selectList(wrapper);
}
if (rules.isEmpty()) {
return rawData;
} }
rule.setEnabled(enabled);
rule.setUpdaterId("system");
rule.setUpdateTime(LocalDateTime.now());
this.updateById(rule);
log.info("脱敏规则 {} 状态已切换为: {}", id, enabled == 1 ? "启用" : "停用");
}
try { @Override
JsonNode root = objectMapper.readTree(rawData); @Transactional(rollbackFor = Exception.class)
for (CoDesensitizationRule rule : rules) { public void deleteById(Long id) {
String fieldPath = rule.getFieldPath(); CoDesensitizationRule rule = this.getById(id);
// 简单处理:假设路径是$.xxx,直接取字段 if (rule == null || rule.getIsDeleted() == 1) {
String fieldName = fieldPath.replace("$.", ""); throw new BusinessException("规则不存在或已删除");
if (root.has(fieldName)) {
String original = root.get(fieldName).asText();
String masked = maskValue(original, rule.getMaskType(), rule.getMaskConfig());
// 由于JsonNode不可变,简单返回,生产需构建新节点
}
}
return objectMapper.writeValueAsString(root);
} catch (Exception e) {
log.error("脱敏失败", e);
return rawData;
} }
rule.setIsDeleted(1);
rule.setUpdaterId("system");
rule.setUpdateTime(LocalDateTime.now());
this.updateById(rule);
log.info("脱敏规则 {} 已逻辑删除", id);
} }
private String maskValue(String original, String maskType, String maskConfig) { /**
try { * 保存或更新前的公共逻辑
JsonNode config = objectMapper.readTree(maskConfig != null ? maskConfig : "{}"); */
int prefix = config.has("prefix") ? config.get("prefix").asInt() : 0; @Override
int suffix = config.has("suffix") ? config.get("suffix").asInt() : 0; @Transactional(rollbackFor = Exception.class)
String replaceChar = config.has("replace_char") ? config.get("replace_char").asText() : "*"; public boolean saveOrUpdate(CoDesensitizationRule entity) {
if (entity == null) {
switch (maskType) { return false;
case "mask": }
case "partial": if (entity.getId() == null) {
if (original.length() <= prefix + suffix) return original; // 新增
int middle = original.length() - prefix - suffix; entity.setRuleBizId(RandomStringGenerator.generateBizId16(CommonEnum.UID_TYPE_DESENSITIZATION_RULE.getCode()));
return original.substring(0, prefix) + entity.setIsDeleted(0);
// replaceChar.repeat(middle) + entity.setCreateTime(LocalDateTime.now());
original.substring(original.length() - suffix); entity.setUpdateTime(LocalDateTime.now());
case "hide": if (StringUtils.isBlank(entity.getCreatorId())) {
return "****"; entity.setCreatorId("system");
case "replace": }
return config.has("replace_char") ? config.get("replace_char").asText() : "***"; if (StringUtils.isBlank(entity.getUpdaterId())) {
default: entity.setUpdaterId("system");
return original; }
// 默认启用
if (entity.getEnabled() == null) {
entity.setEnabled(1);
}
} else {
// 更新
CoDesensitizationRule exist = this.getById(entity.getId());
if (exist == null || exist.getIsDeleted() == 1) {
throw new BusinessException("规则不存在或已删除");
}
entity.setUpdateTime(LocalDateTime.now());
if (StringUtils.isBlank(entity.getUpdaterId())) {
entity.setUpdaterId("system");
} }
} catch (Exception e) { // 不允许修改 ruleBizId 和 isDeleted
return original; entity.setRuleBizId(null);
entity.setIsDeleted(null);
entity.setCreateTime(null);
} }
return super.saveOrUpdate(entity);
} }
} }
\ No newline at end of file
...@@ -5,6 +5,7 @@ import com.yd.communication.service.dao.CoOperationLogMapper; ...@@ -5,6 +5,7 @@ import com.yd.communication.service.dao.CoOperationLogMapper;
import com.yd.communication.service.service.ICoOperationLogService; import com.yd.communication.service.service.ICoOperationLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
...@@ -21,6 +22,7 @@ import org.springframework.stereotype.Service; ...@@ -21,6 +22,7 @@ import org.springframework.stereotype.Service;
public class CoOperationLogServiceImpl extends ServiceImpl<CoOperationLogMapper, CoOperationLog> implements ICoOperationLogService { public class CoOperationLogServiceImpl extends ServiceImpl<CoOperationLogMapper, CoOperationLog> implements ICoOperationLogService {
@Override @Override
@Async("communicationExecutor")
public void log(String bizId, String operatorId, String operatorType, public void log(String bizId, String operatorId, String operatorType,
String operatorName, String action, String content, String operatorName, String action, String content,
String deviceNumber, String ip) { String deviceNumber, String ip) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment