@@ -17,6 +17,7 @@ import com.hula.domain.vo.res.ApiResult;
|
||||
import com.hula.utils.RequestHolder;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -26,6 +27,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -134,6 +136,7 @@ public class ChatController {
|
||||
@PostMapping("/message")
|
||||
public ApiResult sendMessage(@Validated @RequestBody ChatCommand command) {
|
||||
command.setUid(RequestHolder.get().getUid());
|
||||
command.setOperater(command.getUid());
|
||||
command = gptService.validateGptCommand(command);
|
||||
return ApiResult.success(gptService.chatMessage(command));
|
||||
}
|
||||
@@ -162,7 +165,7 @@ public class ChatController {
|
||||
* @date: 2025/03/16
|
||||
* @version: 1.0.0
|
||||
*/
|
||||
@PostMapping("/completions")
|
||||
@PostMapping(value = "/completions", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public void completions(HttpServletResponse response, @RequestBody CompletionsParam completionsParam) {
|
||||
Boolean isWs = false;
|
||||
if (StrUtil.isNotEmpty(completionsParam.getWs())) {
|
||||
|
||||
@@ -38,7 +38,6 @@ public interface ChatMessageMapper extends BaseMapper<ChatMessage> {
|
||||
*/
|
||||
List<ChatMessageVO> listChatMessage(@Param("q") ChatParam query);
|
||||
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*
|
||||
|
||||
@@ -64,4 +64,14 @@ public class ChatParam implements Serializable {
|
||||
* 结束原因
|
||||
*/
|
||||
private String finishReason;
|
||||
|
||||
public Integer getOffset() {
|
||||
if (current == null || current < 1) {
|
||||
current = 1; // 默认第一页
|
||||
}
|
||||
if (size == null || size < 1) {
|
||||
size = 10; // 默认每页10条
|
||||
}
|
||||
return (current - 1) * size;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -125,6 +126,7 @@ public class ChatMessageServiceImpl extends ServiceImpl<ChatMessageMapper, ChatM
|
||||
.content(command.getPrompt()).contentType(ChatContentEnum.TEXT.getValue()).role(ChatRoleEnum.USER.getValue()).status(ChatStatusEnum.REPLY.getValue())
|
||||
.build();
|
||||
chatMessage.setCreatedBy(command.getOperater());
|
||||
chatMessage.setCreatedTime(LocalDateTime.now());
|
||||
chatMessageMapper.insert(chatMessage);
|
||||
return chatMessage.getMessageId();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import com.hula.exception.BizException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -104,6 +106,8 @@ public class ChatServiceImpl extends ServiceImpl<ChatMapper, Chat> implements IC
|
||||
int maxLength = 30;
|
||||
chat.setTitle(command.getPrompt().substring(0, command.getPrompt().length() > maxLength ? maxLength : command.getPrompt().length()));
|
||||
chat.setUid(command.getUid());
|
||||
chat.setCreatedBy(command.getUid());
|
||||
chat.setCreatedTime(LocalDateTime.now());
|
||||
chatMapper.insert(chat);
|
||||
return ChatVO.builder().chatNumber(chat.getChatNumber()).prompt(command.getPrompt()).build();
|
||||
}
|
||||
|
||||
@@ -72,9 +72,9 @@ public class RedisKey {
|
||||
public static final String GROUP_ANNOUNCEMENTS_FORMAT = "groupInfo:announcements_%d";
|
||||
|
||||
/**
|
||||
* 用户token存放
|
||||
* 用户token存放 格式:终端:uid:uuid
|
||||
*/
|
||||
public static final String USER_TOKEN_FORMAT = "userToken:%s:uid_%d";
|
||||
public static final String USER_TOKEN_FORMAT = "userToken:%s:uid_%d:%s";
|
||||
|
||||
/**
|
||||
* 用户refreshToken存放
|
||||
|
||||
@@ -16,10 +16,11 @@ import java.util.stream.Collectors;
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum YesOrNoEnum {
|
||||
NO(0, "否"),
|
||||
YES(1, "是"),
|
||||
NO(false,0, "否"),
|
||||
YES(true,1, "是"),
|
||||
;
|
||||
|
||||
private final Boolean bool;
|
||||
private final Integer status;
|
||||
private final String desc;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.UUID;
|
||||
* @author nyh
|
||||
*/
|
||||
@Slf4j
|
||||
@WebFilter(urlPatterns = "/*")
|
||||
@WebFilter(urlPatterns = "/*", asyncSupported = true)
|
||||
public class HttpTraceIdFilter implements Filter {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,6 +4,10 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* IP 工具类
|
||||
@@ -15,6 +19,10 @@ public class IPUtils {
|
||||
|
||||
// 需要检查的代理头,按优先级排序
|
||||
private static final String[] HEADERS_TO_CHECK = {
|
||||
"CF-Connecting-IP", // Cloudflare
|
||||
"True-Client-IP", // Akamai
|
||||
"X-Original-Forwarded-For", // Kubernetes
|
||||
"X-Client-IP",
|
||||
"X-Real-IP",
|
||||
"X-Forwarded-For",
|
||||
"Proxy-Client-IP",
|
||||
@@ -23,117 +31,194 @@ public class IPUtils {
|
||||
"HTTP_X_FORWARDED_FOR"
|
||||
};
|
||||
|
||||
/**
|
||||
* 从多级反向代理中获取第一个合法IP
|
||||
*/
|
||||
public static String getMultistageReverseProxyIp(String ip) {
|
||||
if (ip != null && ip.contains(",")) {
|
||||
String[] ips = ip.trim().split(",");
|
||||
for (String subIp : ips) {
|
||||
subIp = extractPureIp(subIp.trim());
|
||||
if (isValidIP(subIp) && !isUnknown(subIp)) {
|
||||
return subIp;
|
||||
}
|
||||
}
|
||||
}
|
||||
String pureIp = extractPureIp(ip);
|
||||
return isValidIP(pureIp) ? pureIp : null;
|
||||
// 私有IP范围CIDR(支持IPv4/IPv6)
|
||||
private static final List<String> PRIVATE_CIDRS = Arrays.asList(
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"fd00::/8",
|
||||
"::1/128"
|
||||
);
|
||||
|
||||
// IP格式预编译正则
|
||||
private static final Pattern IPv4_PATTERN = Pattern.compile(
|
||||
"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
||||
);
|
||||
private static final Pattern IPv6_PATTERN = Pattern.compile(
|
||||
"^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$|^[0-9a-fA-F]{1,4}::([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}$"
|
||||
);
|
||||
|
||||
private static boolean isValidHeaderValue(String value) {
|
||||
return value != null &&
|
||||
!value.trim().isEmpty() &&
|
||||
!"unknown".equalsIgnoreCase(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除端口号并提取纯IP
|
||||
* 提取无端口IP(增强IPv6处理)
|
||||
*/
|
||||
private static String extractPureIp(String ip) {
|
||||
if (ip == null) return null;
|
||||
ip = ip.trim();
|
||||
private static String extractPureIP(String ipWithPort) {
|
||||
if (ipWithPort == null) return null;
|
||||
String trimmed = ipWithPort.trim();
|
||||
|
||||
// 处理IPv6带端口格式 [::1]:8080
|
||||
if (ip.startsWith("[") && ip.contains("]")) {
|
||||
int end = ip.indexOf(']');
|
||||
ip = ip.substring(1, end);
|
||||
} else {
|
||||
// 处理IPv4或普通格式
|
||||
int colonPos = ip.indexOf(':');
|
||||
if (colonPos > 0 && !ip.substring(0, colonPos).contains(":")) {
|
||||
ip = ip.substring(0, colonPos);
|
||||
}
|
||||
if (trimmed.startsWith("[") && trimmed.contains("]")) {
|
||||
int endIndex = trimmed.indexOf(']');
|
||||
return trimmed.substring(1, endIndex);
|
||||
}
|
||||
return ip;
|
||||
|
||||
// 处理IPv4或简单IPv6
|
||||
int colonIndex = trimmed.indexOf(':');
|
||||
if (colonIndex > 0 && !trimmed.substring(0, colonIndex).contains(":")) {
|
||||
return trimmed.substring(0, colonIndex);
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字符串是否为"unknown"或空
|
||||
* 增强IP格式验证
|
||||
*/
|
||||
public static boolean isUnknown(String checkString) {
|
||||
return checkString == null || checkString.trim().isEmpty() || "unknown".equalsIgnoreCase(checkString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 严格验证IP合法性
|
||||
*/
|
||||
public static boolean isValidIP(String ip) {
|
||||
private static boolean isValidIPFormat(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return false;
|
||||
ip = ip.trim();
|
||||
|
||||
// 检查IPv4
|
||||
if (ip.contains(".")) {
|
||||
return isValidIPv4(ip);
|
||||
}
|
||||
// 检查IPv6
|
||||
else if (ip.contains(":")) {
|
||||
return isValidIPv6(ip);
|
||||
}
|
||||
return false;
|
||||
return IPv4_PATTERN.matcher(ip).matches() ||
|
||||
IPv6_PATTERN.matcher(ip).matches();
|
||||
}
|
||||
|
||||
private static boolean isValidIPv4(String ip) {
|
||||
String[] parts = ip.split("\\.");
|
||||
if (parts.length != 4) return false;
|
||||
|
||||
/**
|
||||
* 检查是否为私有IP
|
||||
*/
|
||||
private static boolean isPrivateIP(String ip) {
|
||||
try {
|
||||
for (String part : parts) {
|
||||
int num = Integer.parseInt(part);
|
||||
if (num < 0 || num > 255) return false;
|
||||
}
|
||||
return !ip.endsWith(".");
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidIPv6(String ip) {
|
||||
try {
|
||||
InetAddress.getByName(ip);
|
||||
return ip.contains(":") && (ip.split(":").length <= 8);
|
||||
InetAddress address = InetAddress.getByName(ip);
|
||||
return PRIVATE_CIDRS.stream()
|
||||
.anyMatch(cidr -> {
|
||||
IPWithMask cidrRange;
|
||||
try {
|
||||
cidrRange = new IPWithMask(cidr);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return cidrRange.contains(address);
|
||||
});
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实IP
|
||||
* 解析代理头值
|
||||
*/
|
||||
private static String parseProxyHeader(String headerValue) {
|
||||
if (headerValue.contains(",")) {
|
||||
List<String> ips = Arrays.stream(headerValue.split(","))
|
||||
.map(String::trim)
|
||||
.map(IPUtils::extractPureIP)
|
||||
.filter(IPUtils::isValidIPFormat)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 优先选择首个公网IP
|
||||
for (String candidate : ips) {
|
||||
if (!isPrivateIP(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
// 如果没有公网IP则返回第一个合法IP
|
||||
return ips.isEmpty() ? null : ips.get(0);
|
||||
}
|
||||
return extractPureIP(headerValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化特殊地址
|
||||
*/
|
||||
private static String normalizeSpecialIPs(String ip) {
|
||||
if (ip == null) return null;
|
||||
|
||||
// IPv6本地地址标准化
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip) || "::1".equals(ip)) {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
// 处理IPv4兼容格式
|
||||
return ip.replaceAll("^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$", "$1");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否为有效公网IP
|
||||
*/
|
||||
private static boolean isValidPublicIP(String ip) {
|
||||
return isValidIPFormat(ip) && !isPrivateIP(ip);
|
||||
}
|
||||
|
||||
private static boolean isInvalidIP(String ip) {
|
||||
return ip == null ||
|
||||
!isValidIPFormat(ip) ||
|
||||
"127.0.0.1".equals(ip);
|
||||
}
|
||||
|
||||
public static String getHostIp(HttpServletRequest request) {
|
||||
String ip = null;
|
||||
|
||||
// 按优先级检查代理头
|
||||
// 1. 检查代理头
|
||||
for (String header : HEADERS_TO_CHECK) {
|
||||
String headerValue = request.getHeader(header);
|
||||
if (!isUnknown(headerValue)) {
|
||||
ip = getMultistageReverseProxyIp(headerValue);
|
||||
if (ip != null) break;
|
||||
if (isValidHeaderValue(headerValue)) {
|
||||
ip = parseProxyHeader(headerValue);
|
||||
if (ip != null) {
|
||||
log.debug("Found valid IP [{}] in header: {}", ip, header);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 直接获取远端地址并处理特殊IP
|
||||
if (isUnknown(ip)) {
|
||||
// 2. 回退到远端地址
|
||||
if (isInvalidIP(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
// 处理IPv6本地地址
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip) || "::1".equals(ip)) {
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
log.debug("使用远程ip地址: {}", ip);
|
||||
}
|
||||
|
||||
return isValidIP(ip) ? ip : null;
|
||||
// 3. 处理特殊地址
|
||||
ip = normalizeSpecialIPs(ip);
|
||||
|
||||
// 4. 最终验证
|
||||
return isValidPublicIP(ip) ? ip : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CIDR处理辅助类
|
||||
*/
|
||||
private static class IPWithMask {
|
||||
private final InetAddress inetAddress;
|
||||
private final int mask;
|
||||
|
||||
IPWithMask(String cidr) throws UnknownHostException {
|
||||
String[] parts = cidr.split("/");
|
||||
this.inetAddress = InetAddress.getByName(parts[0]);
|
||||
this.mask = (parts.length > 1) ? Integer.parseInt(parts[1]) :
|
||||
(inetAddress.getAddress().length == 4) ? 32 : 128;
|
||||
}
|
||||
|
||||
boolean contains(InetAddress address) {
|
||||
if (!address.getClass().equals(inetAddress.getClass())) return false;
|
||||
|
||||
byte[] targetIP = address.getAddress();
|
||||
byte[] cidrIP = inetAddress.getAddress();
|
||||
|
||||
int maskBytes = mask / 8;
|
||||
int remainingBits = mask % 8;
|
||||
|
||||
// 检查完整字节部分
|
||||
for (int i = 0; i < maskBytes; i++) {
|
||||
if (targetIP[i] != cidrIP[i]) return false;
|
||||
}
|
||||
|
||||
// 处理剩余位
|
||||
if (remainingBits > 0) {
|
||||
byte maskByte = (byte) (0xFF << (8 - remainingBits));
|
||||
return (targetIP[maskBytes] & maskByte) == (cidrIP[maskBytes] & maskByte);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.hula.common.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* IP 工具类
|
||||
*
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Slf4j
|
||||
public class OldIPUtils {
|
||||
|
||||
// 需要检查的代理头,按优先级排序
|
||||
private static final String[] HEADERS_TO_CHECK = {
|
||||
"X-Real-IP",
|
||||
"X-Forwarded-For",
|
||||
"Proxy-Client-IP",
|
||||
"WL-Proxy-Client-IP",
|
||||
"HTTP_CLIENT_IP",
|
||||
"HTTP_X_FORWARDED_FOR"
|
||||
};
|
||||
|
||||
/**
|
||||
* 从多级反向代理中获取第一个合法IP
|
||||
*/
|
||||
public static String getMultistageReverseProxyIp(String ip) {
|
||||
if (ip != null && ip.contains(",")) {
|
||||
String[] ips = ip.trim().split(",");
|
||||
for (String subIp : ips) {
|
||||
subIp = extractPureIp(subIp.trim());
|
||||
if (isValidIP(subIp) && !isUnknown(subIp)) {
|
||||
return subIp;
|
||||
}
|
||||
}
|
||||
}
|
||||
String pureIp = extractPureIp(ip);
|
||||
return isValidIP(pureIp) ? pureIp : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除端口号并提取纯IP
|
||||
*/
|
||||
private static String extractPureIp(String ip) {
|
||||
if (ip == null) return null;
|
||||
ip = ip.trim();
|
||||
|
||||
// 处理IPv6带端口格式 [::1]:8080
|
||||
if (ip.startsWith("[") && ip.contains("]")) {
|
||||
int end = ip.indexOf(']');
|
||||
ip = ip.substring(1, end);
|
||||
} else {
|
||||
// 处理IPv4或普通格式
|
||||
int colonPos = ip.indexOf(':');
|
||||
if (colonPos > 0 && !ip.substring(0, colonPos).contains(":")) {
|
||||
ip = ip.substring(0, colonPos);
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字符串是否为"unknown"或空
|
||||
*/
|
||||
public static boolean isUnknown(String checkString) {
|
||||
return checkString == null || checkString.trim().isEmpty() || "unknown".equalsIgnoreCase(checkString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 严格验证IP合法性
|
||||
*/
|
||||
public static boolean isValidIP(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return false;
|
||||
ip = ip.trim();
|
||||
|
||||
// 检查IPv4
|
||||
if (ip.contains(".")) {
|
||||
return isValidIPv4(ip);
|
||||
}
|
||||
// 检查IPv6
|
||||
else if (ip.contains(":")) {
|
||||
return isValidIPv6(ip);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isValidIPv4(String ip) {
|
||||
String[] parts = ip.split("\\.");
|
||||
if (parts.length != 4) return false;
|
||||
|
||||
try {
|
||||
for (String part : parts) {
|
||||
int num = Integer.parseInt(part);
|
||||
if (num < 0 || num > 255) return false;
|
||||
}
|
||||
return !ip.endsWith(".");
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidIPv6(String ip) {
|
||||
try {
|
||||
InetAddress.getByName(ip);
|
||||
return ip.contains(":") && (ip.split(":").length <= 8);
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实IP
|
||||
*/
|
||||
public static String getHostIp(HttpServletRequest request) {
|
||||
String ip = null;
|
||||
|
||||
// 按优先级检查代理头
|
||||
for (String header : HEADERS_TO_CHECK) {
|
||||
String headerValue = request.getHeader(header);
|
||||
if (!isUnknown(headerValue)) {
|
||||
ip = getMultistageReverseProxyIp(headerValue);
|
||||
if (ip != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
// 直接获取远端地址并处理特殊IP
|
||||
if (isUnknown(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
// 处理IPv6本地地址
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip) || "::1".equals(ip)) {
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
}
|
||||
|
||||
return isValidIP(ip) ? ip : null;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ public class ChatMessageMarkDTO {
|
||||
@Schema(description ="消息id")
|
||||
private Long msgId;
|
||||
|
||||
@Schema(description ="标记类型 1点赞 2举报")
|
||||
private Integer markType;
|
||||
|
||||
@Schema(description ="动作类型 1确认 2取消")
|
||||
|
||||
@@ -42,7 +42,7 @@ public class MessageMark implements Serializable {
|
||||
private Long uid;
|
||||
|
||||
/**
|
||||
* 标记类型 1点赞 2举报
|
||||
* 标记类型 @see com.hula.core.chat.domain.enums.MessageMarkTypeEnum
|
||||
*/
|
||||
@TableField("type")
|
||||
private Integer type;
|
||||
|
||||
@@ -15,21 +15,36 @@ import java.util.stream.Collectors;
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum MessageMarkTypeEnum {
|
||||
LIKE(1, "点赞", 10),
|
||||
DISLIKE(2, "点踩", 5),
|
||||
;
|
||||
LIKE(1, "点赞", 10),
|
||||
DISLIKE(2, "不满", 5),
|
||||
// 新增表情互动类型
|
||||
HEART(3, "爱心", 15), // 表达喜爱
|
||||
ANGRY(4, "愤怒", 20), // 表达不满
|
||||
CELEBRATE(5, "礼炮", 30), // 庆祝消息
|
||||
ROCKET(6, "火箭", 50), // 超级认可
|
||||
LOL(7, "笑哭", 15), // 觉得有趣
|
||||
APPLAUSE(8, "鼓掌", 25), // 表示赞赏
|
||||
FLOWER(9, "鲜花", 20), // 送花支持
|
||||
BOMB(10, "炸弹", 40), // 强烈反对
|
||||
CONFUSED(11, "疑问", 15), // 表示疑惑
|
||||
VICTORY(12, "胜利", 35), // 庆祝胜利
|
||||
LIGHT(13, "灯光", 30), // 打call支持
|
||||
MONEY(14, "红包", 45); // 打赏支持
|
||||
|
||||
private final Integer type;
|
||||
private final String desc;
|
||||
private final Integer riseNum;//需要多少个标记升级
|
||||
private final Integer type;
|
||||
private final String desc;
|
||||
private final Integer riseNum; // 需要多少个标记升级 TODO 达到多少次数之后触发的效果
|
||||
|
||||
private static Map<Integer, MessageMarkTypeEnum> cache;
|
||||
private static final Map<Integer, MessageMarkTypeEnum> CACHE;
|
||||
|
||||
static {
|
||||
cache = Arrays.stream(MessageMarkTypeEnum.values()).collect(Collectors.toMap(MessageMarkTypeEnum::getType, Function.identity()));
|
||||
}
|
||||
static {
|
||||
CACHE = Arrays.stream(values()).collect(Collectors.toMap(
|
||||
MessageMarkTypeEnum::getType,
|
||||
Function.identity()
|
||||
));
|
||||
}
|
||||
|
||||
public static MessageMarkTypeEnum of(Integer type) {
|
||||
return cache.get(type);
|
||||
}
|
||||
}
|
||||
public static MessageMarkTypeEnum of(Integer type) {
|
||||
return CACHE.get(type);
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,10 @@ public class ChatMessageMarkReq {
|
||||
@Schema(description ="消息id")
|
||||
private Long msgId;
|
||||
|
||||
/**
|
||||
* @see com.hula.core.chat.domain.enums.MessageMarkTypeEnum
|
||||
*/
|
||||
@NotNull
|
||||
@Schema(description ="标记类型 1点赞 2举报")
|
||||
private Integer markType;
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -20,4 +20,7 @@ public class MemberExitReq {
|
||||
@NotNull
|
||||
@Schema(description ="会话id")
|
||||
private Long roomId;
|
||||
|
||||
@Schema(description ="搜索关键字")
|
||||
private String account;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 消息
|
||||
@@ -42,19 +43,16 @@ public class ChatMessageResp extends BaseEntity {
|
||||
private Integer type;
|
||||
@Schema(description ="消息内容不同的消息类型,内容体不同")
|
||||
private Object body;
|
||||
@Schema(description ="消息标记")
|
||||
private MessageMark messageMark;
|
||||
@Schema(description = "扩展标记统计(type为MessageMarkTypeEnum的type字段)")
|
||||
private Map<Integer, MarkItem> messageMarks;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MessageMark {
|
||||
@Schema(description ="点赞数")
|
||||
private Integer likeCount;
|
||||
@Schema(description ="该用户是否已经点赞 0否 1是")
|
||||
private Integer userLike;
|
||||
@Schema(description ="举报数")
|
||||
private Integer dislikeCount;
|
||||
@Schema(description ="该用户是否已经举报 0否 1是")
|
||||
private Integer userDislike;
|
||||
}
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class MarkItem {
|
||||
@Schema(description = "标记数量")
|
||||
private Integer count;
|
||||
@Schema(description = "当前用户是否标记")
|
||||
private Boolean userMarked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,21 +60,28 @@ public class MessageAdapter {
|
||||
messageVO.setBody(msgHandler.showMsg(message));
|
||||
}
|
||||
//消息标记
|
||||
messageVO.setMessageMark(buildMsgMark(marks, receiveUid));
|
||||
messageVO.setMessageMarks(buildMsgMark(marks, receiveUid));
|
||||
return messageVO;
|
||||
}
|
||||
|
||||
private static ChatMessageResp.MessageMark buildMsgMark(List<MessageMark> marks, Long receiveUid) {
|
||||
Map<Integer, List<MessageMark>> typeMap = marks.stream().collect(Collectors.groupingBy(MessageMark::getType));
|
||||
List<MessageMark> likeMarks = typeMap.getOrDefault(MessageMarkTypeEnum.LIKE.getType(), new ArrayList<>());
|
||||
List<MessageMark> dislikeMarks = typeMap.getOrDefault(MessageMarkTypeEnum.DISLIKE.getType(), new ArrayList<>());
|
||||
ChatMessageResp.MessageMark mark = new ChatMessageResp.MessageMark();
|
||||
mark.setLikeCount(likeMarks.size());
|
||||
mark.setUserLike(Optional.ofNullable(receiveUid).filter(uid -> likeMarks.stream().anyMatch(a -> Objects.equals(a.getUid(), uid))).map(a -> YesOrNoEnum.YES.getStatus()).orElse(YesOrNoEnum.NO.getStatus()));
|
||||
mark.setDislikeCount(dislikeMarks.size());
|
||||
mark.setUserDislike(Optional.ofNullable(receiveUid).filter(uid -> dislikeMarks.stream().anyMatch(a -> Objects.equals(a.getUid(), uid))).map(a -> YesOrNoEnum.YES.getStatus()).orElse(YesOrNoEnum.NO.getStatus()));
|
||||
return mark;
|
||||
}
|
||||
private static Map<Integer, ChatMessageResp.MarkItem> buildMsgMark(List<MessageMark> marks, Long receiveUid) {
|
||||
Map<Integer, List<MessageMark>> typeMap = marks.stream().collect(Collectors.groupingBy(MessageMark::getType));
|
||||
|
||||
// 构造动态统计为主数据源
|
||||
Map<Integer, ChatMessageResp.MarkItem> stats = new HashMap<>();
|
||||
|
||||
// 批量映射操作数量
|
||||
Arrays.stream(MessageMarkTypeEnum.values()).forEach(typeEnum -> {
|
||||
List<MessageMark> list = typeMap.getOrDefault(typeEnum.getType(), Collections.emptyList());
|
||||
|
||||
stats.put(typeEnum.getType(), new ChatMessageResp.MarkItem(list.size(), Optional.ofNullable(receiveUid)
|
||||
.filter(uid -> list.stream().anyMatch(m -> m != null && Objects.equals(m.getUid(), uid)))
|
||||
.map(uid -> YesOrNoEnum.YES.getBool())
|
||||
.orElse(YesOrNoEnum.NO.getBool())));
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
private static ChatMessageResp.UserInfo buildFromUser(Long fromUid) {
|
||||
ChatMessageResp.UserInfo userInfo = new ChatMessageResp.UserInfo();
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@@ -39,6 +40,14 @@ public class RoomGroupCache extends AbstractRedisStringCache<Long, RoomGroup> {
|
||||
return roomGroups.stream().collect(Collectors.toMap(RoomGroup::getRoomId, Function.identity()));
|
||||
}
|
||||
|
||||
public RoomGroup getByRoomId(Long roomId) {
|
||||
return roomGroupDao.getByRoomId(roomId);
|
||||
}
|
||||
|
||||
public boolean removeById(Serializable id) {
|
||||
return roomGroupDao.removeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据群号查询群信息
|
||||
*/
|
||||
@@ -56,4 +65,11 @@ public class RoomGroupCache extends AbstractRedisStringCache<Long, RoomGroup> {
|
||||
public List<Long> evictGroup(String account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有群组相关缓存
|
||||
*/
|
||||
@CacheEvict(cacheNames = "room", allEntries = true)
|
||||
public void evictAllCaches() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ public class ChatServiceImpl implements ChatService {
|
||||
private RoomCache roomCache;
|
||||
private GroupMemberDao groupMemberDao;
|
||||
private RoomGroupCache roomGroupCache;
|
||||
private RoomGroupDao roomGroupDao;
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
@@ -172,7 +171,7 @@ public class ChatServiceImpl implements ChatService {
|
||||
}
|
||||
// 获取群成员角色ID
|
||||
List<Long> uidList = resultList.stream().map(item -> Long.parseLong(item.getUid())).collect(Collectors.toList());
|
||||
RoomGroup roomGroup = roomGroupDao.getByRoomId(request.getRoomId());
|
||||
RoomGroup roomGroup = roomGroupCache.getByRoomId(request.getRoomId());
|
||||
Map<String, Integer> uidMapRole = groupMemberDao.getMemberMapRole(roomGroup.getId(), uidList);
|
||||
resultList.forEach(member -> member.setRoleId(uidMapRole.get(member.getUid())));
|
||||
// 组装结果
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.hula.core.chat.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.hula.core.chat.service.adapter.RoomAdapter;
|
||||
import com.hula.core.chat.service.cache.RoomGroupCache;
|
||||
import com.hula.enums.CommonErrorEnum;
|
||||
import com.hula.enums.GroupErrorEnum;
|
||||
import com.hula.utils.AssertUtil;
|
||||
@@ -35,10 +37,10 @@ import static com.hula.core.chat.constant.GroupConst.MAX_MANAGE_COUNT;
|
||||
public class GroupMemberServiceImpl implements IGroupMemberService {
|
||||
|
||||
private GroupMemberDao groupMemberDao;
|
||||
private RoomGroupDao roomGroupDao;
|
||||
private RoomDao roomDao;
|
||||
private ContactDao contactDao;
|
||||
private MessageDao messageDao;
|
||||
private RoomGroupCache roomGroupCache;
|
||||
private GroupMemberCache groupMemberCache;
|
||||
private PushService pushService;
|
||||
|
||||
@@ -51,7 +53,7 @@ public class GroupMemberServiceImpl implements IGroupMemberService {
|
||||
@Override
|
||||
public void addAdmin(Long uid, AdminAddReq request) {
|
||||
// 1. 判断群聊是否存在
|
||||
RoomGroup roomGroup = roomGroupDao.getByRoomId(request.getRoomId());
|
||||
RoomGroup roomGroup = roomGroupCache.getByRoomId(request.getRoomId());
|
||||
AssertUtil.isNotEmpty(roomGroup, GroupErrorEnum.GROUP_NOT_EXIST);
|
||||
|
||||
// 2. 判断该用户是否是群主
|
||||
@@ -83,7 +85,7 @@ public class GroupMemberServiceImpl implements IGroupMemberService {
|
||||
@Override
|
||||
public void revokeAdmin(Long uid, AdminRevokeReq request) {
|
||||
// 1. 判断群聊是否存在
|
||||
RoomGroup roomGroup = roomGroupDao.getByRoomId(request.getRoomId());
|
||||
RoomGroup roomGroup = roomGroupCache.getByRoomId(request.getRoomId());
|
||||
AssertUtil.isNotEmpty(roomGroup, GroupErrorEnum.GROUP_NOT_EXIST);
|
||||
|
||||
// 2. 判断该用户是否是群主
|
||||
@@ -109,7 +111,7 @@ public class GroupMemberServiceImpl implements IGroupMemberService {
|
||||
public void exitGroup(Long uid, MemberExitReq request) {
|
||||
Long roomId = request.getRoomId();
|
||||
// 1. 判断群聊是否存在
|
||||
RoomGroup roomGroup = roomGroupDao.getByRoomId(roomId);
|
||||
RoomGroup roomGroup = roomGroupCache.getByRoomId(roomId);
|
||||
AssertUtil.isNotEmpty(roomGroup, GroupErrorEnum.GROUP_NOT_EXIST);
|
||||
|
||||
// 2. 判断房间是否是大群聊 (大群聊禁止退出)
|
||||
@@ -123,8 +125,13 @@ public class GroupMemberServiceImpl implements IGroupMemberService {
|
||||
// 4. 判断该用户是否是群主
|
||||
Boolean isLord = groupMemberDao.isLord(roomGroup.getId(), uid);
|
||||
if (isLord) {
|
||||
// 4.1 删除房间
|
||||
// 4.1 删除房间和群并清除缓存
|
||||
boolean isDelRoom = roomDao.removeById(roomId);
|
||||
roomGroupCache.removeById(roomGroup.getId());
|
||||
roomGroupCache.evictGroup(roomGroup.getAccount());
|
||||
if(StrUtil.isNotEmpty(request.getAccount())){
|
||||
roomGroupCache.evictGroup(request.getAccount());
|
||||
}
|
||||
AssertUtil.isTrue(isDelRoom, CommonErrorEnum.SYSTEM_ERROR);
|
||||
// 4.2 删除会话
|
||||
Boolean isDelContact = contactDao.removeByRoomId(roomId, Collections.EMPTY_LIST);
|
||||
|
||||
@@ -600,6 +600,7 @@ public class RoomAppServiceImpl implements RoomAppService {
|
||||
groupMemberDao.saveBatch(groupMembers);
|
||||
|
||||
// 发送邀请加群消息 ==> 触发每个人的会话
|
||||
roomGroupCache.evictAllCaches();
|
||||
applicationEventPublisher.publishEvent(new GroupMemberAddEvent(this, roomGroup, groupMembers, uid));
|
||||
return roomGroup.getRoomId();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 愤怒标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class AngryStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.ANGRY;
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void doMark(Long uid, Long msgId) {
|
||||
// super.doMark(uid, msgId);
|
||||
// // 标记愤怒时清除正向表情
|
||||
// MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.LIKE.getType()).unMark(uid, msgId);
|
||||
// MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.HEART.getType()).unMark(uid, msgId);
|
||||
// MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.MONEY.getType()).unMark(uid, msgId);
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 鼓掌标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class ApplauseStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.APPLAUSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 炸弹标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class BombStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.BOMB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doMark(Long uid, Long msgId) {
|
||||
super.doMark(uid, msgId);
|
||||
// 使用炸弹时清除正向标记
|
||||
MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.LIKE.getType()).unMark(uid, msgId);
|
||||
MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.MONEY.getType()).unMark(uid, msgId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 礼炮标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class CelebrateStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.CELEBRATE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 疑问标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class ConfusedStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.CONFUSED;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 点踩标记策略类
|
||||
* 不满标记策略类
|
||||
* @author nyh
|
||||
*/
|
||||
@Component
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 鲜花标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class FlowerStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.FLOWER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 爱心标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class HeartStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.HEART;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 笑哭标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class LOLStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.LOL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 灯光标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class LightStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.LIGHT;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ public class LikeStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
public void doMark(Long uid, Long msgId) {
|
||||
super.doMark(uid, msgId);
|
||||
//同时取消点踩的动作
|
||||
//同时取消不满的动作
|
||||
MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.DISLIKE.getType()).unMark(uid, msgId);
|
||||
// MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.BOMB.getType()).unMark(uid, msgId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 红包标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class MoneyStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.MONEY;
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void doMark(Long uid, Long msgId) {
|
||||
// super.doMark(uid, msgId);
|
||||
// // 打赏时清除所有负面标记
|
||||
// MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.BOMB.getType()).unMark(uid, msgId);
|
||||
// MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.DISLIKE.getType()).unMark(uid, msgId);
|
||||
// MsgMarkFactory.getStrategyNoNull(MessageMarkTypeEnum.ANGRY.getType()).unMark(uid, msgId);
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 火箭标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class RocketStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.ROCKET;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hula.core.chat.service.strategy.mark;
|
||||
|
||||
import com.hula.core.chat.domain.enums.MessageMarkTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 胜利标记策略
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Component
|
||||
public class VictoryStrategy extends AbstractMsgMarkStrategy {
|
||||
@Override
|
||||
protected MessageMarkTypeEnum getTypeEnum() {
|
||||
return MessageMarkTypeEnum.VICTORY;
|
||||
}
|
||||
}
|
||||
@@ -29,4 +29,7 @@ public class BindEmailReq implements Serializable {
|
||||
|
||||
@Schema(description = "uuid")
|
||||
private String uuid;
|
||||
|
||||
@Schema(description = "操作类型")
|
||||
private String operationType = "register";
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package com.hula.core.user.domain.vo.req.user;
|
||||
|
||||
import com.hula.domain.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author 乾乾
|
||||
*/
|
||||
@Data
|
||||
public class ForgotPasswordReq extends BaseEntity {
|
||||
public class ForgotPasswordReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "请填写邮箱")
|
||||
@Schema(description = "邮箱")
|
||||
|
||||
@@ -28,12 +28,16 @@ public class OffLineResp {
|
||||
@Schema(description = "登录ip")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "本次登录uuid (不需要被清空)")
|
||||
private String uuid;
|
||||
|
||||
public OffLineResp() {
|
||||
}
|
||||
|
||||
public OffLineResp(Long uid, String client, String ip) {
|
||||
public OffLineResp(Long uid, String client, String ip, String uuid) {
|
||||
this.ip = ip;
|
||||
this.uid = uid;
|
||||
this.client = client;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ public class WSMsgMark {
|
||||
private Long uid;
|
||||
@Schema(description ="消息id")
|
||||
private Long msgId;
|
||||
@Schema(description ="标记类型 1点赞 2举报")
|
||||
/**
|
||||
* @see com.hula.core.chat.domain.enums.MessageMarkTypeEnum
|
||||
*/
|
||||
private Integer markType;
|
||||
@Schema(description ="被标记的数量")
|
||||
private Integer markCount;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class EmailServiceImpl implements EmailService {
|
||||
}
|
||||
|
||||
// 2. 校验邮箱是否存在
|
||||
if (userDao.existsByEmailAndIdNot(null, req.getEmail())) {
|
||||
if ("register".equals(req.getOperationType()) && userDao.existsByEmailAndIdNot(null, req.getEmail())) {
|
||||
throw new BizException("该邮箱已被其他账号绑定");
|
||||
}
|
||||
|
||||
|
||||
@@ -136,19 +136,19 @@ public class LoginServiceImpl implements LoginService {
|
||||
// 1. 拿到token
|
||||
String token = RequestHolder.get().getToken();
|
||||
|
||||
// 2. 解析token里面的数据,精准拿到当前用户的refreshToken
|
||||
try {
|
||||
// 2. 解析token里面的数据,精准拿到当前用户的refreshToken
|
||||
Map<String, Claim> verifyToken = JwtUtils.verifyToken(token);
|
||||
Long uid = verifyToken.get(JwtUtils.UID_CLAIM).asLong();
|
||||
String type = verifyToken.get(JwtUtils.LOGIN_TYPE_CLAIM).asString();
|
||||
String uuid = verifyToken.get(JwtUtils.UUID_CLAIM).asString();
|
||||
|
||||
if(!autoLogin){
|
||||
// 2.1 用户启用自动登录,删除refreshToken
|
||||
Map<String, Claim> verifyToken = JwtUtils.verifyToken(token);
|
||||
Long uid = verifyToken.get(JwtUtils.UID_CLAIM).asLong();
|
||||
String type = verifyToken.get(JwtUtils.LOGIN_TYPE_CLAIM).asString();
|
||||
String key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN_FORMAT, type, uid, verifyToken.get(JwtUtils.UUID_CLAIM).asString());
|
||||
RedisUtils.del(key);
|
||||
RedisUtils.del(RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN_FORMAT, type, uid, uuid));
|
||||
}
|
||||
|
||||
// 3. 删除token
|
||||
RedisUtils.del(RedisKey.getKey(RedisKey.USER_TOKEN_FORMAT, JwtUtils.getLoginType(token), RequestHolder.get().getUid()));
|
||||
RedisUtils.del(RedisKey.getKey(RedisKey.USER_TOKEN_FORMAT, type, uid, uuid));
|
||||
applicationEventPublisher.publishEvent(new UserOfflineEvent(this, User.builder().id(RequestHolder.get().getUid()).lastOptTime(DateUtil.date()).build()));
|
||||
} catch (Exception e) {
|
||||
throw TokenExceedException.expired();
|
||||
|
||||
@@ -39,18 +39,21 @@ public class TokenServiceImpl implements TokenService {
|
||||
|
||||
@Override
|
||||
public boolean verify(String token) {
|
||||
Long uid = JwtUtils.getUidOrNull(token);
|
||||
if (Objects.isNull(uid)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(token, RedisUtils.getStr(RedisKey.getKey(RedisKey.USER_TOKEN_FORMAT, JwtUtils.getLoginType(token), uid)));
|
||||
Map<String, Claim> claim = JwtUtils.verifyToken(token);
|
||||
if (Objects.isNull(claim)) {
|
||||
return false;
|
||||
}
|
||||
Long uid = claim.get(JwtUtils.UID_CLAIM).asLong();
|
||||
String loginType = claim.get(JwtUtils.LOGIN_TYPE_CLAIM).asString();
|
||||
String uuid = claim.get(JwtUtils.UUID_CLAIM).asString();
|
||||
return Objects.equals(token, RedisUtils.getStr(RedisKey.getKey(RedisKey.USER_TOKEN_FORMAT, loginType, uid, uuid)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginResultVO createToken(Long uid, String loginType) {
|
||||
// 1. uuid用于后续区分续签是给哪个token续签
|
||||
String uuid = UUID.randomUUID().toString(true);
|
||||
String tokenKey = RedisKey.getKey(RedisKey.USER_TOKEN_FORMAT, loginType, uid);
|
||||
String tokenKey = RedisKey.getKey(RedisKey.USER_TOKEN_FORMAT, loginType, uid, uuid);
|
||||
String refreshTokenKey = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN_FORMAT, loginType, uid, uuid);
|
||||
String token = RedisUtils.getStr(tokenKey), refreshToken;
|
||||
String key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN_UID_FORMAT, loginType, uid);
|
||||
@@ -58,7 +61,7 @@ public class TokenServiceImpl implements TokenService {
|
||||
|
||||
// 1.2 token存在 旧设备下线
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
applicationEventPublisher.publishEvent(new TokenExpireEvent(this, new OffLineResp(uid, loginType, RequestHolder.get().getIp())));
|
||||
applicationEventPublisher.publishEvent(new TokenExpireEvent(this, new OffLineResp(uid, loginType, RequestHolder.get().getIp(), uuid)));
|
||||
}
|
||||
|
||||
// 2. 创建用户token
|
||||
|
||||
@@ -183,7 +183,7 @@ public class WXMsgServiceImpl implements WebSocketService {
|
||||
}
|
||||
// 让前端的token失效
|
||||
else {
|
||||
sendMsg(channel, WsAdapter.buildInvalidateTokenResp(new OffLineResp(JwtUtils.getUidOrNull(wsAuthorize.getToken()), LoginTypeEnum.PC.getType(), null)));
|
||||
sendMsg(channel, WsAdapter.buildInvalidateTokenResp(new OffLineResp(JwtUtils.getUidOrNull(wsAuthorize.getToken()), LoginTypeEnum.PC.getType(), null, null)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,25 +25,25 @@
|
||||
<!-- 通用查询结果列 -->
|
||||
<sql id="Base_Column_List">
|
||||
t.id, t.created_by, t.created_time, t.updated_by, t.updated_time, t.avatar, t.icon, t.title, t.tag, t.description, t.system_prompt, t.first_message,
|
||||
t.type_id, t.main_model, t.status, t.sort, t.del
|
||||
t.type_id, t.main_model, t.status, t.sort, t.is_del
|
||||
</sql>
|
||||
|
||||
<!-- 通用查询条件 -->
|
||||
<sql id="BaseSelect">
|
||||
select <include refid="Base_Column_List"></include>, t1.name as typeName
|
||||
from gpt_assistant t
|
||||
left join gpt_assistant_type t1 on t.type_id = t1.id
|
||||
from ai_gpt_assistant t
|
||||
left join ai_gpt_assistant_type t1 on t.type_id = t1.id
|
||||
<where>
|
||||
t.del = 0
|
||||
<if test="q.id != null and q.id != ''"> and t.id = #{q.id} </if>
|
||||
t.is_del = 0
|
||||
<!-- <if test="q.id != null and q.id != ''"> and t.id = #{q.id} </if>-->
|
||||
<if test="q.title != null and q.title != ''"> and t.title like concat('%', #{q.title}, '%')</if>
|
||||
<if test="q.typeId != null and q.typeId != '' and q.typeId != -1"> and t.type_id = #{q.typeId}</if>
|
||||
<if test="q.mainModel != null"> and t.main_model = #{q.mainModel}</if>
|
||||
<if test="q.sort != null"> and t.sort = #{q.sort}</if>
|
||||
<if test="q.status != null"> and t.status = #{q.status}</if>
|
||||
<if test="q.description != null and q.description != ''"> and t.description = #{q.description}</if>
|
||||
<if test="q.startDate != null and q.startDate != ''"> and date_format(t.created_time,'%Y-%m-%d') >= #{q.startDate} </if>
|
||||
<if test="q.endDate != null and q.endDate != ''"> and date_format(t.created_time,'%Y-%m-%d') <= #{q.endDate} </if>
|
||||
<!-- <if test="q.startDate != null and q.startDate != ''"> and date_format(t.created_time,'%Y-%m-%d') >= #{q.startDate} </if>-->
|
||||
<!-- <if test="q.endDate != null and q.endDate != ''"> and date_format(t.created_time,'%Y-%m-%d') <= #{q.endDate} </if>-->
|
||||
</where>
|
||||
order by t.sort desc, t.id desc
|
||||
</sql>
|
||||
@@ -58,18 +58,18 @@
|
||||
|
||||
<select id="listAssistantRandom" resultType="com.hula.ai.gpt.pojo.vo.AssistantVO">
|
||||
select <include refid="Base_Column_List"></include>
|
||||
from gpt_assistant t
|
||||
from ai_gpt_assistant t
|
||||
<where>
|
||||
t.del = 0
|
||||
<if test="q.id != null and q.id != ''"> and t.id = #{q.id} </if>
|
||||
t.is_del = 0
|
||||
<!-- <if test="q.id != null and q.id != ''"> and t.id = #{q.id} </if>-->
|
||||
<if test="q.title != null and q.title != ''"> and t.title = #{q.title}</if>
|
||||
<if test="q.typeId != null and q.typeId != -1"> and t.type_id = #{q.typeId}</if>
|
||||
<if test="q.mainModel != null"> and t.main_model = #{q.mainModel}</if>
|
||||
<if test="q.sort != null"> and t.sort = #{q.sort}</if>
|
||||
<if test="q.status != null"> and t.status = #{q.status}</if>
|
||||
<if test="q.startDate != null and q.startDate != ''"> and date_format(t.created_time,'%Y-%m-%d') >= #{q.startDate} </if>
|
||||
<if test="q.endDate != null and q.endDate != ''"> and date_format(t.created_time,'%Y-%m-%d') <= #{q.endDate} </if>
|
||||
<if test="q.dataScope != null and q.dataScope != ''"> ${q.dataScope} </if>
|
||||
<!-- <if test="q.startDate != null and q.startDate != ''"> and date_format(t.created_time,'%Y-%m-%d') >= #{q.startDate} </if>-->
|
||||
<!-- <if test="q.endDate != null and q.endDate != ''"> and date_format(t.created_time,'%Y-%m-%d') <= #{q.endDate} </if>-->
|
||||
<!-- <if test="q.dataScope != null and q.dataScope != ''"> ${q.dataScope} </if>-->
|
||||
</where>
|
||||
ORDER BY RAND() LIMIT #{q.size}
|
||||
</select>
|
||||
|
||||
@@ -11,30 +11,30 @@
|
||||
<result column="updated_time" property="updatedTime"/>
|
||||
<result column="chat_number" property="chatNumber"/>
|
||||
<result column="assistant_id" property="assistantId"/>
|
||||
<result column="user_id" property="userId"/>
|
||||
<result column="uid" property="uid"/>
|
||||
<result column="title" property="title"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 通用查询结果列 -->
|
||||
<sql id="Base_Column_List">
|
||||
t.id, t.created_by, t.created_time, t.updated_by, t.updated_time, t.chat_number, t.assistant_id, t.user_id, t.title, t.del
|
||||
t.id, t.created_by, t.created_time, t.updated_by, t.updated_time, t.chat_number, t.assistant_id, t.uid, t.title, t.is_del
|
||||
</sql>
|
||||
|
||||
<!-- 通用查询条件 -->
|
||||
<sql id="BaseSelect">
|
||||
select <include refid="Base_Column_List"></include>, t1.nick_name as userName, t2.title as assistantTitle, t2.icon as assistantIcon, t2.avatar as assistantAvatar
|
||||
from gpt_chat t
|
||||
left join gpt_user t1 on t.user_id = t1.id
|
||||
left join gpt_assistant t2 on t.assistant_id = t2.id
|
||||
select <include refid="Base_Column_List"></include>, t1.name as userName, t2.title as assistantTitle, t2.icon as assistantIcon, t2.avatar as assistantAvatar
|
||||
from ai_gpt_chat t
|
||||
left join user t1 on t.uid = t1.id
|
||||
left join ai_gpt_assistant t2 on t.assistant_id = t2.id
|
||||
<where>
|
||||
t.del = 0
|
||||
<if test="q.id != null and q.id != ''"> and t.id = #{q.id} </if>
|
||||
<if test="q.userId != null"> and t.user_id = #{q.userId}</if>
|
||||
<if test="q.userName != null"> and (t1.nick_name like concat('%', #{q.userName}, '%') or t1.tel like concat('%', #{q.userName}, '%') )</if>
|
||||
t.is_del = 0
|
||||
<!-- <if test="q.id != null and q.id != ''"> and t.id = #{q.id} </if>-->
|
||||
<if test="q.uid != null"> and t.uid = #{q.uid}</if>
|
||||
<if test="q.userName != null"> and (t1.name like concat('%', #{q.userName}, '%') or t1.tel like concat('%', #{q.userName}, '%') )</if>
|
||||
<if test="q.title != null and q.title != ''"> and t.title = #{q.title}</if>
|
||||
<if test="q.assistantId != null"> and t.assistant_id = #{q.assistantId}</if>
|
||||
<if test="q.startDate != null and q.startDate != ''"> and t.create_time >= #{q.startDate} </if>
|
||||
<if test="q.endDate != null and q.endDate != ''"> and t.create_time <= #{q.endDate} </if>
|
||||
<!-- <if test="q.startDate != null and q.startDate != ''"> and t.create_time >= #{q.startDate} </if>-->
|
||||
<!-- <if test="q.endDate != null and q.endDate != ''"> and t.create_time <= #{q.endDate} </if>-->
|
||||
</where>
|
||||
order by t.id desc
|
||||
</sql>
|
||||
|
||||
@@ -25,29 +25,32 @@
|
||||
<!-- 通用查询结果列 -->
|
||||
<sql id="Base_Column_List">
|
||||
t.id, t.created_by, t.created_time, t.chat_id, parent_message_id, t.message_id, t.model, t.model_version, t.role, t.content, t.content_type, t.finish_reason,
|
||||
t.status, t.app_key, t.used_tokens, t.response, t.del
|
||||
t.status, t.app_key, t.used_tokens, t.response, t.is_del
|
||||
</sql>
|
||||
|
||||
<!-- 通用查询条件 -->
|
||||
<sql id="BaseSelect">
|
||||
select <include refid="Base_Column_List"></include>, t1.title as chatTitle
|
||||
from gpt_chat_message t
|
||||
left join gpt_chat t1 on t.chat_id = t1.id
|
||||
from ai_gpt_chat_message t
|
||||
left join ai_gpt_chat t1 on t.chat_id = t1.id
|
||||
<where>
|
||||
t.del = 0
|
||||
<if test="q.id != null and q.id != ''"> and t.id = #{q.id} </if>
|
||||
t.is_del = 0
|
||||
<if test="q.chatId != null"> and t1.id = #{q.chatId}</if>
|
||||
<if test="q.chatNumber != null"> and t1.chat_number = #{q.chatNumber}</if>
|
||||
<if test="q.userId != null and q.userId != ''"> and t1.user_id = #{q.userId}</if>
|
||||
<if test="q.messageId != null"> and t.message_id = #{q.messageId}</if>
|
||||
<if test="q.model != null and q.model != ''"> and t.model = #{q.model}</if>
|
||||
<if test="q.role != null and q.role != ''"> and t.role = #{q.role}</if>
|
||||
<if test="q.appKey != null and q.appKey != ''"> and t.app_key = #{q.appKey}</if>
|
||||
<if test="q.finishReason != null and q.finishReason != ''"> and t.finish_reason = #{q.finishReason}</if>
|
||||
<if test="q.status != null"> and t.status = #{q.status}</if>
|
||||
<if test="q.startDate != null and q.startDate != ''"> and date_format(t.created_time,'%Y-%m-%d') >= #{q.startDate} </if>
|
||||
<if test="q.endDate != null and q.endDate != ''"> and date_format(t.created_time,'%Y-%m-%d') <= #{q.endDate} </if>
|
||||
</where>
|
||||
order by t.created_time desc
|
||||
<if test="q.size != null and q.size > 0">
|
||||
LIMIT #{q.size}
|
||||
<if test="q.current != null and q.current > 0">
|
||||
OFFSET #{q.offset}
|
||||
</if>
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="pageChatMessage" resultType="com.hula.ai.gpt.pojo.vo.ChatMessageVO">
|
||||
|
||||
@@ -22,20 +22,33 @@
|
||||
|
||||
<!-- 通用查询结果列 -->
|
||||
<sql id="Base_Column_List">
|
||||
t.id, t.created_by, t.created_time, t.updated_by, t.updated_time, t.name, t.icon, t.model, t.version, t.local_model_type, t.model_url, t.knowledge, t.sort, t.status, t.del
|
||||
t.id, t.created_by, t.created_time, t.updated_by, t.updated_time, t.name, t.icon, t.model, t.version, t.local_model_type, t.model_url, t.knowledge, t.sort, t.status, t.is_del
|
||||
</sql>
|
||||
|
||||
<!-- 通用查询条件 -->
|
||||
<sql id="BaseSelect">
|
||||
select <include refid="Base_Column_List"></include>
|
||||
from gpt_model t
|
||||
from ai_gpt_model t
|
||||
<where>
|
||||
t.del = 0
|
||||
<if test="model != null and model != ''"> and t.model = #{model}</if>
|
||||
t.is_del = 0
|
||||
<!-- 针对 pageModel 和 listModel -->
|
||||
<if test="q != null and q.model != null and q.model != ''"> and t.model = #{q.model}</if>
|
||||
</where>
|
||||
order by t.sort desc, t.id desc
|
||||
</sql>
|
||||
|
||||
<!-- 单条查询条件 -->
|
||||
<sql id="GetSelect">
|
||||
select <include refid="Base_Column_List"></include>
|
||||
from ai_gpt_model t
|
||||
where t.is_del = 0
|
||||
<if test="model != null and model != ''">
|
||||
and t.model = #{model}
|
||||
</if>
|
||||
order by t.sort desc, t.id desc
|
||||
limit 1
|
||||
</sql>
|
||||
|
||||
<select id="pageModel" resultType="com.hula.ai.gpt.pojo.vo.ModelVO">
|
||||
<include refid="BaseSelect"></include>
|
||||
</select>
|
||||
@@ -45,8 +58,7 @@
|
||||
</select>
|
||||
|
||||
<select id="getModel" resultType="com.hula.ai.gpt.pojo.vo.ModelVO">
|
||||
<include refid="BaseSelect"></include>
|
||||
limit 1
|
||||
<include refid="GetSelect"></include>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
BIN
preview/wx.png
BIN
preview/wx.png
Binary file not shown.
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 140 KiB |
@@ -188,8 +188,8 @@ CREATE TABLE `ai_gpt_comb` (
|
||||
`price` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '价格',
|
||||
`status` smallint NULL DEFAULT 1 COMMENT '状态 0 禁用 1 启用',
|
||||
`remark` varchar(250) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '' COMMENT '备注',
|
||||
`created_by` bigint NOT NULL COMMENT '创建者',
|
||||
`created_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`created_by` bigint DEFAULT 0 NOT NULL COMMENT '创建者',
|
||||
`created_time` datetime DEFAULT (now()) NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` bigint NULL DEFAULT NULL COMMENT '更新者',
|
||||
`updated_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`is_del` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
|
||||
Reference in New Issue
Block a user